AngularJS 项目采坑笔记

有段时间没写博客了,前段时间在看 Underscore 源码所以写的多了点,这段时间还是在忙自己的其他项目去了,还是有不少收获的。

Angular 视图过渡动画

之前使用 angular-promise-button 这个模块实现了按钮的自动变化,以前自己是用很多标志位来判断特别二。不仅如此,页面切换动画也是用标志位判断,这样就特别不好维护特别不优雅,上次重构的时候就把这些全部去掉了。但是问题来了,页面数据未到达时候页面就渲染肯定会造成视觉上的问题,怎么解决呢。
我们都想写一些应用很广的代码,比如指令,比如上面这个 angular-promise-button 模块等等。其实要解决上面的问题,也是几行代码就可以解决的事情了。
我所使用的是 Angular 的 ui-router。ngRoute 应该也差不多。
在 ui-router 中可以使用 resolve 达到在控制器初始化以及视图加载前确保数据到达。比如:

$stateProvider
    .state('me',{
        url: '/me',
        controller: 'MeCtrl',
        templateUrl: 'me/me_tpl.html',
        controllerAs: 'vm',
        nav: true,
        resolve: {
            me: function(userservice) {
                return userservice.getUserInfo()
                    .then(response => response);
            }
        }
    })

只有 resolve 中的全部方法执行完后,才会开始初始化控制和加载视图。这个数据如果在控制器或者视图中要使用,可以在控制器中进行依赖注入。例如上面这个我的控制器是这样写的:

(function(){
    'use strict';
    
    angular
        .module('index')
        .controller('MeCtrl', MeCtrl);
        
    MeCtrl.$inject = ['me'];
    
    function MeCtrl(me) {
        let vm = this;
        vm.user = me;
    }
}());

resolve中的方法是阻塞页面进行的,这样就会带来问题了,如果数据请求比较久将导致网站停滞,我们这时候就希望可以有过渡动画出来。要达到全局过渡效果的作用,可以直接监听 $rootScope 中的三个状态即 $stateChangeStart$stateChangeSuccess 以及 $stateChangeError 事件。例如上面这个例子中,当我们触发 me 这个 state 时,也就触发了 $rootScope 上的 $stateChangeStart 事件,当处理结束后将出发 $stateChangeSuccess 并加载视图, 处理失败就会触发 $stateChangeError 事件。代码如下:

    angular
        .module('index', [
            'ui.router',
            'ui.bootstrap',
            'ngAnimate',
            'ngSanitize',
            'ngTouch',
            'infinite-scroll',
            'angularPromiseButtons'
        ])
        .config(config)
        .run(($state,$rootScope) => {
            $rootScope.$state = $state;
            $rootScope.$on("$stateChangeStart", (event, toState, toStateParams, fromState, fromStateParams) => {
                var isLoading = toState.resolve;
                if(!isLoading) {
                    for (var prop in toState.views) {
                        if (toState.views.hasOwnProperty(prop)) {
                            if(toState.views[prop].resolve) {
                                isLoading = true;
                                break;
                            }
                        }
                    }
                }
                if (isLoading) {
                    $rootScope.loading = true;
                }
            });
            $rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
                $rootScope.loading = false;
            });
            $rootScope.$on("$stateChangeError", (event, toState, toParams, fromState, fromParams, error) => {
                $rootScope.loading = false;
            });
        });

入口页面,省去了其他代码,这里第一行就是视图,第二行是加载动画,通过ng-show来控制显示。第三行是引入导航栏,这个在后面会说下。

<div ui-view class="uiview" ng-show="!$root.loading"></div>
<div class="cssload-thecube" ng-show="$root.loading"> loading... </div>
<div ng-show="$state.current.nav" ng-include="'navbar/navbar_tpl.html'"></div>

可以看到上面的代码中是监听了 $stateChangeStart 事件,然后获取目标 state 上的 resolve 方法,当 state 上的 resolve 方法全部结束后,$rootScope.loading 设置为 false,否则保持为 true。
当监听到 $stateChangeSuccess 或者 $stateChangeError 事件时,置 $rootScope.loading 为 false,退出过渡动画。在视图中可以使用 $root 得到 $rootScope
可以看到这里有很多参数,可见其功能是很强大的。

再看下上面这个第三行

<div ng-show="$state.current.nav" ng-include="'navbar/navbar_tpl.html'"></div>

结合上面的 JS 代码来看,我已经把 $state 注入到了 $rootScope 中了,之后我就可以使用 $state.current 来获取当前的视图状态和信息。我需要实现导航栏仅仅出现在我指定的页面中,下方按钮可以根据当前视图来激活。第一点可以通过给路由 state 补充变量比如我这里的 nav 来实现,需要导航栏的地方就设置 nav 为 true, 否则就不设置。第二点则可以利用 ui-sref-active 来实现。如下:

<!-- navbar/navbar_tpl.html -->
<nav class="bookist-nav navbar navbar-default" role="navigation">
    <ul class="nav navbar-nav">
        <li ui-sref-active="active"><a ui-sref="index"><i class="fa fa-home fa-lg"></i><p>首页</p></a></li>
        <li ui-sref-active="active"><a ui-sref="booklists"><i class="fa fa-th-large fa-lg"></i><p>书单</p></a></li>
        <li ui-sref-active="active"><a ui-sref="cart"><i class="fa fa-shopping-cart fa-lg"></i><p>购物车</p></a></li>
        <li ui-sref-active="active"><a ui-sref="me"><i class="fa fa-user fa-lg"></i><p>我的</p></a></li>
    </ul>
</nav>

最后还有一个地方就是,下面这个代码

<div ui-view class="uiview" ng-show="!$root.loading"></div>

ui-view 的用法其实还不少,如果你有去注意的话,会看到这个视图在变化的时候其类名会变化,依次我们可以结合 angular-animate 来实现切换动画。注意这个切换动画是在视图加载后才开始的,和上面的不一样,如果同时使用,则会在上方过渡效果结束后触发。例如,我们可以实现淡入淡出:

.uiview {
  &.ng-enter {
    transition: .5s;
    opacity: 0;
  }
  &.ng-enter-active {
    opacity: 1;
  }
}

所以上面其实说了好几点:

  • 使用路由上的 resolve 来实现在控制器初始化前获取到需要的数据
  • 监听 $rootScope 上的事件来实现 resolve 等待动画
  • 在 ui-view 上通过使用 angular-animate 来实现视图的切换动画
  • 通过 ui-sref-active 在当 ui-sref 和当前 state 一致时激活 active 类名
  • 把 $state 注入到 $rootScope 达到在视图中获取 $state 用途
  • 使用 $root 得到 $rootScope,利用 $root 获取 $rootScope 上的对象

静态资源自动发布七牛云

这个很简单啦,我使用了 gulp-qiniu 这个模块来实现,很简单,结合前面说的 gulp-usemin 就更完美了。

<!-- build:js //cdn.bookist.org/bookist.min.js -->
<script src="src/js/bookist.js"></script>
<script src="src/js/templates.js"></script>
<!-- endbuild -->

例如对上面 index.html 中的这一片段,我们要在开发环境中使用本地资源,而在线上环境则使用 CDN 资源。我们可以这样配置 gulpfile。

gulp.task('cdn', () => {
    gulp.src('index.html')
        .pipe(usemin())
        .pipe(gulp.dest('backend/app/templates'));
    gulp.src(['./src/js/bookist.js', './src/js/templates.js'])
        .pipe(plumber())
        .pipe(uglify())
        .pipe(babel())
        .pipe(concat('bookist.min.js'))
        .pipe(qiniu({
            accessKey: "xxx",
            secretKey: "xxx",
            bucket: "bookist",
            private: false
        }));
}

运行上面的任务后就会在 backend/app/templates 生成一个修改过的 index.html。对于上面的片段,处理之后是这样的:

<script src="//cdn.bookist.org/bookist.min.js"></script>

前面的注释是给 usemin 使用的,所以要按照规范书写。
同时上面的任务也会把资源发布到七牛云上面,如果你还想指定文件头或者版本号之类的信息,可以参考 gulp-qiniu 的文档进行配置。
上面的方法有一个小问题就是还会在 backend/app/templates 生成一个 cdn.bookist.org 的文件夹,不过无所谓咯,放到 .gitignore 就好了。
至于图片和 css 都差不多吧,就不多说了。

微信坑逼

有段时间我的网站在微信显示部分地方是有问题的,但在手机浏览器和电脑都没任何问题,一开始以为是样式的问题。等到自己要去解决这个问题了,才知道是 js 的问题。
由于微信开发工具不支持 linux,然后我虚拟机的 win10 有不知道什么原因一直连接不到手机。所以就手动调试了,这边改一点,发布上去,然后等微信缓存没了看效果(微信恶心的缓存...我都叫用苹果的人帮我看,因为苹果可以刷新..)。就这样一步步看,最后定位了问题代码。中间过程就不描述了,直接看:

for(let book of books) {
    if(book.rate)   book.star = Math.ceil(book.rate/2);
    else if(book.star)   book.star = Math.ceil(book.star/2);
}

省略了无关代码,这是一个方法里面的部分代码,如果一个对象传入这个方法中,在微信会得不到返回值,在手机浏览器和电脑上都正常。
虽然这里使用了 ES6 的语法,但是我其实已经大量使用 let...of... 语句,项目并没有出现太大问题,就只是上面描述的一些地方异常而已。再者我使用了 babel 转码了。越想越觉得没道理啊。。。
最后虚拟机换了 win7 然后手机连接上了,打开了微信开发工具,调试微信 webview,报错 Symbol is not defined
果断控制台敲 Symbol,结果 Symbol is not defined
好吧,问题出来了,微信这个辣鸡不支持 Symbol...
但问题是我没有用 Symbol 这个东西啊,我瞄了 babel 一眼。
看下上面那段代码转码的结果:

var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;

try {
    for (var _iterator5 = books[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
        var book = _step5.value;

        if (book.rate) book.star = Math.ceil(book.rate / 2);else if (book.star) book.star = Math.ceil(book.star / 2);
    }
} catch (err) {
    _didIteratorError5 = true;
    _iteratorError5 = err;
} finally {
    try {
        if (!_iteratorNormalCompletion5 && _iterator5.return) {
            _iterator5.return();
        }
    } finally {
        if (_didIteratorError5) {
            throw _iteratorError5;
        }
    }

什么鬼...算了,不看了,但是确定了是 babel 转码导致的。然后查了下资料:

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator, Generator, Set, Maps, Proxy, Reflect, Symbol, Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

其实早有耳闻 polyfill 这个东西,但我想他一般是用在 IE 这种辣鸡浏览器上的,没想到微信 webview 这么不争气。
多打包一个 babel-polyfill 解决了这个问题。
坑比微信...

故事还没结束...
昨天发现网站并没有使用 http2,但是已经进行了设置了,最后查了很多资料原因好像是 openssl 的问题,但由于使用的是 docker,貌似不太好搞,但其实更换 nginx 版本就好了,原先是1.9版本,换成1.11-alpine 版就解决了,用 docker 更换 nginx 版本非常方便。额,具体原因不深究了,可能跟 alpine 这个字眼有关?不清楚...
http2 具备多路复用的特点,在 http1.1 中,并行传输文件是有限制的,因为用户端和服务端的最大连接数是有限制的,而连接的建立和销毁又会带来开销,所以在 http1.1 中对文件进行压缩合并是很有必要的。不过在 http2 就不需要这样做了,http2 可以在一条通道上传输多个文件,如果合并剩几个,就没法发挥并行传输的优势,而且文件太大,还会降低运输层的效率,即丢包或者乱序到达的影响。
我把网站改到了 http2 后,就不再进行文件合并了,转而可以大量使用 bootcss 的 CDN,bootcss 的 CDN 支持 http2,传输很快。至于上面没有的和自己写的,就发布到七牛云上面。恩,电脑上加载是变快了很多。
但是感觉不到微信加载变快...最后发现是微信不支持 http2...
好伤心...
最后我的方案是产生两个 css 文件和两个 js 文件,之所以是两个,因为一个是自己写的,经常变,另一个是用别人的,几乎不会变。

最后又发现微信好像支持 spdy... 心好累,算了,降了 nginx 版本开启 spdy 不理了。


总结:

  • Angular 是一个大而全的框架,我觉得很强大很牛逼,越来越喜欢 Angular 了
  • 使用 CDN 可以大大的加速,尽量使用 CDN
  • 微信这个坑比我就不说了,不支持 Symbol 不支持 http2
  • 能用 http2 就尽量用 http2
  • 考虑浏览器兼容性,根据需要引入 babel-polyfill