标签 Angular 下的文章

关于前后端分离鉴权的思考

前后端分离项目的 Token 存储问题由来已久,有的人存 Cookie 有的人存 LocalStorage 或 SessionStorage。在之前的个人项目中,曾经花时间去研究这个问题,以下分享下我的看法。

如何安全的传输用户 token

这是最传统也是最简单的方式了,前端登录,后端根据用户信息生成一个 token,并保存这个 token 和对应的用户 id,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个 cookie,后端根据这个 cookie 来标识用户。

flow-cookie-session.jpg

但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。

尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。另外,后端每次都需要根据 token 查出用户 id,这就增加了数据库的查询和存储开销。

在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。

secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

CSRF.jpg

看起来我们不能兼顾。确实,光依靠这一个 token 我们没办法兼顾这两点。既然一个不够,那就两个。于是有了 XSRF-TOKEN,它和作为用户令牌的 token 类似,也是服务器生成的一个散列值。我们把 token 通过 httpOnly 发回去,把 XSRF-TOKEN 直接发回去。我们可以无视 httpOnly 的 cookie 因为我们没法操纵它,但对于这个 XSRF-TOKEN,我们就可以在我们网站的每个请求中都加入到 header 里面去。而服务端就需要检查这个 header 的 XSRF-TOKEN 是否真实有效。

- 阅读剩余部分 -

Angular 实践总结

搞了快一年的 Angular,Angular 真的是一个非常强大非常齐全非常好用的框架,而且拥有强大的社区支持,虽然踩了很多坑,但是仍然无悔学习了这样一个框架。Angular 2 已经正式发布了,我也打算学 Angular2 去了,现在手头上还有一个用 Angular 的项目,这个项目我是想把我学到的很多 Angular 的最佳实践都用进去,借此我就想干脆也写几篇博客总结下好了,我写的比较散,想到什么就说什么。

使用单次绑定或单向绑定

从 Angular 1.3 开始就有了 once-time binding,Angular 1.5 开始支持了指令和组件的 one-way binding。看看他们的用法:

单次绑定很简单,加上:: 就可以了。

<p>{{::vm.title}}</p>

An expression that starts with :: is considered a one-time expression. One-time expressions will stop recalculating once they are stable, which happens after the first digest if the expression result is a non-undefined value (see value stabilization algorithm below).

当 digest 结束之后并且单次绑定的值不为 undefined 时,这个值将不再被监听。官方称这个算法为 Value stabilization algorithm 还解释了一下。看到这个,再想起 Angular 源码那注释,觉得真的业界良心啊。想具体了解下单次绑定,出门右转 -> docs.angularjs

在视图进行单向绑定也很简单,使用 ng-bind 就可以了,这里主要说的是指令和组件中的单向绑定。

其实 Angular1 还是不断再发展,现在最新的是 1.5.9,加了很多新的东西,也做了很多性能优化。单向绑定也是 1.5 之后才引入的新东西。1.5 也引入了一个新的东西叫 component,和 directive 差不多,具体我没用过我也不太了解,不过写法简洁了很多。单向绑定主要就是用于 component 和 directive 的。

随便找个比较简单的例子来说下,

- 阅读剩余部分 -

Angular1 之折腾记

前几天 Angular2 正式发布了,虽然他也在我的学习计划里面,但我并没有把他应用在我最近开展的一个项目中。最近在写一个 Rss 订阅器,基于 Angular1 和 Koa2(总感觉两个有点不搭 =.= ),主要是不想在这个项目花太长时间,再者我还想集我目前掌握的所有技术之大成写一个能拿的出手的项目,所以就没有选择 Angular2 了。至于 Koa2,其实很早就想学了,只是之前一直在忙别的。

angular-resource 介绍

今天捣鼓 Angular 的 Resource 功能,前端后端都掌握在自己的手上时去用 Angular 的 Resource 特别舒服,大大减少了代码量,特么强大。

(function() {
    angular
        .module('app')
        .factory('Post', $resource => {
            return $resource('/api/feed/:feed_id/post/:id', {id: '@_id'}, {
                update: {method: 'PUT'},
                get: {method: 'GET', cache: true}
            });
        })
}());

在上面我定义了一个 Post 资源,一旦创建完成后,他就自动拥有了以下方法。

{
    'get': {method: 'GET'},
    'save': {method: 'POST'},
    'query': {method: 'GET', isArray: true},
    'remove': {method: 'DELETE'},
    'delete': {method: 'DELETE'}
}

有些 IE 浏览器可能不支持 delete,这时候可以使用 remove
我们还可以自定义或者修改里面的方法,比如我上面中就自定义了一个 update 方法以及给 get 方法开启了缓存。

- 阅读剩余部分 -

浅谈 Angular 脏检查

Angular 的脏值检查机制一直是 Angular 被人诟病的地方,但瑕不掩瑜,Angular 还是一个非常优秀的框架,并且 Angular2 也已经抛弃了这个脏值检查的算法。
最近在看《AngularJS 深度剖析与最佳实践》,不得不说是一本很好的书籍,作者在第三章开始讲背后的原理,这里分析了 Angular 的 $digest 函数,即脏检查机制。所以自己也去下载了 Angular 最新的源码去瞧了下,然后做下笔记吧。

首先要注意,Angular 的 digest 的触发不是定时的,只有在指定的事件触发之后才会进入 $digest。基本上我们用的带 $ 的东西调用之后都可能会触发 digest。比如我们使用 setTimeout 就不会触发 digest,即当你使用 setTimeout 更改 viewmodel 的值后,它不会同步的反映到用户的视图中去,解决方法有两个,一个是使用 Angular 提供的 $timeout 替代 setTimeout$timeout 会在执行结束之后自动触发 digest; 另一个方法是手动调用 $apply,$apply 是 Angular 对 digest 的一层封装,我们一般不会直接调用 digest 而是通过使用 $apply 方法。比如对于 setTimeout,我们就可以这样触发 digest。

setTimeout(() => {
  $scope.$apply(() => {
    $scope.test = 123;
  })    
}, 500);

我们看一个例子,这也是 Angular 源码 $digest 部分的一个示例。

var scope = ...;
scope.name = 'misko';
scope.counter = 0;

expect(scope.counter).toEqual(0);
scope.$watch('name', function(newValue, oldValue){
  scope.counter = scope.counter + 1;
});
expect(scope.counter).toEqual(0);

// 执行第一次 digest,第一次 digest 会遍历全部的 watcher,并触发上面的方法,从而使的 count+1
scope.$digest();
expect(scope.counter).toEqual(1);

// 第二次调用时,由于上一次调用检查 name 不脏,所以不会再去处理
scope.$digest();
expect(scope.counter).toEqual(1);

// 第三次调用时,由于 name 发生了变化,使得当前值和上一次保存的值不同,所以会触发起 $watch 方法
scope.name = 'adam';
scope.$digest();
expect(scope.counter).toEqual(2);

Angular 的脏值检查过程大致如下:
对当前作用域和子作用域上的 $$watchers 进行遍历,$$watches 保存着 scope 上的所有变量以及其 $watch 方法,调用时会取当前值和上一次值进行比较,如果不相等则会调用 $watch 方法,同时会保存当前的值以在下一次进行比较,并且记录此次检查结果为脏。然后重复进行直到数据不脏为止,因此至少要 digest 两次,超出 10 次会报错,可以调高这个次数限制。当数据不再脏即 model 稳定下来之后, Angular 才会开始一次性批量更新 UI。从而减少了浏览器的 repaint 次数,提升性能。

深入到源码来看:

- 阅读剩余部分 -

项目采坑笔记

有段时间没写博客了,前段时间在看 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

一次项目重构

上学期由于期末停工的项目又要继续开展了,然而停了一个多月的时间,我已经看不下去他的代码了,简直惨不忍睹,花了我将近40个小时的时间去做了重构。虽然重构说明有进步了,但是一改就要改几十个页面啊...累觉不爱..说一说这次将近40小时的重构吧。

git-diff.jpg

Angular 重构

项目是基于 Angular 的 SPA,项目参考Angular规范进行重构,主要是以下几点:

  • 把控制器的业务逻辑(主要是 HTTP 请求)分离到 Factory
  • Controller 和 Directive 以及 Factory 全部用立即函数包装
  • Controller 和 Directive 以及 Factory 内部书写格式
  • 使用 controllerAs 代替 $scope
  • 全部 JavaScript 文件使用 use strict 严格模式
  • 利用单体做部分数据的缓存
  • 提取大部分可复用模块到 directive
  • 全部 ng-repeat 加上 track by
  • 过大的试图使用 ng-include 进行分离
  • 去掉全部辅助变量,用 angular-promise-buttons 来达到按钮状态变化
  • 去掉全部页面切换动画
  • 手动进行依赖注入
  • 使用 ES6 语法,用 babel 转为 ES5
  • 使用 eslint 来做代码格式检查

之前我几乎没有使用 Factory 这一层,全部业务逻辑都在 Controller 里面做,随着项目越来越大(有26个页面),页面之间函数重复的情况很多,而且控制器太厚,可读性差,给维护带来了巨大的困难。在这次重构之中,我把全部的 HTTP 请求全部放在 Factory 实现,从而做到了以下几点:

  • 函数复用,多个控制器用一个 Factory,避免同个函数多次书写
  • HTTP 请求返回 promise,结合 angular-promise-buttons 做到了按钮状态的自动变化以及过渡效果,去掉了先前实现同样目的的全部辅助变量
  • 对部分相对不变的数据,在第一次缓存后直接在该 Factory 进行缓存,第二次获取的时候直接返回内存中的数据,加快了部分页面的二次加载速度,对跨页面你的同个请求同样有效
  • 容易做单元测试和更改逻辑,因为全部 HTTP 请求都放在 Factory 实现,对后期修改以及代码测试都带来了很大的方便

- 阅读剩余部分 -