开发小组在 2015 年 11
月的时候,就已经开始尝试使用webpack
+babel
+react
+reflux
技术栈,但是团队对这种编译式前端开发的反馈并不友好,一方面
webpack 1.x 版本的打包效率仍然较差,每次保存操作后页面
reload
的速度缓慢如蜗牛,非常影响开发过程中的心情愉悦指数。另一方面,团队的同学们对于传统jQuery
+backbone
+handlebar
+requirejs
开发方式带有思维惯性,不太能接受
JSX 和 ES6 模块化的写法。
对于 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。
项目结构
目录 build
、release
主要放置编译、打包压缩后的前端代码,mocks
里是基于
express
编写的模拟 Restful
接口,与前端页面服务分处于不同端口不同域下,因此 Express 中需要进行
CORS 跨域处理。partials
目录下是全部工程代码,内部模块组织结构如下:
将 JavaScript 业务、CSS
样式、HTML 表现层分离开来进行编写,其中 CSS 采用 Less
进行预编译,使用 Gulp 先合并全部 Less 后再处理成 CSS,便于
colors
、resets
等变量全局共享。使用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" /> <link href ="libraries/font-awesome/css/font-awesome.min.css" rel ="stylesheet" /> <link href ="libraries/animate/animate.min.css" rel ="stylesheet" /> <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" /> <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" /> <link href ="bundles/styles.css" rel ="stylesheet" /> </head > <body class ="fixed skin-red-light layout-top-nav" > <div id ="app" ui-view > </div > <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 > <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 > <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 > <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 > <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
源文件都使用立即调用函数表达式IIFE (Immediately-Invoked
Function
Expression )进行封装,防止局部变量泄露到全局。run
和config
代码块编写为函数名称进行引用,从而避免
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" , [ "ngAnimate" , "ngSanitize" , "ui.router" , "ui.bootstrap" , "app.common" , "app.login" , "app.layout" , ]) .config (config) .run (run); config.$inject = [ "$qProvider" , "$stateProvider" , "$urlRouterProvider" , "$httpProvider" , ]; function config ( $qProvider, $stateProvider, $urlRouterProvider, $httpProvider ) {} 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/>" , }) .state ("template.judged" , { parent : "template" , url : "/judged" , templateUrl : "partials/template/judge/view.html" , controller : "TemplateJudgeController" , controllerAs : "Judge" , }) .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 ; function activate ( ) {} 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 ; } }); }, }; 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 ( ) { }, }; } })();
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 ( ) { 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 ( ) { element.css ({ "overflow-y" : "scroll" , }); var contentHeight = _window.height () || 0 ; var navbarHeight = _document.find (".main-header" ).outerHeight () || 0 ; var boxheaderHeight = _document.find (".box-header" ).outerHeight () || 0 ; element.outerHeight ( contentHeight - navbarHeight - boxheaderHeight - _offset ); } }, }; } 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); 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 ( ) { var contentHeight = _window.height () || 0 ; var navbarHeight = _document.find (".main-header" ).outerHeight () || 0 ; var boxheaderHeight = _document.find (".box-header" ).outerHeight () || 0 ; element.slimScroll ({ height : contentHeight - navbarHeight - boxheaderHeight - _height + "px" , color : _color, size : _size, distance : _distance, alwaysVisible : true , }); } }, }; } })();
Filter 过滤器
过滤输出给用户的表达式值,可用于view
、controller
、service
。
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 个核心问题:
登陆校验成功后,持有服务器端生成的 token 令牌。
每次 HTTP 请求都需要持有该 token 令牌的信息。
登陆超时和访问未授权页面的处理(通常为跳转)。
如下代码为项目入口点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" , }); $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 ) { $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 个生命周期:
Config :首先执行的是 Config 阶段,该阶段发生在
provider
注册和配置的时候,主要用于连接并注册好所有数据源,因此provider 、constant 都可以注入到
Config 代码块中,但是其它不确定是否初始化完成的服务不能注入进来。
run :其次开始进入 Run 阶段,该阶段发生在 injector
创建完成之后,主要用于启动应用,是 Angular 中最接近 C 语言 main
方法的概念。为了避免在模块启动之后再进行配置操作,所以只允许注入service 、value 、constant 。
$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 个步骤:
$compile 遍历 DOM 查找匹配的 Angular 指令。
当 DOM
上的所有指令被识别,$compile
会按其priority
属性的优先级进行排序,接下来指令的compile
函数被执行(每个
compile 函数都拥有 1 次修改 DOM
的机会 ),每一条指令的compile
函数都将返回一个link
函数,这些函数最后会被合并到一个统一的链接函数当中。
$compile
通过这个被合并的链接函数,依次调用每个指令的link
函数,注册监听器到
HTML
元素,以及在scope
中设置$watch
,最后完成scope
和template
的双向绑定。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var $compile = ...; var scope = ...;var parent = ...; var html = "<div ng-bind=" exp"></div>" ;var template = angular.element (html);var linkFn = $compile(template);var element = linkFn (scope);parent.appendChild (element);
上面是 Angular 官方文档中非常具有信息量的一段 demo 代码。
指令 complie()和 link()的区别
将 compile
和 link
分为两个阶段主要是出于性能的考虑,让多次 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 ) { ... } } return function postLink ( ... ) { ... } }
link()
: 在 View
和 Model
之间进行动态关联,将会被执行多次,只能在未定义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 ) { ... } } link : function postLink ( ... ) { ... } }
$digest
循环
Angular 增强了浏览器原生的事件循环(event
loop )机制,将事件循环分为JavaScript
原生 和Angular 可执行上下文 两部分,只有进入
Angular
可执行上下文才能够使用双向数据绑定、异常处理、属性观察等特性。
可以在 JavaScript 中通过$apply()
方法进入到 Angular
可执行上下文,在大多数controller
、service
当中$apply()
已经被隐式的调用,只有在整合第
3 方类库需要自定义事件时才会显式使用$apply()
。
调用scope.$apply(stimulusFn)
进入 Angular
可执行上下文,作为参数的 stimulusFn()
函数就是需要运行在
Angular 可执行上下文内的代码。
Angular 执行 stimulusFn()
函数,该函数通常会对应用状态进行修改。
Angular 进入 $digest
循环,$digest
循环又分为 $evalAsync
队列和 $watch
列表 2
个较小的循环。$digest
循环会迭代执行直到 Model
状态稳定下来,即 $evalAsync
队列为空且 $watch
列表中检测不到任何变化的时候。
$evalAsync
队列主要用来异步处理 Angular
执行上下文之外的任务(例如基于当前 scope
对表达式进行异步渲染 ),这一过程将会发生在浏览器视图渲染之前,从而避免视图闪烁。
$watch
列表是一个在最后一次迭代之后,依然可能发生变化的表达式集合。一旦检测到变化发生,$watch()
函数将会被调用,并使用改变后的值对
DOM 进行更新。
当 $digest
循环结束后,执行流程离开 Angular 和
JavaScript 上下文。
综上所述,$digest
循环作为 Angular
的脏值检查机制,繁重的实现过程造成其性能开销较大。因此视图层绑定数据时,最佳实践原则是尽量使用::
一次性绑定,以便减少不必要的
$digest
循环。
1 <span > {{::username}}</span >
如何理解 Provider
Provider 用于创建可以由 injector 依赖注入的服务,Provider 需要通过
auto 模块中的$provide 服务进行创建,Provider
拥有provider()
、value()
、factory()
、service()
、constant()
、decorator()
六种创建方式。
provider(name, provider)
该方式必须实现一个$get
方法,是其它 Provide
创建方式的核心(不包括 Constant )。
constant(name, obj)
定义常量,可以被注入到任何地方,但是不能被 decorator
装饰,也不能被注入其它 service。
value(name, obj)
可以是任意数据类型,不能被注入到
config,但可以被 decorator 装饰。
factory(name, fn)
创建可注入的普通函数,可以 return
任意值,实质是只拥有$get 方法的 provider。
service(name, Fn)
创建可注入的构造函数,调用时会通过new
关键字,可以不用
return 任何值,它在 AngularJS 中是单例的。
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 ); testConfig.$inject = ["$provide" ]; function testConfig ($provide ) { 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!" ); $provide.factory ("factory" , function ( ) { return { reminder : "This is a Factory!" , }; }); $provide.service ("service" , function ( ) { this .warning = "This is a Service!" ; }); $provide.constant ("CONSTANT" , "This is a Constant!" ); $provide.value ("value" , "This is a Value!" ); $provide.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 ) { console .info (message.infomation ); console .info (CONSTANT ); console .info (value); console .info (factory.reminder ); console .info (service.warning ); } })();
除了在 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!" ); } module .provider ("message" , function ( ) { var message; return { setMessage : function (infomation ) { message = infomation; }, $get : function ( ) { return { infomation : "This is a " + message, }; }, }; }); module .factory ("factory" , function ( ) { return { reminder : "This is a Factory!" , }; }); module .service ("service" , function ( ) { this .warning = "This is a Service!" ; }); module .constant ("CONSTANT" , "This is a Constant!" ); module .value ("value" , "This is a Value!" ); 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 ) { console .info (message.infomation ); console .info (CONSTANT ); console .info (value); console .info (factory.reminder ); console .info (service.warning ); } })();
Angular 当中的$q
一个 promises/deferred 对象的 Promises/A+兼容实现,受到Kris Kowal"s
Q 启发但并未实现全部功能,$q 能够以如下 2 种流行的方式进行使用。
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 ; }
类似 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
进行无害化处理。