Devil May Code...

Vergil's Blog

上一篇博文提到,是由于jQuery.val()方法设置DOM元素的值的时候,并没有触发AngularJS的$digest循环。然后我写了一个directive来解决这个问题。

那是否可以有一个更加通用的方法,让AngularJS适应其他使用val()方法设置值的jQuery插件呢?如何让val()自动触发DOM事件??

jQuery Hooks

豆浆油条求助,得知jQuery有一个API,称为“hook(钩子)”(怪自己不去看官方文档)。

Hooks可以使用在val()css()attr()prop()这些getter/setter方法上。如果你设置了hook,当你调用这些函数的时候,jQuery会触发你hook定义的方法。

Hooks签名如下:

var someHook = {
    get: function(elem) {
        // obtain and return a value
        return "something";
    },
    set: function(elem, value) {
        // do something with value
    }
}

.val() Hooks

valHooks允许我们拦截.val()的功能

HTML:

<input type="text" id="t" />

JavaScript:

$.valHooks.text = {
    set: function(elem, val) {
        elem.value = 'hello  ' + val;
        return true;
    },
    get: function(elem) {
        return 'fuck';
    }
}

$('#t').val('world!');      //设置值为:hello world!
$('#t').val();              //返回:fuck

.css() Hooks

以后补充

.prop()和.attr() Hooks

以后补充

使用valHooks让jQuery.val()与AngularJS模型数据同步

知道Hooks之后,我们就可以拦截.val()的功能,在设置好值之后,触发一个让AngularJS进入$degist的情况。比如,触发input事件

Example:

<!doctype html>
<html ng-app="app">
    <head>
        <script src="http://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
        <script src="http://cdn.bootcss.com/angular.js/1.4.8/angular.min.js"></script>
        <script>
            $(function(){
                $('#setvalue').on('click', function() {
                    $('#input').val('hello world!');
                });

                $.valHooks.text = {
                    set: function(elem, val) {
                        elem.value = val;   //把DOM元素设置为传进来的val值
                        $(elem).trigger('input');  //触发input事件
                        return true;
                    }
                };
            });
        </script>
    </head>
    <body>
        <div>
            <input type="text" ng-model="value" id="input" />
            <button type="button" id="setvalue">jQuery set value</button>
            <p>ng model value is:{{value}}</p>
        </div>
    </body>
</html>

OK,那么现在,使用val()方法设置元素的值之后,也可能让AngularJS的模型同步了。

一般情况下,这样子确实可以解决问题,但如果你不想让它触发input事件呢?比如说,你的input事件有另外的功能。

我的思路就是,触发一个自定义事件,写一个directive监听该事件。

Example:

<!doctype html>
<html ng-app="app">
    <head>
        <script src="http://cdn.bootcss.com/jquery/2.2.0/jquery.min.js"></script>
        <script src="http://cdn.bootcss.com/angular.js/1.4.8/angular.min.js"></script>
        <script>
            $(function(){
                $('#setvalue').on('click', function() {
                    $('#input').val('hello world!');
                });

                $.valHooks.text = {
                    set: function(elem, val) {
                        elem.value = val;
                        $(elem).trigger('jquery.set.value');    //触发自定义事件
                        return true;
                    }
                };
            });
        </script>
    </head>
    <body>
        <div>
            <!-- 注意这个input标签使用了jqSetValue组件 -->
            <input type="text" ng-model="value" id="input" jq-set-value />
            <button type="button" id="setvalue">jQuery set value</button>
            <p>ng model value is:{{value}}</p>
        </div>
        <script>
            angular.module('app', []).directive('jqSetValue', function() {
                return {
                    restrict: 'AC',
                    require: '?ngModel',
                    link: function(scope, element, attrs, ngModel) {
                         //设置自定义事件的回调函数
                        element.on('jquery.set.value', function(event) {
                            if(!ngModel) return;
                            scope.$apply(function(){
                                ngModel.$setViewValue($(element).val());
                            });
                        });
                    }
                };
            });
        </script>
    </body>
</html>

今天在AngularJS下使用Fuel UXspinbox时出现了问题:

如果手动输入input的值,一切正常。但如果使用spinbox的增加/减少按钮时,DOM已经被改变,$scope却无法同步修改后的值。

示例代码如下:

<div class="spinbox" data-initialize="spinbox" id="mySpinbox">
  <input type="text" class="form-control input-mini spinbox-input" ng-model="number">
  <div class="spinbox-buttons btn-group btn-group-vertical">
    <button type="button" class="btn btn-default spinbox-up btn-xs">
      <span class="glyphicon glyphicon-chevron-up"></span><span class="sr-only">Increase</span>
    </button>
    <button type="button" class="btn btn-default spinbox-down btn-xs">
      <span class="glyphicon glyphicon-chevron-down"></span><span class="sr-only">Decrease</span>
    </button>
  </div>
</div>

Why?

查看FuelUX源码可以发现它是使用jQuery的val()方法去修改DOM的值的(猜也猜到-_-||)。所以并没有启动$digest循环,详细看这篇文章

解决办法

最简单的方法就是直接改FuelUX的源码,找到它调用render()方法之后,触发一下input事件:

this.$element.trigger('input');     

不过这种修改别人源码的做法非常不好

那么,我写了一个directive来更新模型:

angular.module('your-app').directive('spinbox', ['$parse', function($parse) {
    return {
        restrict: 'C',      //以ClassName的形式被声明
        link: function(scope, element, attrs) {
            element.on('changed.fu.spinbox', function() {
                var $spinbox = angular.element(this);
                var $input = $spinbox.find('.spinbox-input');
                var model = $input.attr('ng-model');

                if (!model) return;

                var setter = $parse(model).assign;
                scope.$apply(function(){
                    setter(scope, $spinbox.spinbox('value'));
                });
            });

        }
    }
}]);

使用jQuery钩子与AngularJS模型数据同步

OK,如果有更好的方法,欢迎讨论:)


转自阮一峰的网络日志


我每天使用 Git ,但是很多命令记不住。
一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。
Workspace:工作区
Index / Stage:暂存区
Repository:仓库区(或本地仓库)
Remote:远程仓库

阅读剩余部分...


本文是GitHub上的文章的索引。


PHP编码规范(中文版)导读

本文档是PHP互操作性框架制定小组(PHP-FIG :PHP Framework Interoperability Group)制定的PHP编码规范(PSR:Proposing a Standards Recommendation)中译版。

翻译过程中参照了 莫希爾(Mosil)手札 的繁体中文版,以及 Corrie Zhao 组织翻译的简体中文版,
译文中为了让语句通顺,便于理解,没有对原文逐字翻译,个别语句与原文原意可能略有偏差,希望告知指正。

目前官方已制定的规范包括以下六份文件:

  • 2014/04/25 添加PSR-2补充文件以及修改之前版本中的翻译不当与错误。
  • 2014/07/31 添加PSR-4

以下是原版的导读:


PHP互操作性框架制定小组

组建本小组的目的是,通过在各项目的代表之间讨论他们共同的编码规范,以制定一个协作标准。本规范的主要面向对象是本小组的各个组成成员,当然,同时也欢迎关注本规范的其它PHP社区采用本规范。


今天在Github上发现了一个挺有意思的PHP项目:PsySH。
百度了一番,发现没有任何关于它的中文文章,经过研究,决定写本篇博文来讲述一下。
如果对你有所帮助,请留下你的回复

PsySH

PsySH is a runtime developer console, interactive debugger and REPL for PHP.

PsySH是一个PHP的运行时开发平台,交互式调试器和Read-Eval-Print Loop (REPL)。

说的简单点,就像你用firebug的console调试你的JavaScript代码一样。

安装

官网介绍了3种安装方式:

  • 直接下载
  • Composer安装
  • 直接cloneGitHub仓库的代码

我比较建议选择Composer安装,因为这个项目还有其他的依赖项目,用Composer很好解决这个问题。

以下教程以OS X和Windows 10为例,假定已经安装phpComposer并已经将它们设置为系统环境变量:

OS X

先用Composer下载PsySH,这里用的是全局安装:

$ composer global require psy/psysh

安装完毕后,PsySH已经安装到/Users/{用户名}/.composer/vendor/psy/psysh目录下

此时,你可以直接运行:

$ /Users/{用户名}/.composer/vendor/psy/psysh/bin/psysh

为了使用方便,建议将它加入到环境变量:

$ echo 'export PATH="/Users/{用户名}/.composer/vendor/psy/psysh/bin:$PATH"' >> ~/.bashrc
$ source ~/.bashrc

Windows

同样地,使用Composer安装,按win键+R键,输入cmd,打开windows控制台,然后:

composer global require psy/psysh

安装完成后,PsySH被安装到C:\Users\{用户名}\AppData\Roaming\Composer\vendor\psy\psysh

因为bin/psysh文件并不是windows的可执行文件,所以需要使用以下命令运行PsySH

php C:\Users\{用户名}\AppData\Roaming\Composer\vendor\psy\psysh\bin\psysh

为了使用方便,在C:\Users\{用户名}\AppData\Roaming\Composer\vendor\psy\psysh\bin目录下新建一个名为psysh.bat的文件,其内容如下:

@ECHO OFF
php "%~dp0psysh" %*

此时,把C:\Users\{用户名}\AppData\Roaming\Composer\vendor\psy\psysh\bin加入到系统的环境变量PATH,以后可以直接在cmd下运行psysh了:

C:\Users\Vergil>psysh
Psy Shell v0.6.1 (PHP 5.6.8 — cli) by Justin Hileman
>>>

阅读剩余部分...


转自:WooYun知识库


0x01 前言:

Charles是一款抓包修改工具,相比起burp,charles具有界面简单直观,易于上手,数据请求控制容易,修改简单,抓取数据的开始暂停方便等等优势!下面来详细介绍下这款强大好用的抓包工具。

0x02 下载与安装

首先是工具下载和安装 首先需要下载java的运行环境支持(一般用burp的人肯定也都有装java环境)。装好java环境后,可以直接去百度搜索并下载charles的破解版,下载到破解版之后,里面一般会有注册的jar文件,然后注册后就可以永久使用了(ps:不注册的话,每次使用30分钟,工具就会自动关闭)。

0x03 PC端抓包

下面是pc端的抓包使用情况 Charles支持抓去http、https协议的请求,不支持socket。

然后charles会自动配置IE浏览器和工具的代理设置,所以说打开工具直接就已经是抓包状态了。 这里打开百度抓包下,工具界面和相关基础功能如下图所示:

上图中的7个位置是最常用的几个功能。

1 那个垃圾桶图标,功能是clear,清理掉所有请求显示信息。

2 那个望远镜图标,功能是搜索关键字,也可以使用ctrl+f实现,可以设置搜索的范围。

3 圆圈中间红点的图标,功能是领抓去的数据显示或者不显示的设置。 这个本人认为是charles工具很方便的一个两点,一般都使其为不显示抓去状态,只有当自己测试的时候的前后,在令其为抓取并显示状态。这样可以快准狠的获取到相关自己想要的信息,而不必在一堆数据请求中去寻找。

4 编辑修改功能,可以编辑修改任意请求信息,修改完毕后点击Execute就可以发送一个修改后的请求数据包。

5 抓取的数据包的请求地址的url信息显示。

6 抓取的数据包的请求内容的信息显示。

post请求可以显示form形式,直观明了。

7 返回数据内容信息的显示。

其中5、6、7中都有各种形式的数据显示形式,其中raw是原始数据包的状态。

0x04 显示模式

charles抓包的显示,支持两种模式,Structure和Sequence,其优点分别如下。

Structure形式如下图 优点:可以很清晰的看到请求的数据结构,而且是以域名划分请求信息的,可以很清晰的去分析和处理数据。

Sequence形式如下图 优点:可以很清晰的看到全部请求,不用一层一层的去点开,这里是以数据请求的顺序去执行的,也就是说那个请求快就在前面显示。

具体要说两种形式哪个更好,这个就是见仁见智了。本人比较喜欢第二种,粗矿豪放!

0x05 移动APP抓包

这里相比其他抓包软件来说要简单的多了,具体步骤如下:

1 使手机和电脑在一个局域网内,不一定非要是一个ip段,只要是同一个漏油器下就可以了,比如电脑连接的有线网ip为192.168.16.12,然后手机链接的wifi ip为192.168.1.103,但是这个有线网和无线网的最终都是来自于一个外部ip,这样的话也是可以的。

2 下面说说具体配置,这里电脑端是不用做任何配置的,但是需要把防火墙关掉(这点很重要)!

然后charles设置需要设置下允许接收的ip地址的范围。 设置首先要进入这个位置 Proxy - Access Control Settings 然后如果接收的ip范围是192.168.1.xxx的话,那么就添加并设置成192.168.1.0/24 如果全部范围都接收的话,那么就直接设置成0.0.0.0/0

然后如果勾选了Proxy - Windows Proxy 的话,那么就会将电脑上的抓包请求也抓取到,如果只抓手机的话,可以将这个设置为不勾选。

3 接下来下面是手机端的配置

首先利用cmd - ipconfig命令查看自己电脑的ip地址

然后在手机端的wifi代理设置那里去进行相关的配置设置。

这里的代理服务器地址填写为电脑的ip地址,然后端口这里写8888(这个是charles的默认设置),如果自己修改了就写成自己所修改的端口就可以了。

4 好了,这样就配置完成就大功告成了!下面打开UC浏览器或者其他东西,随便访问个网页看有没有抓取到数据就可以了(我这里是直接访问的新浪新闻首页)。

0x06 其他常用功能

相信上面介绍的那些你已经学会了吧,下面再说说charles的一些其他常用的功能

选择请求后,右键可以看到一些常用的功能,这里说说Repeat 就是重复发包一次。 然后Advanced Repeat就是重复发包多次,这个功能用来测试短信轰炸漏洞很方便。

还有比如说修改referer测试CSRF漏洞,修改form内容测试XSS,修改关键的参数测试越权,修改url、form、cookie等信息测试注入等,都非常方便。

好了,这款工具的介绍就到这里了,相信这款方便好用的工具,以后肯定会被更多的人使用到的。

0x07 charles使用问题汇总

Charles是一款很好用的抓包修改工具,但是如果你不是很熟悉这个工具的话,肯定会遇到各种感觉很莫名其妙的状况,这里就来帮你一一解答。

1 为什么下载了不能用啊?打不开啊。

因为charles是需要java环境才能运行的,需要先安装java环境才可以。

2 为什么我用着用着就自动关闭了?大概30分钟就会关闭一次。

因为charles如果没有注册的话,每次打开后就只能哟个30分钟,然后就会自动关闭,所以最好在使用前先按照说明去进行工具的注册操作。

3 为什么我在操作的时候有时候就直接工具就界面卡住死了,关都关不掉,只能用任务管理器才可以关掉?

这个的确是charles这个工具的一个bug,开始用的时候,我也很恶心,而且经常悲剧,但是现在也有相应的解决办法了,下面那样操作就可以了。

首先随便抓些包,要求有图片的请求。

然后选中一个图片的请求,然后分别点击 Response - Raw 然后那里会加载其中的内容,然后加载完毕后,再去随便操作就可以了,就不会在悲剧的直接工具卡死掉了。。。

4 为什么用了charles后,我就上不了网页了,但是qq可以。

因为如果charles是非正常状态下关闭的话,那么IE的代理就不会被自动取消,所以会导致这种情况。

解决办法:

第一种:直接打开charles,然后再正常关闭即可。 第二种:去将IE浏览器代理位置的勾选去掉。

5 为什么我用charles不能抓到socket和https的数据呢?

首先,charles是不支持抓去socket数据的。 然后,如果抓不到https的数据的话,请查看你是不是没有勾选ssl功能。 Proxy - Proxy Settings - SSL 设置

6 为什么我用charles抓取手机APP,什么都是配置正确的,但是却抓不到数据。

首先,请确保电脑的防火墙是关闭状态,这个很重要。

如果,防火墙关了还是不行,那么请把手机wifi断掉后重新连接,这样一般就可以解决问题了。 如果以上方法还是不行的话,那么请将手机wifi位置的ip地址设置成静态ip,然后重启charles工具。

7 抓包后发现form中有些数据显示是乱码怎么办?

请在Raw模式下查看,Raw模式显示的是原始数据包,一般不会因为编码问题导致显示为乱码。

8 我用charles抓手机app的数据,但是同时也会抓去到电脑端的数据,可以设置吗?

可以,设置位置在Proxy - Windows Proxy ,勾选表示接收电脑的数据抓包,如果只想抓去APP的数据请求,可以不勾选此功能。

9 为什么我用IE可以抓到数据,但是用360或者谷歌浏览器就不行?

请确保360或者谷歌的代码设置中是不是勾选设置的是 使用IE代理。

10 想要复制粘贴某些数据的话,怎么办,右键没有相应功能啊?

请直接使用Ctrl +C 和 Ctrl+V 即可。

以上就是charles在使用过程中常见的10中问题和相应的解决情况,有了这个文章,大家就不用在遇到问题的时候懊恼了,嘿嘿。


Angular Team Endorsed

非常感谢领导Angular团队的Igor Minar对本指南做出的审查和贡献,并且委托我继续打理本指南。

目的

Angular规范@john_papa

如果你正在寻找一些关于语法、约定和结构化的Angular应用的一个有建设性的规范,那么你来对地方了。这里所包含的内容是基于我在团队中使用Angular的一些经验、一些演讲和Pluralsight培训课程

这个规范的目的是为构建Angular应用提供指导,当然更加重要的是让大家知道我为什么要选择它们。

如果你喜欢这个规范,请在Pluralsight看看Angular Patterns: Clean Code

Angular Patterns: Clean Code

Community Awesomeness and Credit

Angular社区是一个热衷于分享经验的令人难以置信的社区,尽管Todd Motto(他是我的一个朋友,也是Angular专家)和我合作了多种规范和惯例,但是我们也存在着一些分歧。我鼓励你去看看Todd的指南,在那里你能看到我们之间的区别。

我的许多规范都是从大量的程序会话Ward Bell和我所拥有的而来的,我的好友Ward也影响了本规范的最终演变。

在示例App中了解这些规范

看示例代码有助于你更好地理解,你可以在modular文件夹下找到命名为modular的示例应用程序,随便克隆。

##翻译
Angular规范翻译版本

##目录

  1. 单一职责
  2. IIFE
  3. Modules
  4. Controllers
  5. Services
  6. Factories
  7. Data Services
  8. Directives
  9. 解决Controller的Promises
  10. 手动依赖注入
  11. 压缩和注释
  12. 异常处理
  13. 命名
  14. 应用程序结构LIFT原则
  15. 应用程序结构
  16. 模块化
  17. 启动逻辑
  18. Angular $包装服务
  19. 测试
  20. 动画
  21. 注释
  22. JSHint
  23. JSCS
  24. 常量
  25. 文件模板和片段
  26. Yeoman Generator
  27. 路由
  28. 任务自动化
  29. Filters
  30. Angular文档
  31. 贡献
  32. 许可

单一职责

###规则一

[Style Y001]
  • 一个文件只定义一个组件。

    下面的例子在同一个文件中定义了一个app的module和它的一些依赖、一个controller和一个factory。

/* avoid */
angular
  .module('app', ['ngRoute'])
  .controller('SomeController', SomeController)
  .factory('someFactory', someFactory);

function SomeController() { }

function someFactory() { }

推荐以下面的方式来做,把上面相同的组件分割成单独的文件。

/* recommended */

// app.module.js
angular
    .module('app', ['ngRoute']);
/* recommended */

// someController.js
angular
      .module('app')
      .controller('SomeController', SomeController);

function SomeController() { }
/* recommended */
// someFactory.js
angular
      .module('app')
      .factory('someFactory', someFactory);

function someFactory() { }

返回顶部

IIFE

JavaScript闭包

[Style Y010]
  • 把Angular组件包装到一个立即调用函数表达式中(IIFE)。

为什么?:把变量从全局作用域中删除了,这有助于防止变量和函数声明比预期在全局作用域中有更长的生命周期,也有助于避免变量冲突。

为什么?:当你的代码为了发布而压缩了并且被合并到同一个文件中时,可能会有很多变量发生冲突,使用了IIFE(给每个文件提供了一个独立的作用域),你就不用担心这个了。

/* avoid */
// logger.js
angular
    .module('app')
    .factory('logger', logger);

// logger function会被当作一个全局变量
function logger() { }

// storage.js
angular
    .module('app')
    .factory('storage', storage);

// storage function会被当作一个全局变量
function storage() { }
/**
 * recommended
 *
 * 再也不存在全局变量了
 */

// logger.js
(function() {
    'use strict';

    angular
        .module('app')
        .factory('logger', logger);

    function logger() { }
})();

// storage.js
(function() {
    'use strict';

    angular
        .module('app')
        .factory('storage', storage);

    function storage() { }
})();
  • 注:为了简洁起见,本规范余下的示例中将会省略IIFE语法。

  • 注:IIFE阻止了测试代码访问私有成员(正则表达式、helper函数等),这对于自身测试是非常友好的。然而你可以把这些私有成员暴露到可访问成员中进行测试,例如把私有成员(正则表达式、helper函数等)放到factory或是constant中。

返回顶部

Modules

###避免命名冲突

[Style Y020]
  • 每一个独立子模块使用唯一的命名约定。

为什么:避免冲突,每个模块也可以方便定义子模块。

###定义(aka Setters)

[Style Y021]
  • 不使用任何一个使用了setter语法的变量来定义modules。

    为什么?:在一个文件只有一个组件的条件下,完全不需要为一个模块引入一个变量。

/* avoid */
var app = angular.module('app', [
    'ngAnimate',
    'ngRoute',
    'app.shared',
    'app.dashboard'
]);
你只需要用简单的setter语法来代替。
/* recommended */
angular
      .module('app', [
      'ngAnimate',
      'ngRoute',
      'app.shared',
      'app.dashboard'
  ]);

###Getters

[Style Y022]
  • 使用module的时候,避免直接用一个变量,而是使用getter的链式语法。

    为什么?:这将产生更加易读的代码,并且可以避免变量冲突和泄漏。

/* avoid */
var app = angular.module('app');
app.controller('SomeController', SomeController);

function SomeController() { }
/* recommended */
angular
    .module('app')
    .controller('SomeController', SomeController);

function SomeController() { }

###Setting vs Getting

[Style Y023]
  • 只能设置一次。

为什么?:一个module只能被创建一次,创建之后才能被检索到。

- 设置module,`angular.module('app', []);`。
- 获取module,`angular.module('app');`。

###命名函数 vs 匿名函数

[Style Y024]
  • 回调函数使用命名函数,不要用匿名函数。

    为什么?:易读,方便调试,减少嵌套回调函数的数量。

/* avoid */
angular
    .module('app')
    .controller('Dashboard', function() { })
    .factory('logger', function() { });
/* recommended */

// dashboard.js
angular
    .module('app')
    .controller('Dashboard', Dashboard);

function Dashboard () { }
// logger.js
angular
    .module('app')
    .factory('logger', logger);

function logger () { }

回到顶部

Controllers

###controllerAs在View中的语法

[Style Y030]
  • 使用controllerAs 语法代替直接用经典的$scope定义的controller的方式。

    为什么?:controller被构建的时候,就会有一个新的实例,controllerAs 的语法比经典的$scope语法更接近JavaScript构造函数。

    为什么?:这促进在View中对绑定到“有修饰”的对象的使用(例如用customer.name 代替name),这将更有语境、更容易阅读,也避免了任何没有“修饰”而产生的引用问题。

    为什么?:有助于避免在有嵌套的controllers的Views中调用 $parent

<!-- avoid -->
<div ng-controller="Customer">
  {{ name }}
</div>
<!-- recommended -->
<div ng-controller="Customer as customer">
  {{ customer.name }}
</div>

###controllerAs在controller中的语法

[Style Y031]
  • 使用 controllerAs 语法代替 经典的$scope语法 语法。

  • 使用controllerAs 时,controller中的$scope被绑定到了this上。

    为什么?controllerAs$scope的语法修饰,你仍然可以绑定到View上并且访问 $scope的方法。

    为什么?:避免在controller中使用 $scope,最好不用它们或是把它们移到一个factory中。factory中可以考虑使用$scope,controller中只在需要时候才使用$scope,例如当使用$emit$broadcast,或者 $on

/* avoid */
function Customer ($scope) {
    $scope.name = {};
    $scope.sendMessage = function() { };
}
/* recommended - but see next section */
function Customer () {
    this.name = {};
    this.sendMessage = function() { };
}

###controllerAs with vm

[Style Y032]
  • 使用controllerAs语法时把this 赋值给一个可捕获的变量,选择一个有代表性的名称,例如vm代表ViewModel。

为什么?this在不同的地方有不同的语义(就是作用域不同),在controller中的一个函数内部使用this时可能会改变它的上下文。用一个变量来捕获this的上下文从而可以避免遇到这样的坑。

/* avoid */
function Customer() {
    this.name = {};
    this.sendMessage = function() { };
}
/* recommended */
function Customer () {
    var vm = this;
    vm.name = {};
    vm.sendMessage = function() { };
}
  • 注:你可以参照下面的做法来避免 jshint的警告。但是构造函数(函数名首字母大写)是不需要这个的.
/* jshint validthis: true */
var vm = this;
  • 注:在controller中用controller as创建了一个watch时,可以用下面的语法监测vm.*的成员。(创建watch时要谨慎,因为它会增加更多的负载)
<input ng-model="vm.title"/>
function SomeController($scope, $log) {
    var vm = this;
    vm.title = 'Some Title';

    $scope.$watch('vm.title', function(current, original) {
        $log.info('vm.title was %s', original);
        $log.info('vm.title is now %s', current);
    });
}

###置顶绑定成员

[Style Y033]
  • 把可绑定的成员放到controller的顶部,按字母排序,并且不要通过controller的代码传播。

为什么?:虽然设置单行匿名函数很容易,但是当这些函数的代码超过一行时,这将极大降低代码的可读性。在可绑定成员下面定义函数(这些函数被提出来),把具体的实现细节放到下面,可绑定成员置顶,这会提高代码的可读性。

/* avoid */
function Sessions() {
    var vm = this;

    vm.gotoSession = function() {
      /* ... */
    };
    vm.refresh = function() {
      /* ... */
    };
    vm.search = function() {
      /* ... */
    };
    vm.sessions = [];
    vm.title = 'Sessions';
/* recommended */
function Sessions() {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = refresh;
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';

    ////////////

    function gotoSession() {
      /* */
    }

    function refresh() {
      /* */
    }

    function search() {
      /* */
    }

Controller Using &quot;Above the Fold&quot;

注:如果一个函数就是一行,那么只要不影响可读性就把它放到顶部。

/* avoid */
function Sessions(data) {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = function() {
        /**
         * lines
         * of
         * code
         * affects
         * readability
         */
    };
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';
/* recommended */
function Sessions(dataservice) {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = dataservice.refresh; // 1 liner is OK
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';

###函数声明隐藏实现细节

[Style Y034]
  • 使用函数声明来隐藏实现细节,置顶绑定成员,当你需要在controller中绑定一个函数时,把它指向一个在文件的后面会出现函数声明。更多详情请看这里

为什么?:易读,易识别哪些成员可以在View中绑定和使用。

为什么?:把函数的实现细节放到后面,你可以更清楚地看到重要的东西。

为什么?:由于函数声明会被置顶,所以没有必要担心在声明它之前就使用函数的问题。

为什么?:你再也不用担心当 a依赖于 b时,把var a放到var b之前会中断你的代码的函数声明问题。

为什么?:函数表达式中顺序是至关重要的。

/**
 * avoid
 * Using function expressions.
 */
function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    var activate = function() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    var getAvengers = function() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }

    vm.getAvengers = getAvengers;

    activate();
}

注意这里重要的代码分散在前面的例子中。
下面的示例中,可以看到重要的代码都放到了顶部。实现的详细细节都在下方,显然这样的代码更易读。

/*
 * recommend
 * Using function declarations
 * and bindable members up top.
 */
function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];
    vm.getAvengers = getAvengers;
    vm.title = 'Avengers';

    activate();

    function activate() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    function getAvengers() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }
}

###把Controller中的逻辑延迟到Service中

[Style Y035]
  • 通过委派到service和factory中来延迟controller中的逻辑。

为什么?:把逻辑放到service中,并通过一个function暴露,就可以被多个controller重用。

为什么?:把逻辑放到service中将会使单元测试的时候更加容易地把它们分离,相反,如果在controller中调用逻辑就显得很二了。

为什么?:保持controller的简洁。

为什么?:从controller中删除依赖关系并且隐藏实现细节。

/* avoid */
function Order($http, $q, config, userInfo) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit () {
        var settings = {};
        // Get the credit service base URL from config
        // Set credit service required headers
        // Prepare URL query string or data object with request data
        // Add user-identifying info so service gets the right credit limit for this user.
        // Use JSONP for this browser if it doesn't support CORS
        return $http.get(settings)
            .then(function(data) {
               // Unpack JSON data in the response object
               // to find maxRemainingAmount
               vm.isCreditOk = vm.total <= maxRemainingAmount
            })
            .catch(function(error) {
               // Interpret error
               // Cope w/ timeout? retry? try alternate service?
               // Re-reject with appropriate error for a user to see
            });
    };
}
/* recommended */
function Order (creditService) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit () {
        return creditService.isOrderTotalOk(vm.total)
          .then(function(isOk) { vm.isCreditOk = isOk; })
          .catch(showServiceError);
    };
}

###保持Controller的专一性

[Style Y037]
  • 一个view定义一个controller,尽量不要在其它view中使用这个controller。把可重用的逻辑放到factory中,保证controller只服务于当前视图。

    为什么?:不同的view用同一个controller是非常不科学的,良好的端对端测试覆盖率对于保证大型应用稳定性是必需的。

###分配Controller

[Style Y038]
  • 当一个controller必须匹配一个view时或者任何一个组件可能被其它controller或是view重用时,连同controller的route一起定义。

    注:如果一个view是通过route外的其它形式加载的,那么就用ng-controller="Avengers as vm"语法。

    为什么?:在route中匹配controller允许不同的路由调用不同的相匹配的controller和view,当在view中通过ng-controller分配controller时,这个view总是和相同的controller相关联。

 /* avoid - when using with a route and dynamic pairing is desired */

 // route-config.js
 angular
     .module('app')
     .config(config);

 function config ($routeProvider) {
     $routeProvider
         .when('/avengers', {
             templateUrl: 'avengers.html'
         });
 }
<!-- avengers.html -->
<div ng-controller="Avengers as vm">
</div>
/* recommended */

// route-config.js
angular
    .module('app')
    .config(config);

function config ($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm'
        });
}
<!-- avengers.html -->
<div>
</div>

返回顶部

Services

###单例

[Style Y040]
  • new实例化service,用this实例化公共方法和变量,由于这和factory是类似的,所以为了保持统一,推荐用facotry来代替。

注意:所有的Angular services都是单例,这意味着每个injector都只有一个实例化的service。

// service
angular
    .module('app')
    .service('logger', logger);

function logger () {
    this.logError = function(msg) {
      /* */
    };
}
// factory
angular
    .module('app')
    .factory('logger', logger);

function logger () {
    return {
        logError: function(msg) {
          /* */
        }
    };
}

返回顶部

Factories

###单一职责

[Style Y051]
  • factory应该是单一职责,这是由其上下文进行封装的。一旦一个factory将要处理超过单一的目的时,就应该创建一个新的factory。

###单例

[Style Y051]

###可访问的成员置顶###

[Style Y052]
  • 使用从显露模块模式派生出来的技术把service(它的接口)中可调用的成员暴露到顶部,

    为什么?:易读,并且让你可以立即识别service中的哪些成员可以被调用,哪些成员必须进行单元测试(或者被别人嘲笑)。

    为什么?:当文件内容很长时,这可以避免需要滚动才能看到暴露了哪些东西。

    为什么?:虽然你可以随意写一个函数,但当函数代码超过一行时就会降低可读性并造成滚动。通过把实现细节放下面、把可调用接口置顶的形式返回service的方式来定义可调用的接口,从而使代码更加易读。

/* avoid */
function dataService () {
    var someValue = '';
    function save () {
      /* */
    };
    function validate () {
      /* */
    };

    return {
        save: save,
        someValue: someValue,
        validate: validate
    };
}
/* recommended */
function dataService () {
    var someValue = '';
    var service = {
        save: save,
        someValue: someValue,
        validate: validate
    };
    return service;

    ////////////

    function save () {
      /* */
    };

    function validate () {
      /* */
    };
}

这种绑定方式复制了宿主对象,原始值不会随着暴露模块模式的使用而更新。

Factories Using &quot;Above the Fold&quot;

###函数声明隐藏实现细节

[Style Y053]
  • 函数声明隐藏实现细节,置顶绑定成员,当你需要在controller中绑定一个函数时,把它指向一个函数声明,这个函数声明在文件的后面会出现。

    为什么?:易读,易识别哪些成员可以在View中绑定和使用。

    为什么?:把函数的实现细节放到后面,你可以更清楚地看到重要的东西。

    为什么?:由于函数声明会被置顶,所以没有必要担心在声明它之前就使用函数的问题。

    为什么?:你再也不用担心当 a依赖于 b时,把var a放到var b之前会中断你的代码的函数声明问题。

    为什么?:函数表达式中顺序是至关重要的。

/**
 * avoid
 * Using function expressions
 */
 function dataservice($http, $location, $q, exception, logger) {
    var isPrimed = false;
    var primePromise;

    var getAvengers = function() {
      // implementation details go here
    };

    var getAvengerCount = function() {
      // implementation details go here
    };

    var getAvengersCast = function() {
      // implementation details go here
    };

    var prime = function() {
      // implementation details go here
    };

    var ready = function(nextPromises) {
      // implementation details go here
    };

    var service = {
        getAvengersCast: getAvengersCast,
        getAvengerCount: getAvengerCount,
        getAvengers: getAvengers,
        ready: ready
    };

    return service;
}
/**
 * recommended
 * Using function declarations
 * and accessible members up top.
 */
function dataservice($http, $location, $q, exception, logger) {
    var isPrimed = false;
    var primePromise;

    var service = {
        getAvengersCast: getAvengersCast,
        getAvengerCount: getAvengerCount,
        getAvengers: getAvengers,
        ready: ready
    };

    return service;

    ////////////

    function getAvengers() {
      // implementation details go here
    }

    function getAvengerCount() {
      // implementation details go here
    }

    function getAvengersCast() {
      // implementation details go here
    }

    function prime() {
      // implementation details go here
    }

    function ready(nextPromises) {
      // implementation details go here
    }
}

返回顶部

Data Services

###独立的数据调用

[Style Y060]
  • 把进行数据操作和数据交互的逻辑放到factory中,数据服务负责XHR请求、本地存储、内存存储和其它任何数据操作。

    为什么?:controller的作用是查看视图和收集视图的信息,它不应该关心如何取得数据,只需要知道哪里需要用到数据。把取数据的逻辑放到数据服务中能够让controller更简单、更专注于对view的控制。

    为什么?:方便测试。

    为什么?:数据服务的实现可能有非常明确的代码来处理数据仓库,这可能包含headers、如何与数据交互或是其它service,例如$http。把逻辑封装到单独的数据服务中,这隐藏了外部调用者(例如controller)对数据的直接操作,这样更加容易执行变更。

/* recommended */

// dataservice factory
angular
    .module('app.core')
    .factory('dataservice', dataservice);

dataservice.$inject = ['$http', 'logger'];

function dataservice($http, logger) {
    return {
        getAvengers: getAvengers
    };

    function getAvengers() {
        return $http.get('/api/maa')
            .then(getAvengersComplete)
            .catch(getAvengersFailed);

        function getAvengersComplete(response) {
            return response.data.results;
        }

        function getAvengersFailed(error) {
            logger.error('XHR Failed for getAvengers.' + error.data);
        }
    }
}
注意:数据服务被调用时(例如controller),隐藏调用的直接行为,如下所示。
/* recommended */

// controller calling the dataservice factory
angular
    .module('app.avengers')
    .controller('Avengers', Avengers);

Avengers.$inject = ['dataservice', 'logger'];

function Avengers(dataservice, logger) {
    var vm = this;
    vm.avengers = [];

    activate();

    function activate() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    function getAvengers() {
        return dataservice.getAvengers()
          .then(function(data) {
              vm.avengers = data;
              return vm.avengers;
          });
    }
}

###数据调用返回一个Promise

[Style Y061]
  • 就像$http一样,调用数据时返回一个promise,在你的调用函数中也返回一个promise。

    为什么?:你可以把promise链接到一起,在数据调用完成并且resolve或是reject这个promise后采取进一步的行为。

/* recommended */

activate();

function activate() {
    /**
     * Step 1
     * Ask the getAvengers function for the
     * avenger data and wait for the promise
     */
    return getAvengers().then(function() {
      /**
       * Step 4
       * Perform an action on resolve of final promise
       */
      logger.info('Activated Avengers View');
    });
}

function getAvengers() {
    /**
     * Step 2
     * Ask the data service for the data and wait
     * for the promise
     */
    return dataservice.getAvengers()
      .then(function(data) {
          /**
           * Step 3
           * set the data and resolve the promise
           */
          vm.avengers = data;
          return vm.avengers;
      });
}

返回顶部

Directives

###一个directive一个文件

[Style Y070]
  • 一个文件中只创建一个directive,并依照directive来命名文件。

    为什么?:虽然把所有directive放到一个文件中很简单,但是当一些directive是跨应用的,一些是跨模块的,一些仅仅在一个模块中使用时,想把它们独立出来就非常困难了。

    为什么?:一个文件一个directive也更加容易维护。

    注: "最佳实践:Angular文档中有提过,directive应该自动回收,当directive被移除后,你可以使用element.on('$destroy', ...)或者scope.$on('$destroy', ...)来执行一个clearn-up函数。"

/* avoid */
/* directives.js */

angular
    .module('app.widgets')

    /* order directive仅仅会被order module用到 */
    .directive('orderCalendarRange', orderCalendarRange)

    /* sales directive可以在sales app的任意地方使用 */
    .directive('salesCustomerInfo', salesCustomerInfo)

    /* spinner directive可以在任意apps中使用 */
    .directive('sharedSpinner', sharedSpinner);

function orderCalendarRange() {
    /* implementation details */
}

function salesCustomerInfo() {
    /* implementation details */
}

function sharedSpinner() {
    /* implementation details */
}
/* recommended */
/* calendarRange.directive.js */

/**
 * @desc order directive that is specific to the order module at a company named Acme
 * @example <div acme-order-calendar-range></div>
 */
angular
    .module('sales.order')
    .directive('acmeOrderCalendarRange', orderCalendarRange);

function orderCalendarRange() {
    /* implementation details */
}
/* recommended */
/* customerInfo.directive.js */

/**
 * @desc sales directive that can be used anywhere across the sales app at a company named Acme
 * @example <div acme-sales-customer-info></div>
 */
angular
    .module('sales.widgets')
    .directive('acmeSalesCustomerInfo', salesCustomerInfo);

function salesCustomerInfo() {
    /* implementation details */
}
/* recommended */
/* spinner.directive.js */

/**
 * @desc spinner directive that can be used anywhere across apps at a company named Acme
 * @example <div acme-shared-spinner></div>
 */
angular
    .module('shared.widgets')
    .directive('acmeSharedSpinner', sharedSpinner);

function sharedSpinner() {
    /* implementation details */
}
注:由于directive使用条件比较广,所以命名就存在很多的选项。选择一个让directive和它的文件名都清楚分明的名字。下面有一些例子,不过更多的建议去看[命名](#命名)章节。

###在directive中操作DOM

[Style Y072]
  • 当需要直接操作DOM的时候,使用directive。如果有替代方法可以使用,例如:使用CSS来设置样式、animation services、Angular模板、ngShow或者ngHide,那么就直接用这些即可。例如,如果一个directive只是想控制显示和隐藏,用ngHide/ngShow即可。

    为什么?:DOM操作的测试和调试是很困难的,通常会有更好的方法(CSS、animations、templates)。

###提供一个唯一的Directive前缀

[Style Y073]
  • 提供一个短小、唯一、具有描述性的directive前缀,例如acmeSalesCustomerInfo在HTML中声明为acme-sales-customer-info

    为什么?:方便快速识别directive的内容和起源,例如acme-可能预示着这个directive是服务于Acme company。

    注:避免使用ng-为前缀,研究一下其它广泛使用的directive避免命名冲突,例如Ionic Frameworkion-

###限制元素和属性

[Style Y074]
  • 当创建一个directive需要作为一个独立元素时,restrict值设置为E(自定义元素),也可以设置可选值A(自定义属性)。一般来说,如果它就是为了独立存在,用E是合适的做法。一般原则是允许EA,但是当它是独立的时候这更倾向于作为一个元素来实施,当它是为了增强已存在的DOM元素时则更倾向于作为一个属性来实施。

    为什么?:这很有意义!

    为什么?:虽然我们允许directive被当作一个class来使用,但如果这个directive的行为确实像一个元素的话,那么把directive当作元素或者属性是更有意义的。

    注:Angular 1.3 +默认使用EA。

<!-- avoid -->
<div class="my-calendar-range"></div>
/* avoid */
angular
    .module('app.widgets')
    .directive('myCalendarRange', myCalendarRange);

function myCalendarRange () {
    var directive = {
        link: link,
        templateUrl: '/template/is/located/here.html',
        restrict: 'C'
    };
    return directive;

    function link(scope, element, attrs) {
      /* */
    }
}
<!-- recommended -->
<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>
/* recommended */
angular
    .module('app.widgets')
    .directive('myCalendarRange', myCalendarRange);

function myCalendarRange () {
    var directive = {
        link: link,
        templateUrl: '/template/is/located/here.html',
        restrict: 'EA'
    };
    return directive;

    function link(scope, element, attrs) {
      /* */
    }
}

###Directives和ControllerAs

[Style Y075]
  • directive使用controller as语法,和view使用controller as保持一致。

    为什么?:因为不难且有必要这样做。

    注意:下面的directive演示了一些你可以在link和directive控制器中使用scope的方法,用controllerAs。这里把template放在行内是为了在一个地方写出这些代码。

    注意:关于依赖注入的内容,请看手动依赖注入

    注意:directive的控制器是在directive外部的,这种风格避免了由于注入造成的return之后的代码无法访问的情况。

<div my-example max="77"></div>
angular
    .module('app')
    .directive('myExample', myExample);

function myExample() {
    var directive = {
        restrict: 'EA',
        templateUrl: 'app/feature/example.directive.html',
        scope: {
            max: '='
        },
        link: linkFunc,
        controller : ExampleController,
        controllerAs: 'vm',
        bindToController: true // because the scope is isolated
    };

    return directive;

    function linkFunc(scope, el, attr, ctrl) {
        console.log('LINK: scope.min = %s *** should be undefined', scope.min);
        console.log('LINK: scope.max = %s *** should be undefined', scope.max);
        console.log('LINK: scope.vm.min = %s', scope.vm.min);
        console.log('LINK: scope.vm.max = %s', scope.vm.max);
    }
}

ExampleController.$inject = ['$scope'];

function ExampleController($scope) {
    // Injecting $scope just for comparison
    var vm = this;

    vm.min = 3;

    console.log('CTRL: $scope.vm.min = %s', $scope.vm.min);
    console.log('CTRL: $scope.vm.max = %s', $scope.vm.max);
    console.log('CTRL: vm.min = %s', vm.min);
    console.log('CTRL: vm.max = %s', vm.max);
}
<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="{{vm.max}}"/></div>
<div>min={{vm.min}}<input ng-model="{{vm.min}}"/></div>
注意:当你把controller注入到link的函数或可访问的directive的attributes时,你可以把它命名为控制器的属性。
// Alternative to above example
function linkFunc(scope, el, attr, vm) { // 和上面例子的区别在于把vm直接传递进来
  console.log('LINK: scope.min = %s *** should be undefined', scope.min);
  console.log('LINK: scope.max = %s *** should be undefined', scope.max);
  console.log('LINK: vm.min = %s', vm.min);
  console.log('LINK: vm.max = %s', vm.max);
}
[Style Y076]
  • 当directive中使用了controller as语法时,如果你想把父级作用域绑定到directive的controller作用域时,使用bindToController = true

    为什么?:这使得把外部作用域绑定到directive controller中变得更加简单。

    注意:Angular 1.3.0才介绍了bindToController

<div my-example max="77"></div>
angular
    .module('app')
    .directive('myExample', myExample);

function myExample() {
    var directive = {
        restrict: 'EA',
        templateUrl: 'app/feature/example.directive.html',
        scope: {
            max: '='
        },
        controller: ExampleController,
        controllerAs: 'vm',
        bindToController: true
      };

    return directive;
}

function ExampleController() {
    var vm = this;
    vm.min = 3;
    console.log('CTRL: vm.min = %s', vm.min);
    console.log('CTRL: vm.max = %s', vm.max);
}
<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>

返回顶部

解决Controller的Promises

###Controller Activation Promises

[Style Y080]
  • activate函数中解决controller的启动逻辑。

    为什么?:把启动逻辑放在一个controller中固定的位置可以方便定位、有利于保持测试的一致性,并能够避免controller中到处都是激活逻辑。

    为什么?activate这个controller使得重用刷新视图的逻辑变得很方便,把所有的逻辑都放到了一起,可以让用户更快地看到视图,可以很轻松地对ng-viewui-view使用动画,用户体验更好。

    注意:如果你需要在开始使用controller之前有条件地取消路由,那么就用route resolve来代替。

/* avoid */
function Avengers(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    dataservice.getAvengers().then(function(data) {
        vm.avengers = data;
        return vm.avengers;
    });
}
/* recommended */
function Avengers(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    activate();

    ////////////

    function activate() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }
}

###Route Resolve Promises

[Style Y081]
  • 当一个controller在激活之前,需要依赖一个promise的完成时,那么就在controller的逻辑执行之前在$routeProvider中解决这些依赖。如果你需要在controller被激活之前有条件地取消一个路由,那么就用route resolver。

  • 当你决定在过渡到视图之前取消路由时,使用route resolve。

    为什么?:controller在加载前可能需要一些数据,这些数据可能是从一个通过自定义factory或是$http的promise而来的。route resolve允许promise在controller的逻辑执行之前解决,因此它可能对从promise中来的数据做一些处理。

    为什么?:这段代码将在路由后的controller的激活函数中执行,视图立即加载,promise resolve的时候将会开始进行数据绑定,可以(通过ng-viewui-view)在视图的过渡之间加个loading状态的动画。

    注意:这段代码将在路由之前通过一个promise来执行,拒绝了承诺就会取消路由,接受了就会等待路由跳转到新视图。如果你想更快地进入视图,并且无需验证是否可以进入视图,你可以考虑用控制器 activate 技术

/* avoid */
angular
    .module('app')
    .controller('Avengers', Avengers);

function Avengers (movieService) {
    var vm = this;
    // unresolved
    vm.movies;
    // resolved asynchronously
    movieService.getMovies().then(function(response) {
        vm.movies = response.movies;
    });
}
/* better */

// route-config.js
angular
    .module('app')
    .config(config);

function config ($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: function(movieService) {
                    return movieService.getMovies();
                }
            }
        });
}

// avengers.js
angular
    .module('app')
    .controller('Avengers', Avengers);

Avengers.$inject = ['moviesPrepService'];
function Avengers (moviesPrepService) {
    var vm = this;
    vm.movies = moviesPrepService.movies;
}
注意:下面这个例子展示了命名函数的路由解决,这种方式对于调试和处理依赖注入更加方便。
/* even better */

// route-config.js
angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: moviesPrepService
            }
        });
}

function moviesPrepService(movieService) {
    return movieService.getMovies();
}

// avengers.js
angular
    .module('app')
    .controller('Avengers', Avengers);

Avengers.$inject = ['moviesPrepService'];
function Avengers(moviesPrepService) {
      var vm = this;
      vm.movies = moviesPrepService.movies;
}

注意:示例代码中的movieService不符合安全压缩的做法,可以到手动依赖注入压缩和注释部分学习如何安全压缩。

返回顶部

手动依赖注入

压缩的不安全性

[Style Y090]
  • 声明依赖时避免使用缩写语法。

    为什么?:组件的参数(例如controller、factory等等)将会被转换成各种乱七八糟错误的变量。例如,commondataservice可能会变成a或者b,但是这些转换后的变量在Angular中是找不到的。

/* avoid - not minification-safe*/
angular
    .module('app')
    .controller('Dashboard', Dashboard);

function Dashboard(common, dataservice) {
}

这一段代码在压缩时会产生错误的变量,因此在运行时就会报错。

/* avoid - not minification-safe*/
angular.module('app').controller('Dashboard', d);function d(a, b) { }

###手动添加依赖

[Style Y091]
  • $inject手动添加Angular组件所需的依赖。

    为什么?:这种技术反映了使用ng-annotate的技术,这就是我推荐的对依赖关系进行自动化创建安全压缩的方式,如果ng-annotate检测到已经有了注入,那么它就不会再次重复执行。

    为什么?:可以避免依赖变成其它Angular找不到的变量,例如,commondataservice可能会变成a或者b

    为什么?:避免创建内嵌的依赖,因为一个数组太长不利于阅读,此外,内嵌的方式也会让人感到困惑,比如数组是一系列的字符串,但是最后一个却是组件的function。

/* avoid */
angular
    .module('app')
    .controller('Dashboard',
        ['$location', '$routeParams', 'common', 'dataservice',
            function Dashboard($location, $routeParams, common, dataservice) {}
        ]);
/* avoid */
angular
    .module('app')
    .controller('Dashboard',
      ['$location', '$routeParams', 'common', 'dataservice', Dashboard]);

function Dashboard($location, $routeParams, common, dataservice) {
}
/* recommended */
angular
    .module('app')
    .controller('Dashboard', Dashboard);

Dashboard.$inject = ['$location', '$routeParams', 'common', 'dataservice'];

function Dashboard($location, $routeParams, common, dataservice) {
}
注意:当你的函数处于return语句后面,那么`$inject`是无法访问的(这会在directive中发生),你可以通过把Controller移到directive外面来解决这个问题。
/* avoid */
// inside a directive definition
function outer() {
    var ddo = {
        controller: DashboardPanelController,
        controllerAs: 'vm'
    };
    return ddo;

    DashboardPanelController.$inject = ['logger']; // Unreachable
    function DashboardPanelController(logger) {
    }
}
/* recommended */
// outside a directive definition
function outer() {
    var ddo = {
        controller: DashboardPanelController,
        controllerAs: 'vm'
    };
    return ddo;
}

DashboardPanelController.$inject = ['logger'];
function DashboardPanelController(logger) {
}

###手动确定路由解析器依赖

[Style Y092]
  • $inject手动给Angular组件添加路由解析器依赖。

    为什么?:这种技术打破了路由解析的匿名函数的形式,易读。

    为什么?$inject语句可以让任何依赖都可以安全压缩。

/* recommended */
function config ($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'AvengersController',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: moviesPrepService
            }
        });
}

moviesPrepService.$inject =  ['movieService'];
function moviesPrepService(movieService) {
    return movieService.getMovies();
}

返回顶部

压缩和注释

###ng-annotate

[Style Y100]
  • GulpGrunt中使用ng-annotate,用/** @ngInject */对需要自动依赖注入的function进行注释。

    为什么?:可以避免代码中的依赖使用到任何不安全的写法。

    为什么?:不推荐用ng-min

    我更喜欢Gulp,因为我觉得它是易写易读易调试的。

    下面的代码没有注入依赖,显然压缩是不安全的。

    angular
        .module('app')
        .controller('Avengers', Avengers);
    
    /* @ngInject */
    function Avengers (storageService, avengerService) {
        var vm = this;
        vm.heroSearch = '';
        vm.storeHero = storeHero;
    
        function storeHero(){
          var hero = avengerService.find(vm.heroSearch);
          storageService.save(hero.name, hero);
        }
    }
    

    当上面的代码通过ng-annotate运行时,就会产生如下的带有$inject注释的输出结果,这样的话压缩就会安全了。

    angular
        .module('app')
        .controller('Avengers', Avengers);
    
    /* @ngInject */
    function Avengers (storageService, avengerService) {
        var vm = this;
        vm.heroSearch = '';
        vm.storeHero = storeHero;
    
        function storeHero(){
          var hero = avengerService.find(vm.heroSearch);
          storageService.save(hero.name, hero);
        }
    }
    
    Avengers.$inject = ['storageService', 'avengerService'];
    

    注意:如果ng-annotate检测到已经有注入了(例如发现了@ngInject),就不会重复生成$inject代码了。

    注意:路由的函数前面也可以用/* @ngInject */

    // Using @ngInject annotations
    function config($routeProvider) {
        $routeProvider
            .when('/avengers', {
                templateUrl: 'avengers.html',
                controller: 'Avengers',
                controllerAs: 'vm',
                resolve: { /* @ngInject */
                    moviesPrepService: function(movieService) {
                        return movieService.getMovies();
                    }
                }
            });
    }
    

    注意:从Angular 1.3开始,你就可以用ngApp指令的 ngStrictDi参数来检测任何可能失去依赖的地方,当以“strict-di”模式创建injector时,会导致应用程序无法调用不使用显示函数注释的函数(这也许无法安全压缩)。记录在控制台的调试信息可以帮助追踪出问题的代码。我只在需要调试的时候才会用到ng-strict-di
    <body ng-app="APP" ng-strict-di>

###使用Gulp或Grunt结合ng-annotate

[Style Y101]
  • 在自动化任务中使用gulp-ng-annotategrunt-ng-annotate,把/* @ngInject */注入到任何有依赖关系函数的前面。

    为什么?:ng-annotate会捕获大部分的依赖关系,但是有时候需要借助于/* @ngInject */语法提示。

    下面的代码是gulp任务使用ngAnnotate的例子。

    gulp.task('js', ['jshint'], function() {
        var source = pkg.paths.js;
    
        return gulp.src(source)
            .pipe(sourcemaps.init())
            .pipe(concat('all.min.js', {newLine: ';'}))
            // Annotate before uglify so the code get's min'd properly.
            .pipe(ngAnnotate({
                // true helps add where @ngInject is not used. It infers.
                // Doesn't work with resolve, so we must be explicit there
                add: true
            }))
            .pipe(bytediff.start())
            .pipe(uglify({mangle: true}))
            .pipe(bytediff.stop())
            .pipe(sourcemaps.write('./'))
            .pipe(gulp.dest(pkg.paths.dev));
    });
    
    

**[返回顶部](#目录)**

## 异常处理

###修饰符
###### [Style [Y110](#style-y110)]

- 使用一个[decorator](https://docs.angularjs.org/api/auto/service/$provide#decorator),在配置的时候用[`$provide`](https://docs.angularjs.org/api/auto/service/$provide)服务,当发生异常时,在[`$exceptionHandler`](https://docs.angularjs.org/api/ng/service/$exceptionHandler)服务中执行自定义的处理方法。

  *为什么?*:在开发时和运行时提供了一种统一的方式来处理未被捕获的Angular异常。

  注:另一个选项是用来覆盖service的,这个可以代替decorator,这是一个非常nice的选项,但是如果你想保持默认行为,那么推荐你扩展一个decorator。

  ```javascript
  /* recommended */
  angular
      .module('blocks.exception')
      .config(exceptionConfig);

  exceptionConfig.$inject = ['$provide'];

  function exceptionConfig($provide) {
      $provide.decorator('$exceptionHandler', extendExceptionHandler);
  }

  extendExceptionHandler.$inject = ['$delegate', 'toastr'];

  function extendExceptionHandler($delegate, toastr) {
      return function(exception, cause) {
          $delegate(exception, cause);
          var errorData = {
            exception: exception,
            cause: cause
          };
          /**
           * Could add the error to a service's collection,
           * add errors to $rootScope, log errors to remote web server,
           * or log locally. Or throw hard. It is entirely up to you.
           * throw exception;
           */
          toastr.error(exception.msg, errorData);
      };
  }

###异常捕获器

[Style Y111]
  • 创建一个暴露了一个接口的factory来捕获异常并以合适方式处理异常。

    为什么?:提供了一个统一的方法来捕获代码中抛出的异常。

    注:异常捕获器对特殊异常的捕获和反应是非常友好的,例如,使用XHR从远程服务获取数据时,你想要捕获所有异常并做出不同的反应。

    /* recommended */
    angular
        .module('blocks.exception')
        .factory('exception', exception);
    
    exception.$inject = ['logger'];
    
    function exception(logger) {
        var service = {
            catcher: catcher
        };
        return service;
    
        function catcher(message) {
            return function(reason) {
                logger.error(message, reason);
            };
        }
    }
    

###路由错误

[Style Y112]
  • $routeChangeError来处理并打印出所有的路由错误信息。

    为什么?:提供一个统一的方式来处理所有的路由错误。

    为什么?:当一个路由发生错误的时候,可以给展示一个提示信息,提高用户体验。

    /* recommended */
    var handlingRouteChangeError = false;
    
    function handleRoutingErrors() {
        /**
         * Route cancellation:
         * On routing error, go to the dashboard.
         * Provide an exit clause if it tries to do it twice.
         */
        $rootScope.$on('$routeChangeError',
            function(event, current, previous, rejection) {
                if (handlingRouteChangeError) { return; }
                handlingRouteChangeError = true;
                var destination = (current && (current.title ||
                    current.name || current.loadedTemplateUrl)) ||
                    'unknown target';
                var msg = 'Error routing to ' + destination + '. ' +
                    (rejection.msg || '');
    
                /**
                 * Optionally log using a custom service or $log.
                 * (Don't forget to inject custom service)
                 */
                logger.warning(msg, [current]);
    
                /**
                 * On routing error, go to another route/state.
                 */
                $location.path('/');
    
            }
        );
    }
    

返回顶部

命名

###命名原则

[Style Y120]
  • 遵循以描述组件功能,然后是类型(可选)的方式来给所有的组件提供统一的命名,我推荐的做法是feature.type.js。大多数文件都有2个名字。

    • 文件名 (avengers.controller.js)
    • 带有Angular的注册组件名 (AvengersController)

    为什么?:命名约定有助于为一目了然地找到内容提供一个统一的方式,在项目中和团队中保持统一性是非常重要的,保持统一性对于跨公司来说提供了巨大的效率。

    为什么?:命名约定应该只为代码的检索和沟通提供方便。

###功能文件命名

[Style Y121]
  • 遵循以“描述组件功能.类型(可选)”的方式来给所有的组件提供统一的命名,我推荐的做法是feature.type.js

    为什么?:为快速识别组件提供了统一的方式。

    为什么?:为任何自动化的任务提供模式匹配。

    /**
     * common options
     */
    
    // Controllers
    avengers.js
    avengers.controller.js
    avengersController.js
    
    // Services/Factories
    logger.js
    logger.service.js
    loggerService.js
    
    /**
     * recommended
     */
    
    // controllers
    avengers.controller.js
    avengers.controller.spec.js
    
    // services/factories
    logger.service.js
    logger.service.spec.js
    
    // constants
    constants.js
    
    // module definition
    avengers.module.js
    
    // routes
    avengers.routes.js
    avengers.routes.spec.js
    
    // configuration
    avengers.config.js
    
    // directives
    avenger-profile.directive.js
    avenger-profile.directive.spec.js
    

注意:另外一种常见的约定就是不要用controller这个词来给controller文件命名,例如不要用avengers.controller.js,而是用avengers.js。所有其它的约定都坚持使用类型作为后缀,但是controller是组件中最为常用的类型,因此这种做法的好处貌似仅仅是节省了打字,但是仍然很容易识别。我建议你为你的团队选择一种约定,并且要保持统一性。我喜欢的命名方式是avengers.controller.js

```javascript
/**
 * recommended
 */
// Controllers
avengers.js
avengers.spec.js
```

###测试文件命名

[Style Y122]
  • 和组件命名差不多,带上一个spec后缀。

    为什么?:为快速识别组件提供统一的方式。

    为什么?:为karma或是其它测试运行器提供模式匹配。

    /**
     * recommended
     */
    avengers.controller.spec.js
    logger.service.spec.js
    avengers.routes.spec.js
    avenger-profile.directive.spec.js
    

###Controller命名

[Style Y123]
  • 为所有controller提供统一的名称,先特征后名字,鉴于controller是构造函数,所以要采用UpperCamelCase(每个单词首字母大写)的方式。

    为什么?:为快速识别和引用controller提供统一的方式。

    为什么?:UpperCamelCase是常规的识别一个可以用构造函数来实例化的对象的方式。

    /**
     * recommended
     */
    
    // avengers.controller.js
    angular
        .module
        .controller('HeroAvengersController', HeroAvengersController);
    
    function HeroAvengers(){ }
    

###Controller命名后缀

[Style Y124]
  • 使用Controller

    为什么?Controller使用更广泛、更明确、更具有描述性。

    /**
     * recommended
     */
    
    // avengers.controller.js
    angular
        .module
        .controller('AvengersController', AvengersController);
    
    function AvengersController(){ }
    

###Factory命名

[Style Y125]
  • 一样要统一,对service和factory使用camel-casing(驼峰式,第一个单词首字母小写,后面单词首字母大写)方式。避免使用$前缀。

    为什么?:可以快速识别和引用factory。

    为什么?:避免与内部使用$前缀的服务发生冲突。

    /**
     * recommended
     */
    
    // logger.service.js
    angular
        .module
        .factory('logger', logger);
    
    function logger(){ }
    

###Directive组件命名

[Style Y126]
  • 使用camel-case方式,用一个短的前缀来描述directive在哪个区域使用(一些例子中是使用公司前缀或是项目前缀)。

    为什么?:可以快速识别和引用controller。

    /**
     * recommended
     */
    
    // avenger-profile.directive.js
    angular
        .module
        .directive('xxAvengerProfile', xxAvengerProfile);
    
    // usage is <xx-avenger-profile> </xx-avenger-profile>
    
    function xxAvengerProfile(){ }
    

###模块

[Style Y127]
  • 当有很多的模块时,主模块文件命名成app.module.js,其它依赖模块以它们代表的内容来命名。例如,一个管理员模块命名成admin.module.js,它们各自的注册模块名字就是appadmin

    为什么?:给多模块的应用提供统一的方式,这也是为了扩展大型应用。

    为什么?:对使用任务来自动化加载所有模块的定义(先)和其它所有的angular文件(后)提供了一种简单的方式。

###配置

[Style Y128]
  • 把一个模块的配置独立到它自己的文件中,以这个模块为基础命名。app模块的配置文件命名成app.config.js(或是config.js),admin.module.js的配置文件命名成admin.config.js

    为什么?:把配置从模块定义、组件和活跃代码中分离出来。

    为什么?:为设置模块的配置提供了一个可识别的地方。

###路由

[Style Y129]
  • 把路由的配置独立到单独的文件。主模块的路由可能是app.route.jsadmin模块的路由可能是admin.route.js。即使是在很小的应用中,我也喜欢把路由的配置从其余的配置中分离出来。

返回顶部

应用程序结构的LIFT准则

###LIFT

[Style Y140]
  • 构建一个可以快速定位(Locate)代码、一目了然地识别(Identify)代码、拥有一个平直(Flattest)的结构、尽量(Try)坚持DRY(Don’t Repeat Yourself)的应用程序,其结构应该遵循这4项基本准则。

    为什么是LIFT?: 提供一个有良好扩展的结构,并且是模块化的,更快的找到代码能够帮助开发者提高效率。另一种检查你的app结构的方法就是问你自己:你能多块地打开涉及到一个功能的所有相关文件并开始工


  • 文档主要目的是为设计接口时提供建议,使大家不必重复造 HTTP 协议已经完成的轮子
  • 只是建议,不是必须遵从的要求
  • 大家有什么问题想法或者建议欢迎 创建 Issue 或者 提交 Pull Request

目录

HTTP 协议

HTTP/1.1

2014 年 6 月的时候 IETF 已经正式的废弃了 RFC 2616 ,将它拆分为六个单独的协议说明,并重点对原来语义模糊的部分进行了解释:

相关资料:

HTTP/2

HTTP 协议的 2.0 版本还没有正式发布,但目前已经基本稳定下来了。

2.0 版本的设计目标是尽量在使用层面上保持与 1.1 版本的兼容,所以,虽然数据交换的格式发生了变化,但语义基本全部被保留下来了

因此,作为使用者而言,我们并不需要为了支持 2.0 而大幅修改代码。

URL

HOST 地址:

https://api.example.com

所有 URI 都需要遵循 RFC 3986 的要求。

强烈建议 API 部署 SSL 证书,这样接口传递的数据的安全性才能都得一定的保障。

空字段

接口遵循“输入宽容,输出严格”原则,输出的数据结构中空字段的值一律为 null

国际化

语言标签

RFC 5646 (BCP 47) 规定的语言标签的格式如下:

language-script-region-variant-extension-privateuse
  1. language:这部分使用的是 ISO 639-1, ISO 639-2, ISO 639-3, ISO 639-5 中定义的语言代码,必填
    • 这个部分由 primary-extlang 两个部分构成
    • primary 部分使用 ISO 639-1, ISO 639-2, ISO 639-3, ISO 639-5 中定义的语言代码,优先使用 ISO 639-1 中定义的条目,比如汉语 zh
    • extlang 部分是在某些历史性的兼容性的原因,在需要非常细致地区别 primary 语言的时候使用,使用 ISO 639-3 中定义的三个字母的代码,比如普通话 cmn
    • 虽然 language 可以只写 extlang 省略 primary 部分,但出于兼容性的考虑,还是建议加上 primary 部分
  2. script: 这部分使用的是 ISO 15924 (Wikipedia) 中定义的语言代码,比如简体汉字是 zh-Hans ,繁体汉字是 zh-Hant
  3. region: 这部分使用的是 ISO 3166-1 (Wikipedia) 中定义的地理区域代码,比如 zh-Hans-CN 就是中国大陆使用的简体中文。
  4. variant: 用来表示 extlang 的定义里没有包含的方言,具体的使用方法可以参考 RFC 5646
  5. extension: 用来为自己的应用做一些语言上的额外的扩展,具体的使用方法可以参考 RFC 5646
  6. privateuse: 用来表示私有协议中约定的一些语言上的区别,具体的使用方法可以参考 RFC 5646

其中只有 language 部分是必须的,其他部分都是可选的;不过为了便于编写程序,建议设计接口时约定语言标签的结构,比如统一使用 language-script-region 的形式( zh-Hans-CN, zh-Hant-HK 等等)。

语言标签是大小写不敏感的,但按照惯例,建议 script 部分首字母大写, region 部分全部大写,其余部分全部小写。

有一点需要注意,任何合法的标签都必须经过 IANA 的认证,已通过认证的标签可以在这个网页查到。此外,网上还有一个非官方的标签搜索引擎

相关资料:

时区

客户端请求服务器时,如果对时间有特殊要求(如某段时间每天的统计信息),则可以参考 IETF 相关草案 增加请求头 Timezone

Timezone: 2007-06-12T23:48:22+0800
// OR
Timezone: 1977-07-30T12:00:11+0200;;Europe/Athens

时区的名称可以参考 tz datebase(Wikipedia) 。

如果客户端请求时没有指定相应的时区,则服务端默认使用 UTC 时间返回相应数据。

PS 考虑到存在夏时制这种东西,所以不推荐客户端在请求时使用 Offset 。

时间格式

时间格式遵循 ISO 8601(Wikipedia) 建议的格式:

  • 日期 2014-07-09
  • 时间 14:31:22+0800
  • 具体时间 2007-11-06T16:34:41Z
  • 持续时间 P1Y3M5DT6H7M30S (表示在一年三个月五天六小时七分三十秒内)
  • 时间区间 2007-03-01T13:00:00Z/2008-05-11T15:30:00Z2007-03-01T13:00:00Z/P1Y2M10DT2H30MP1Y2M10DT2H30M/2008-05-11T15:30:00Z
  • 重复时间 R3/2004-05-06T13:00:00+08/P0Y6M5DT3H0M0S (表示从2004年5月6日北京时间下午1点起,在半年零5天3小时内,重复3次)

相关资料:

货币名称

货币名称可以参考 ISO 4217(Wikipedia) 中的约定,标准为货币名称规定了三个字母的货币代码,其中的前两个字母是 ISO 3166-1(Wikipedia) 中定义的双字母国家代码,第三个字母通常是货币的首字母。在货币上使用这些代码消除了货币名称(比如 dollar )或符号(比如 $ )的歧义。

相关资料:

  • 《RESTful Web Services Cookbook 中文版》 3.9 节《如何在表述中使用可移植的数据格式》

请求方法

  • 如果请求头中存在 X-HTTP-Method-Override 或参数中存在 _method(拥有更高权重),且值为 GET, POST, PUT, DELETE, PATCH, OPTION, HEAD 之一,则视作相应的请求方式进行处理
  • GET, DELETE, HEAD 方法,参数风格为标准的 GET 风格的参数,如 url?a=1&b=2
  • POST, PUT, PATCH, OPTION 方法
    • 默认情况下请求实体会被视作标准 json 字符串进行处理,当然,依旧推荐设置头信息的 Content-Typeapplication/json
    • 在一些特殊接口中(会在文档中说明),可能允许 Content-Typeapplication/x-www-form-urlencoded 或者 multipart/form-data ,此时请求实体会被视作标准 POST 风格的参数进行处理

关于方法语义的说明:

  • OPTIONS 用于获取资源支持的所有 HTTP 方法
  • HEAD 用于只获取请求某个资源返回的头信息
  • GET 用于从服务器获取某个资源的信息
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被请求的资源详细信息
  • POST 用于创建新资源
    • 创建完成后返回状态码 201 Created
    • 完成请求后需要返回被创建的资源详细信息
  • PUT 用于完整的替换资源或者创建指定身份的资源,比如创建 id 为 123 的某个资源
    • 如果是创建了资源,则返回 201 Created
    • 如果是替换了资源,则返回 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • PATCH 用于局部更新资源
    • 完成请求后返回状态码 200 OK
    • 完成请求后需要返回被修改的资源详细信息
  • DELETE 用于删除某个资源
    • 完成请求后返回状态码 204 No Content

相关资料:

状态码

请求成功

  • 200 OK : 请求执行成功并返回相应数据,如 GET 成功
  • 201 Created : 对象创建成功并返回相应资源数据,如 POST 成功;创建完成后响应头中应该携带头标 Location ,指向新建资源的地址
  • 202 Accepted : 接受请求,但无法立即完成创建行为,比如其中涉及到一个需要花费若干小时才能完成的任务。返回的实体中应该包含当前状态的信息,以及指向处理状态监视器或状态预测的指针,以便客户端能够获取最新状态。
  • 204 No Content : 请求执行成功,不返回相应资源数据,如 PATCHDELETE 成功

重定向

重定向的新地址都需要在响应头 Location 中返回

  • 301 Moved Permanently : 被请求的资源已永久移动到新位置
  • 302 Found : 请求的资源现在临时从不同的 URI 响应请求
  • 303 See Other : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该使用 GET 方法进行请求
  • 307 Temporary Redirect : 对应当前请求的响应可以在另一个 URI 上被找到,客户端应该保持原有的请求方法进行请求

条件请求

  • 304 Not Modified : 资源自从上次请求后没有再次发生变化,主要使用场景在于实现数据缓存
  • 409 Conflict : 请求操作和资源的当前状态存在冲突。主要使用场景在于实现并发控制
  • 412 Precondition Failed : 服务器在验证在请求的头字段中给出先决条件时,没能满足其中的一个或多个。主要使用场景在于实现并发控制

客户端错误

  • 400 Bad Request : 请求体包含语法错误
  • 401 Unauthorized : 需要验证用户身份,如果服务器就算是身份验证后也不允许客户访问资源,应该响应 403 Forbidden
  • 403 Forbidden : 服务器拒绝执行
  • 404 Not Found : 找不到目标资源
  • 405 Method Not Allowed : 不允许执行目标方法,响应中应该带有 Allow 头,内容为对该资源有效的 HTTP 方法
  • 406 Not Acceptable : 服务器不支持客户端请求的内容格式,但响应里会包含服务端能够给出的格式的数据,并在 Content-Type 中声明格式名称
  • 410 Gone : 被请求的资源已被删除,只有在确定了这种情况是永久性的时候才可以使用,否则建议使用 404 Not Found
  • 413 Payload Too Large : POST 或者 PUT 请求的消息实体过大
  • 415 Unsupported Media Type : 服务器不支持请求中提交的数据的格式
  • 422 Unprocessable Entity : 请求格式正确,但是由于含有语义错误,无法响应
  • 428 Precondition Required : 要求先决条件,如果想要请求能成功必须满足一些预设的条件

服务端错误

  • 500 Internal Server Error : 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。
  • 501 Not Implemented : 服务器不支持当前请求所需要的某个功能。
  • 502 Bad Gateway : 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
  • 503 Service Unavailable : 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。如果能够预计延迟时间,那么响应中可以包含一个 Retry-After 头用以标明这个延迟时间(内容可以为数字,单位为秒;或者是一个 HTTP 协议指定的时间格式)。如果没有给出这个 Retry-After 信息,那么客户端应当以处理 500 响应的方式处理它。

501405 的区别是:405 是表示服务端不允许客户端这么做,501 是表示客户端或许可以这么做,但服务端还没有实现这个功能

相关资料:

错误处理

在调用接口的过程中,可能出现下列几种错误情况:

  • 服务器维护中,503 状态码

    HTTP/1.1 503 Service Unavailable
    Retry-After: 3600
    Content-Length: 41
    
    {"message": "Service In the maintenance"}
    
  • 发送了无法转化的请求体,400 状态码

    HTTP/1.1 400 Bad Request
    Content-Length: 35
    
    {"message": "Problems parsing JSON"}
    
  • 服务到期(比如付费的增值服务等), 403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 29
    
    {"message": "Service expired"}
    
  • 因为某些原因不允许访问(比如被 ban ),403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 29
    
    {"message": "Account blocked"}
    
  • 权限不够,403 状态码

    HTTP/1.1 403 Forbidden
    Content-Length: 31
    
    {"message": "Permission denied"}
    
  • 需要修改的资源不存在, 404 状态码

    HTTP/1.1 404 Not Found
    Content-Length: 32
    
    {"message": "Resource not found"}
    
  • 缺少了必要的头信息,428 状态码

    HTTP/1.1 428 Precondition Required
    Content-Length: 35
    
    {"message": "Header User-Agent is required"}
    
  • 发送了非法的资源,422 状态码

    HTTP/1.1 422 Unprocessable Entity
    Content-Length: 149
    
    {
      "message": "Validation Failed",
      "errors": [
        {
          "resource": "Issue",
          "field": "title",
          "code": "required"
        }
      ]
    }
    

所有的 error 哈希表都有 resource, field, code 字段,以便于定位错误,code 字段则用于表示错误类型:

  • invalid: 某个字段的值非法,接口文档中会提供相应的信息
  • required: 缺失某个必须的字段
  • not_exist: 说明某个字段的值代表的资源不存在
  • already_exist: 发送的资源中的某个字段的值和服务器中已有的某个资源冲突,常见于某些值全局唯一的字段,比如 @ 用的用户名(这个错误我有纠结,因为其实有 409 状态码可以表示,但是在修改某个资源时,很一般显然请求中不止是一种错误,如果是 409 的话,多种错误的场景就不合适了)

身份验证

部分接口需要通过某种身份验证方式才能请求成功(这些接口应该在文档中标注出来),合适的身份验证解决方案目前有两种:

超文本驱动和资源发现

REST 服务的要求之一就是超文本驱动,客户端不再需要将某些接口的 URI 硬编码在代码中,唯一需要存储的只是 API 的 HOST 地址,能够非常有效的降低客户端与服务端之间的耦合,服务端对 URI 的任何改动都不会影响到客户端的稳定。

目前有几种方案试图实现这个效果:

目前所知的方案都实现了发现资源的功能,服务端同时需要实现 OPTIONS 方法,并在响应中携带 Allow 头来告知客户端当前拥有的操作权限。

分页

请求某个资源集合时,可以通过指定 count 参数来指定每页的资源数量,通过 page 参数指定页码,或根据 last_cursor 参数指定上一页最后一个资源的标识符。

如果没有传递 count 参数或者 count 参数的值为空,则使用默认值 20 , count 参数的最大上限为 100 。

如何同时传递了 last_cursorpage 参数,则使用 page

分页的相关信息会包含在 Link HeaderX-Total-Count 中。

如果是第一页或者是最后一页时,不会返回 previousnext 的 Link 。

HTTP/1.1 200 OK
X-Total-Count: 542
Link: <http://api.example.com/#{RESOURCE_URI}?last_cursor=&count=100>; rel="first",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=200&count=100>; rel="last"
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=90&count=100>; rel="previous",
      <http://api.example.com/#{RESOURCE_URI}?last_cursor=120&count=100>; rel="next",

[
  ...
]

相关资料:

数据缓存

大部分接口应该在响应头中携带 Last-Modified, ETag, Vary, Date 信息,客户端可以在随后请求这些资源的时候,在请求头中使用 If-Modified-Since, If-None-Match 等请求头来确认资源是否经过修改。

如果资源没有进行过修改,那么就可以响应 304 Not Modified 并且不在响应实体中返回任何内容。

$ curl -i http://api.example.com/#{RESOURCE_URI}
HTTP/1.1 200 OK
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:30 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT

Content
$ curl -i http://api.example.com/#{RESOURCE_URI} -H "If-Modified-Since: Thu, 05 Jul 2012 15:31:30 GMT"
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:45 GMT
Vary: Accept, Authorization
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT
$ curl -i http://api.example.com/#{RESOURCE_URI} -H 'If-None-Match: "644b5b0155e6404a9cc4bd9d8b1ae730"'
HTTP/1.1 304 Not Modified
Cache-Control: public, max-age=60
Date: Thu, 05 Jul 2012 15:31:55 GMT
Vary: Accept, Authorization
ETag: "644b5b0155e6404a9cc4bd9d8b1ae730"
Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT

相关资料:

并发控制

不严谨的实现,或者缺少并发控制的 PUTPATCH 请求可能导致 “更新丢失”。这个时候可以使用 Last-Modified 和/或 ETag 头来实现条件请求,支持乐观并发控制。

下文只考虑使用 PUTPATCH 方法更新资源的情况。

  • 客户端发起的请求如果没有包含 If-Unmodified-Since 或者 If-Match 头,那就返回状态码 403 Forbidden ,在响应正文中解释为何返回该状态码
  • 客户端发起的请求提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 值不匹配的时候,返回状态码 412 Precondition Failed
  • 客户端发起的请求提供的 If-Unmodified-Since 或者 If-Match 头与服务器记录的实际修改时间或 ETag 的历史值匹配,但资源已经被修改过的时候,返回状态码 409 Conflict
  • 客户端发起的请求提供的条件符合实际值,那就更新资源,响应 200 OK 或者 204 No Content ,并且包含更新过的 Last-Modified 和/或 ETag 头,同时包含 Content-Location 头,其值为更新后的资源 URI

相关资料:

跨域

CORS

接口支持“跨域资源共享”(Cross Origin Resource Sharing, CORS)这里这里这份中文资料有一些指导性的资料。

简单示例:

$ curl -i https://api.example.com -H "Origin: http://example.com"
HTTP/1.1 302 Found
$ curl -i https://api.example.com -H "Origin: http://example.com"
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Allow-Credentials: true

预检请求的响应示例:

$ curl -i https://api.example.com -H "Origin: http://example.com" -X OPTIONS
HTTP/1.1 302 Found
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization, Content-Type, If-Match, If-Modified-Since, If-None-Match, If-Unmodified-Since, X-Requested-With
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE
Access-Control-Expose-Headers: ETag, Link, X-Total-Count
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true

JSON-P

如果在任何 GET 请求中带有参数 callback ,且值为非空字符串,那么接口将返回如下格式的数据

$ curl http://api.example.com/#{RESOURCE_URI}?callback=foo
foo({
  "meta": {
    "status": 200,
    "X-Total-Count": 542,
    "Link": [
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=0&count=100", "rel": "first"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=90&count=100", "rel": "prev"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=120&count=100", "rel": "next"},
      {"href": "http://api.example.com/#{RESOURCE_URI}?cursor=200&count=100", "rel": "last"}
    ]
  },
  "data": // data
})

其他资料

更细节的接口设计指南

这里还有一些其他参考资料:

原文地址:https://github.com/bolasblack/http-api-guide/blob/master/README.md


Powered by Typecho.