AngularJS 1.6.x 最佳实践总结

开发小组在 2015 年 11 月的时候,就已经开始尝试使用webpack+babel+react+reflux技术栈,但是团队对这种编译式前端开发的反馈并不友好,一方面 webpack 1.x 版本的打包效率仍然较差,每次保存操作后页面 reload 的速度缓慢如蜗牛,非常影响开发过程中的心情愉悦指数。另一方面,团队的同学们对于传统jQuery+backbone+handlebar+requirejs开发方式带有思维惯性,不太能接受 JSXES6 模块化的写法。

对于 React 的组件化思想,笔者本人非常推崇,但是遗憾 facebook 并未提供出解决组件间通讯的官方实现,其 Virtual DOM 与 Webpack.sourcemap 结合使用后,debug 变成一件非常困难的事情,并未在实际开发中体现其性能和效率上的优势。且因为社区驱动的 Reflux**、Redux** 的存在,实质上又为开发带来了额外的复杂度。更具有决定因素的是,截至在 2015 年底 React 依然停留在 0.14.x 版本,技术栈本身还处于不断成熟的过程,API 也一直在调整与变化。最终从技术成熟度的角度考量,还是稳妥的选择了 **Angular 1.6.x** 版本。

在 React 进入 15.x 版本之后,穿插使用其完成了一个称为Saga的新项目,新增的 context 属性结合 Redux 使用,可以简化组件间通信不少的工作量。

早在 2013 年 beta 版发布的时候,Angular 就被视为一件神奇的事物,虽然双向绑定的性能问题一直遭到开发人员诟病,但 Google 从 2013 年至今一直给予比较完善的支持,形成了成熟的 API 文档的同时,也提供了大量的最佳实践原则。而 Gulp 这样基于事件流的前端自动化工具的兴起,简化了前、后端技术架构分离后,前端宿主环境的开发、打包、模拟数据的问题。

截至到目前为止,前端小组的同学已经使用 Angular 近 1 年半的时间,其间经历了 5 个项目、2 款产品的考验,积累了许多实践经验,仅在这里做一些总结和分享。

2017 年,Webpack2、Vue2、Angular4 的相继发布,编译式的前端开发已经成为大势所趋,且单页面场景下 Angular 在性能和组件化解耦方面暴露出非常多不足,目前勤智的前端小组正在全面转向 Vue2。

项目结构

目录 buildrelease 主要放置编译、打包压缩后的前端代码,mocks 里是基于 express 编写的模拟 Restful 接口,与前端页面服务分处于不同端口不同域下,因此 Express 中需要进行 CORS 跨域处理。partials 目录下是全部工程代码,内部模块组织结构如下:

JavaScript 业务、CSS 样式、HTML 表现层分离开来进行编写,其中 CSS 采用 Less 进行预编译,使用 Gulp 先合并全部 Less 后再处理成 CSS,便于 colorsresets 等变量全局共享。使用script.style.view.前缀便于在 vscode 或 atom 中组织代码层次,以体现更加直观、优雅的项目结构。

Index 索引页

一个非常传统的 index.html,但是内置了 URL 的配置模块,方便实施人员根据现场服务环境,对后端 URL 地址进行修改。但更好的实践是单独将其作为一个 config.js 文件外部引入,代价是需要调整打包策略,避免 Gulp 对 config.js 进行代码混淆和压缩操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
<!DOCTYPE html>
<html lang="zh-CN" ng-app="app" ng-strict-di>
<head>
<title>Angular Demo</title>
<meta name="renderer" content="webkit" />
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="assets/favicon.ico" type="image/png" />
<!-- base -->
<link
href="libraries/font-awesome/css/font-awesome.min.css"
rel="stylesheet"
/>
<link href="libraries/animate/animate.min.css" rel="stylesheet" />
<!-- jquery plugin -->
<link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<link href="libraries/admin/css/AdminLTE.css" rel="stylesheet" />
<link
href="libraries/admin/css/skins/skin-red-light.css"
rel="stylesheet"
/>
<!-- angular plugin -->
<link
href="libraries/angular-tree-control/css/tree-control.css"
rel="stylesheet"
/>
<link
href="libraries/angular-ui-tree/angular-ui-tree.min.css"
rel="stylesheet"
/>
<!-- bundle -->
<link href="bundles/styles.css" rel="stylesheet" />
</head>

<body class="fixed skin-red-light layout-top-nav">
<div id="app" ui-view></div>
<!-- base -->
<script src="libraries/jquery/jquery.min.js"></script>
<script src="libraries/lodash/lodash.min.js"></script>
<script src="libraries/moment/moment-with-locales.min.js"></script>
<!-- jquery plugin -->
<script src="libraries/bootstrap/js/bootstrap.min.js"></script>
<script src="libraries/admin/js/app.js"></script>
<script src="libraries/jquery-fastclick/fastclick.min.js"></script>
<script src="libraries/jquery-slimscroll/jquery.slimscroll.min.js"></script>
<!-- angular -->
<script src="libraries/angular/angular.js"></script>
<script src="libraries/angular/angular-animate.js"></script>
<script src="libraries/angular/angular-messages.js"></script>
<script src="libraries/angular/angular-aria.js"></script>
<script src="libraries/angular/i18n/angular-locale_zh-cn.js"></script>
<script src="libraries/angular/angular-sanitize.js"></script>
<!-- angular plugin -->
<script src="libraries/angular-router/angular-ui-router.js"></script>
<script src="libraries/angular-ui-select/select.min.js"></script>
<script src="libraries/angular-ui/ui-bootstrap-tpls.js"></script>
<!-- bundle -->
<script>
angular.module("app.common", []).constant("URL", {
"master": "http://192.168.13.77:8081/test"
"slave": "http://192.168.13.77:8080/test"
});
</script>
<script src="bundles/scripts.js"></script>
</body>
</html>

App 启动点

该文件是整个项目的程序入口点,Gulp 自动化压缩后会作为 bundle.js 文件最顶部的一段代码,因此这里开启 Javascript 严格模式后全局有效。每个 Javascript 源文件都使用立即调用函数表达式IIFEImmediately-Invoked Function Expression)进行封装,防止局部变量泄露到全局。runconfig代码块编写为函数名称进行引用,从而避免 Javascript 函数过度嵌套后,影响代码的可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"use strict";

(function () {
angular
.module("app", [
// library
"ngAnimate",
"ngSanitize",
"ui.router",
"ui.bootstrap",
// custom
"app.common",
"app.login",
"app.layout",
])
.config(config)
.run(run);

/* config block */
config.$inject = [
"$qProvider",
"$stateProvider",
"$urlRouterProvider",
"$httpProvider",
];

function config(
$qProvider,
$stateProvider,
$urlRouterProvider,
$httpProvider
) {}

/* run block */
run.$inject = ["$rootScope"];

function run($rootScope) {}
})();

Module 模块

为了更直观的体现Angular 模块化的概念,会将如下代码新建为单独的a.module.js文件,主要用于模块的依赖声明,以及嵌套路由配置。其中,路由使用了ui-router提供的方案,父级路由使用abstract属性和template:"<ui-view/>"来实现与子路由的松耦合。

Angular 当中 module 里的路由配置是整份前端代码的切割点,通过它完成了整个单页面应用在源码层面的文件切分。更重要的是,通过controllerAs的使用,在接下来的控制器中,通过this指针代替传统$scope的写法,有助于避免在有嵌套的 controller 中调用$parent,有效防止作用域污染的发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function () {
angular.module("app.template", []);

angular.module("app.template").config(templateConfig);

templateConfig.$inject = ["$stateProvider"];

function templateConfig($stateProvider) {
$stateProvider
.state("template", {
parent: "layout",
abstract: true,
url: "/template",
template: "<ui-view/>",
})
// Judged
.state("template.judged", {
parent: "template",
url: "/judged",
templateUrl: "partials/template/judge/view.html",
controller: "TemplateJudgeController",
controllerAs: "Judge",
})
// Trial
.state("template.trial", {
parent: "template",
url: "/trial",
templateUrl: "partials/template/trial/view.html",
controller: "TemplateTrialController",
controllerAs: "Trial",
});
}
})();

Controller 控制器

控制器中,通过$inject属性注解手动实现依赖注入,避免代码压缩合并后出现依赖丢失的情况。将this指针赋值给vm对象(view model),其它方法和属性都以子对象的形式挂载到 vm 下面,使其更加整洁和面向对象。

由于 Angular 会自动绑定未在 HTML 中声明的属性,因此约定将所有双向绑定属性声明到 vm 对象当中,且通过赋予其默认值来表达所属数据类型。

下面代码中的activate()函数主要用来放置 controller 的初始化逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
(function () {
angular.module("app.login").controller("LoginController", LoginController);

LoginController.$inject = [
"loginService",
"$state",
"verify",
"$uibModal",
"$scope",
];

function LoginController(loginService, $state, verify, $uibModal, $scope) {
var vm = this;

// initialization
function activate() {}

// two-way data binding on view
vm.account = {
username: "",
password: "",
message: "",
onSubmit: function () {
loginService
.auth({
username: vm.account.username,
password: vm.account.password,
})
.then(function (result) {
if (loginService.validate(result)) {
vm.account.message = result.message;
}
});
},
};

// initialization function invoked
activate();
}
})();

Service 服务

主要用来放置数据操作和数据交互的逻辑,例如:负责 XHR 请求、本地存储、内存存储和其它任何数据操作。最后,Service 通过返回一个对象来组织这些服务。通常情况下,项目每个模块只拥有一个 controller,但是可以存在多个 service,Angular 的设计理念就是寄希望通过 service 来完成业务的复用,这一点主要继承了传统 MVC 的分层思想。

Angular 的 service 总是单例的,这意味每个injector都只有一个实例化的service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function () {
angular.module("app.login").service("loginService", loginService);

loginService.$inject = ["$http", "URL"];

function loginService($http, URL) {
var path = URL.master;
return {
auth: function (data) {
return $http.post(path + "/login", data).catch(function (error) {
console.error(error);
});
},
validate: function () {
// it is a login validator
},
};
}
})();

Directive 指令

指令的命名需要使用一个短小、唯一、具有描述性的前缀(比例企业名称),可以通过在指令配置对象中使用controllerAs属性,取得与控制器中 vm 同样的用法,

使用 controllerAs 属性时,如果需要把父级作用域绑定到指令 controller 属性指定的作用域时,可以使用bindToController=true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
(function () {
/**
* @type directive
* @name uinika-scroll
* @param uinika-scroll-offset
* @description auto resize base on overflow-y
*/
angular.module("app.common").directive("uinikaScroll", uinikaScroll);

uinikaScroll.$inject = ["$window", "$document"];

function uinikaScroll($window, $document) {
return {
restrict: "ACE",
scope: {
offset: "@uinikaScrollOffset",
},
link: function link(scope, element, attrs) {
var _window = $($window);
var _document = $($document);
var _offset = $window.parseInt(scope.offset);

format();

_window.resize(function () {
format();
});

function format() {
// scroll
element.css({
"overflow-y": "scroll",
});
// size
var contentHeight = _window.height() || 0;
var navbarHeight = _document.find(".main-header").outerHeight() || 0;
var boxheaderHeight =
_document.find(".box-header").outerHeight() || 0;
// calculate
element.outerHeight(
contentHeight - navbarHeight - boxheaderHeight - _offset
);
}
},
};
}

/**
* @type directive
* @name slim-scroll
* @param slim-scroll-offset slim-scroll-width slim-scroll-color slim-scroll-edge
* @description auto resize base on slimscroll
*/
angular.module("app.common").directive("slimScroll", slimScroll);

slimScroll.$inject = ["$window", "$document"];

function slimScroll($window, $document) {
return {
restrict: "ACE",
scope: {
height: "@slimScrollOffset",
size: "@slimScrollWidth",
color: "@slimScrollColor",
distance: "@slimScrollEdge",
},
link: function link(scope, element, attrs) {
var _window = $($window);
var _document = $($document);
// property
var _height = $window.parseInt(scope.height);
var _size = scope.size ? scope.size + "px" : "6px";
var _color = scope.color ? scope.color + "" : "red";
var _distance = scope.distance ? scope.distance + "px" : "0px";

format();

_window.resize(function () {
format();
});

function format() {
// size
var contentHeight = _window.height() || 0;
var navbarHeight = _document.find(".main-header").outerHeight() || 0;
var boxheaderHeight =
_document.find(".box-header").outerHeight() || 0;
// calculate
element.slimScroll({
height:
contentHeight - navbarHeight - boxheaderHeight - _height + "px",
color: _color,
size: _size,
distance: _distance,
alwaysVisible: true,
});
}
},
};
}
})();

Filter 过滤器

过滤输出给用户的表达式值,可用于viewcontrollerservice

1
2
3
4
5
6
7
8
9
10
11
12
(function () {
angular.module("app.common").filter("trim", trim);

trim.$inject = ["$sce"];

function trim($sce) {
return function (input) {
if (typeof input === "string" && input) input.replace(/\s/g, "");
return out;
};
}
})();

最佳实践是只通过 filter 筛选指定的对象属性,而非扫描对象本身,避免带来糟糕的性能问题。

JWT 权限控制

为了适配移动端浏览器,采用 JWT(JSON Web Token,一种 JSON 风格的轻量级的授权和身份认证规范)作为前后端交互时的权限控制协议。主要是考虑前后端分离之后,在不借助 cookie 和 session 的场景下(部分移动端浏览器未实现相关特性),使浏览器端发起的每个 HTTP 请求都能正确携带权限信息,以便于服务器端进行有效行拦截。

单页面场景下,权限认证需要关注如下 3 个核心问题:

  1. 登陆校验成功后,持有服务器端生成的 token 令牌。
  2. 每次 HTTP 请求都需要持有该 token 令牌的信息。
  3. 登陆超时和访问未授权页面的处理(通常为跳转)。

如下代码为项目入口点config代码块的配置,用于获取并放置 token 令牌,以及处理登陆超时的跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
config.$inject = [
"$qProvider",
"$stateProvider",
"$urlRouterProvider",
"$httpProvider",
];

function config($qProvider, $stateProvider, $urlRouterProvider, $httpProvider) {
$qProvider.errorOnUnhandledRejections(false);
$urlRouterProvider.otherwise("/login");
$stateProvider
.state("login", {
url: "/login",
templateUrl: "partials/login/view.html",
controller: "LoginController",
controllerAs: "Login",
})
.state("layout", {
url: "/layout",
templateUrl: "partials/layout/view.html",
controller: "LayoutController",
controllerAs: "Layout",
});

/** HTTP Interceptor */
$httpProvider.interceptors.push(interceptor);
interceptor.$inject = ["$q", "$location"];

function interceptor($q, $location) {
return {
request: function (config) {
var token = sessionStorage.token;
if (token) {
config.headers = _.assign(
{},
{
Authorization: "uinika " + token,
},
config.headers
);
}
return config;
},
response: function (response) {
$q.when(response, function (result) {
if (
response.data &&
response.data.head &&
typeof response.data === "object"
) {
if (result.data.head.status === 202) {
sessionStorage.message = "登录超时,请重新登录!";
$location.url("/uinika");
}
}
});
return response;
},
};
}
}

如下代码为项目入口点run代码块的配置,主要是处理未登陆访问授权页面的跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
run.$inject = ["$rootScope"];

function run($rootScope) {
// router listener
$rootScope.$on(
"$stateChangeStart",
function (event, toState, toParams, fromState, fromParams) {
if (toState.name !== "login") {
if (!sessionStorage.token) {
window.location.href = "/uinika";
}
}
}
);
}

Module 中的 Run 和 Config

Run 和 Config 分别是 Aangular 模块加载的 2 个生命周期:

  1. Config:首先执行的是 Config 阶段,该阶段发生在 provider 注册和配置的时候,主要用于连接并注册好所有数据源,因此providerconstant都可以注入到 Config 代码块中,但是其它不确定是否初始化完成的服务不能注入进来。
  2. run:其次开始进入 Run 阶段,该阶段发生在 injector 创建完成之后,主要用于启动应用,是 Angular 中最接近 C 语言 main 方法的概念。为了避免在模块启动之后再进行配置操作,所以只允许注入servicevalueconstant

$location 的配置

$location服务是 Angular 对浏览器原生window.location的封装,可以通过$locationProvider进行配置。

1
$locationProvider.html5Mode(true).hashPrefix("*");
  • Hashbang 模式http://localhost:5008/#!/login?user=hank
  • HTML5 模式http://localhost:5008/login?user=hank

Hashbang 是指由#!构成的字符串,类 UNIX 系统会将 Hashbang 后内容作为解释器指令进行解析。

跨控制器的事件交互

当 Angular 当中同一张页面存在多个控制器时(例如使用了嵌套路由),可以通过 scope 的事件机制进行通信。

  • $scope.$on(name, listener); 监听给定类型的事件。
  • $scope.$emit(name, args);派发事件,可以携带参数。
  • $scope.$broadcast(name, args);派发事件,可以携带参数。
1
2
3
4
5
6
7
8
9
// 事件广播
$scope.$broadcast("SEARCH", vm.search.input);

// 监听事件
function activate() {
$scope.$on("SEARCH", function (event, data) {
vm.menu.fetch();
});
}

事件相关的处理($on()方法),可以统一写在每个控制器的初始化方法activate()当中。

Angular 的 HTML 模板编译步骤

Angular 中 HTML 模板的编译会经历下面 3 个步骤:

  1. $compile 遍历 DOM 查找匹配的 Angular 指令。

  2. 当 DOM 上的所有指令被识别,$compile会按其priority属性的优先级进行排序,接下来指令的compile函数被执行(每个 compile 函数都拥有 1 次修改 DOM 的机会),每一条指令的compile函数都将返回一个link函数,这些函数最后会被合并到一个统一的链接函数当中。

  3. $compile通过这个被合并的链接函数,依次调用每个指令的link函数,注册监听器到 HTML 元素,以及在scope中设置$watch,最后完成scopetemplate的双向绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var $compile = ...; // injected into your code
var scope = ...;
var parent = ...; // DOM element where the compiled template can be appended

var html = "<div ng-bind="exp"></div>";

// Step 1: parse HTML into DOM element
var template = angular.element(html);

// Step 2: compile the template
var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.
var element = linkFn(scope);

// Step 4: Append to DOM (optional)
parent.appendChild(element);

上面是 Angular 官方文档中非常具有信息量的一段 demo 代码。

指令 complie()和 link()的区别

compilelink 分为两个阶段主要是出于性能的考虑,让多次 Model 的变化只引发一次 DOM 结构的改变。

  • compile():对 HTML 模板自身进行转换,仅在编译阶段执行一次。
1
2
3
4
5
6
7
8
function compile(tElement, tAttrs, transclude) {
return {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
return function postLink( ... ) { ... }
}
  • link(): 在 ViewModel 之间进行动态关联,将会被执行多次,只能在未定义compile的场景下使用。
1
2
3
4
5
6
7
8
function link(scope, iElement, iAttrs, controller, transcludeFn) {
link: {
pre: function preLink(scope, iElement, iAttrs, controller) { ... },
post: function postLink(scope, iElement, iAttrs, controller) { ... }
}
// or
link: function postLink( ... ) { ... }
}

$digest 循环

Angular 增强了浏览器原生的事件循环(event loop)机制,将事件循环分为JavaScript 原生Angular 可执行上下文两部分,只有进入 Angular 可执行上下文才能够使用双向数据绑定、异常处理、属性观察等特性。

可以在 JavaScript 中通过$apply()方法进入到 Angular 可执行上下文,在大多数controllerservice当中$apply()已经被隐式的调用,只有在整合第 3 方类库需要自定义事件时才会显式使用$apply()

  1. 调用scope.$apply(stimulusFn)进入 Angular 可执行上下文,作为参数的 stimulusFn() 函数就是需要运行在 Angular 可执行上下文内的代码。
  2. Angular 执行 stimulusFn() 函数,该函数通常会对应用状态进行修改。
  3. Angular 进入 $digest 循环,$digest 循环又分为 $evalAsync 队列和 $watch 列表 2 个较小的循环。$digest 循环会迭代执行直到 Model 状态稳定下来,即 $evalAsync 队列为空且 $watch 列表中检测不到任何变化的时候。
  4. $evalAsync 队列主要用来异步处理 Angular 执行上下文之外的任务(例如基于当前 scope 对表达式进行异步渲染),这一过程将会发生在浏览器视图渲染之前,从而避免视图闪烁。
  5. $watch 列表是一个在最后一次迭代之后,依然可能发生变化的表达式集合。一旦检测到变化发生,$watch()函数将会被调用,并使用改变后的值对 DOM 进行更新。
  6. $digest 循环结束后,执行流程离开 Angular 和 JavaScript 上下文。

综上所述,$digest 循环作为 Angular 的脏值检查机制,繁重的实现过程造成其性能开销较大。因此视图层绑定数据时,最佳实践原则是尽量使用::一次性绑定,以便减少不必要的 $digest 循环。

1
<span>{{::username}}</span>

如何理解 Provider

Provider 用于创建可以由 injector 依赖注入的服务,Provider 需要通过 auto 模块中的$provide 服务进行创建,Provider 拥有provider()value()factory()service()constant()decorator()六种创建方式。

  1. provider(name, provider) 该方式必须实现一个$get方法,是其它 Provide 创建方式的核心(不包括 Constant)。
  2. constant(name, obj) 定义常量,可以被注入到任何地方,但是不能被 decorator 装饰,也不能被注入其它 service。
  3. value(name, obj) 可以是任意数据类型,不能被注入到 config,但可以被 decorator 装饰。
  4. factory(name, fn) 创建可注入的普通函数,可以 return 任意值,实质是只拥有$get 方法的 provider。
  5. service(name, Fn) 创建可注入的构造函数,调用时会通过new关键字,可以不用 return 任何值,它在 AngularJS 中是单例的。
  6. decorator(name, decorFn) 用来装饰其他 provider,可以中断服务的创建流程,然后重写或者修改服务的行为。但 Constant 不能被装饰,因为 Constant 并非 provider()创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
(function () {
var module = angular
.module("app.test", [])
.config(testConfig)
.controller("TestController", TestController);

/* Config */
testConfig.$inject = ["$provide"];

function testConfig($provide) {
// 模块上定义provider
var provider = $provide.provider("message", function () {
var message;
return {
setMessage: function (infomation) {
message = infomation;
},
$get: function () {
return {
infomation: "This is a " + message,
};
},
};
});
provider.setMessage("Provider!"); // 设置Provider需要返回的消息

// config中定义factory
$provide.factory("factory", function () {
return {
reminder: "This is a Factory!",
};
});
// config中定义service
$provide.service("service", function () {
this.warning = "This is a Service!";
});

// config中定义constant
$provide.constant("CONSTANT", "This is a Constant!");
// config中定义value
$provide.value("value", "This is a Value!");

// config上定义decorator
$provide.decorator("value", decorator);
decorator.$inject = ["$delegate"]; // $delegate是需要被修饰服务的实例,即上面语句中声明的value
function decorator($delegate) {
return $delegate + " with Decorator!!"; // 对value进行修饰
}
}

/* Controller */
TestController.$inject = [
"message",
"CONSTANT",
"value",
"factory",
"service",
];

function TestController(message, CONSTANT, value, factory, service) {
// controller当中调用各个provider
console.info(message.infomation);
console.info(CONSTANT);
console.info(value);
console.info(factory.reminder);
console.info(service.warning);
}

/**
* This is a Provider!
* This is a Constant!
* This is a Value! with Decorator!!
* This is a Factory!
* This is a Service!
*/
})();

除了在 config 当中通过$provide定义 provider 之外,还可以通过 module 上提供的语法糖方法更加方便的建立 provider,出于代码松耦合以及分块编写的考虑,这里推荐使用语法糖的方式去提供 provider。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
(function () {
var module = angular
.module("app.test", [])
.config(testConfig)
.controller("TestController", TestController);

testConfig.$inject = ["$provide", "messageProvider"];

function testConfig($provide, messageProvider) {
messageProvider.setMessage("Provider!"); // provider可以被注入到config中
}

// module上定义provider
module.provider("message", function () {
var message;
return {
setMessage: function (infomation) {
message = infomation;
},
$get: function () {
return {
infomation: "This is a " + message,
};
},
};
});

// module上定义factory
module.factory("factory", function () {
return {
reminder: "This is a Factory!",
};
});

// module上定义service
module.service("service", function () {
this.warning = "This is a Service!";
});

module.constant("CONSTANT", "This is a Constant!"); // module上定义constant
module.value("value", "This is a Value!"); // module上定义value

// module上定义decorator
module.decorator("value", decorator);
decorator.$inject = ["$delegate"];
function decorator($delegate) {
return $delegate + " with Decorator!!";
}

TestController.$inject = [
"message",
"CONSTANT",
"value",
"factory",
"service",
];

function TestController(message, CONSTANT, value, factory, service) {
// controller当中调用各个provider
console.info(message.infomation);
console.info(CONSTANT);
console.info(value);
console.info(factory.reminder);
console.info(service.warning);
}

/**
* This is a Provider!
* This is a Constant!
* This is a Value! with Decorator!!
* This is a Factory!
* This is a Service!
*/
})();

Angular 当中的$q

一个 promises/deferred 对象的 Promises/A+兼容实现,受到Kris Kowal"s Q启发但并未实现全部功能,$q 能够以如下 2 种流行的方式进行使用。

  1. Kris Kowal"s Q 或者 jQuery 的 Deferred 对象:首先通过$q.defer()创建一个 deferred 对象,然后通过 deferred 对象的 promise 属性转换为 Promise 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function asyncGreet(name) {
var deferred = $q.defer();

setTimeout(function () {
deferred.notify("About to greet " + name + ".");
if (okToGreet(name)) {
deferred.resolve("Hello, " + name + "!");
} else {
deferred.reject("Greeting " + name + " is not allowed.");
}
}, 1000);

return deferred.promise;
}
  1. 类似 ES6 原生 Promise 的方式:$q 作为构造函数,接收resolver()函数作为第 1 个参数,$q(resolver)将会返回一个新建的 Promise 对象。
1
2
3
4
5
6
7
8
9
$q(function (resolve, reject) {
setTimeout(function () {
if (okToGreet(name)) {
resolve("Hello, " + name + "!");
} else {
reject("Greeting " + name + " is not allowed.");
}
}, 1000);
});

出于 team 逐步向 ES6 标准演进的考虑,这里推荐使用 ES6 原生风格的 Promise 写法。

不容忽视的$sce 服务

\(sce用于在Angular中提供严格的上下文转义(*SCE, Strict Contextual Escaping*)服务,从而避免XSS(*跨站脚本攻击*), clickjacking(*点击劫持*)等安全性问题,\)sce 服务目前支持的上下文类型有 5 种。

1
2
3
4
5
6
7
8
9
10
11
(function () {
angular.module("app.common").filter("uinikaTrustHtml", uinikaTrustHtml);

uinikaTrustHtml.$inject = ["$sce"];

function uinikaTrustHtml($sce) {
return function (val) {
return $sce.trustAsHtml(val);
};
}
})();

最佳实践原则:项目中所有用户可以自由编辑的位置,都需要通过$sce进行无害化处理。