AngularJs 团队编码风格指南 (个人观点 by @john_papa)
如果你正在寻找一个固定的关于语法和约定的编码风格并且正在构建AngularJs应用,那么就进来看看吧,以下包含的这些内容是基于我在AngularJs、展示、Pluralsight 培训课程和团队工作中获得的经验总结出来的。
写这篇编码指南的目的是提供一个关于如何构建AngularJs的引导。我将通过展示这些我正在用的一些约定来说明,更重要的是我为什么这样用他们。
很棒的社区和声明
独木不成林。我发现AngularJs社区是一个不可思议的社区,人人都很热情的分享他们的经验。正因如此一个朋友、AngularJs专家Todd Motto 和我才能才能合作完成很多风格和约定。我们在大部分内容上意见统一,以小部分有分歧。我建议你去看一下Todd's guidelines 来体会一下他的观点,来比较着学习。
我的很多风格来自与Ward Bell 一起的结对编程。最然我们不总是意见统一,但是我的朋友Ward's在某种程度上在本指南的最终完善提供了帮助。
用一个简单的程序来了解这个风格
这篇指南只说明了是什么,为什么,怎样做,但是我发现在实际练习中更加有帮助。这篇指南伴有一个简单的程序:你可以在modular文件夹下找到它地址。当然,它是开源的。
目录
- 单一职责
- 模块
- 控制器
- 服务
- 工厂
- 指令
- 为控制器预载入保证数据
- 手动依赖注入
- @
- 异常处理
- 命名
- 应用结构
- 模块化
- Angular $ 封装服务
- 注释
- AngularJs 文档
- 贡献
- 许可声明
单一职责
- 规则1:每个文件定义一个组件。
下面的例子定义了app模块和它的依赖项、控制器和工厂在同一个文件中。
/* 避免 */ angular .module('app', ['ngRoute']) .controller('SomeController' , SomeController) .factory('someFactory' , someFactory); function SomeController() { } function someFactory() { }
相同的组件分别定义到同一个文件中。
/* 建议 */ // app.module.js angular .module('app', ['ngRoute']);
/* 建议 */ // someController.js angular .module('app') .controller('SomeController' , SomeController); function SomeController() { }
/* 建议 */ // someFactory.js angular .module('app') .factory('someFactory' , someFactory); function someFactory() { }
回到目录
模块
- 定义(亦称setter):声明模块不要用变量而是用setter语法
/* 避免 */ var app = angular.module('app', [ 'ngAnimate', 'ngRoute', 'app.shared' 'app.dashboard' ]);
最好简单的使用getter语句
/* 建议*/ angular .module('app', [ 'ngAnimate', 'ngRoute', 'app.shared' 'app.dashboard' ]);
- Getter: 当使用模块时,避免使用定义变量的方式,而是用对应的getter语法
为什么?:这会产生更多可读性强的代码,并且避免了变量的冲突和泄露(leaks)
/* 避免 */ var app = angular.module('app'); app.controller('SomeController' , SomeController); function SomeController() { }
/* 建议 */ angular .module('app') .controller('SomeController' , SomeController); function SomeController() { }
- Setting VS Getting:一次声明多处获取
为什么?:一个模块只能被创建一次,然后可以从上一个时间点获取到。
·使用angular.module('app‘, []);来声明一个模块
·使用angular.module('app');来获取一个模块
- 命名函数和匿名函数:使用命名函数而不是传递一个匿名函数作为回调
为什么?:代码可读性强,更易于调试,并且减少了嵌套的回调函数的数量。
/* 避免 */ angular .module('app') .controller('Dashboard', function () { }); .factory('logger', function () { });
/* 建议 */ // dashboard.js angular .module('app') .controller('Dashboard', Dashboard); function Dashboard () { }
// logger.js angular .module('app') .factory('logger', logger); function logger () { }
- IIFE:把AngularJs组件包装在立即执行的函数表达式中(IIFE)
为什么?:一个IIFE移除了全局作用域中的变量,也会避免变量冲突。
(function () { angular .module('app') .factory('logger', logger); function logger () { } })();
- 注意:为简单起见,指南剩下的例子中会省略IIFE语法
控制器
- controllerAs 视图语法:使用controllerAs语句替代传统的带$scope的控制器语句
为什么?:控制器已经被创建了,提供了一个新的单例,并且controllerAs语法比传统的$scope语法更接近Javascript构造器。
为什么?:你可以在视图中绑定一个“点”访问的对象(例:用customer.name代替name),这样看起来和上下文关联更紧密,更易读,并且避免了可能因为缺少“点"而出现的引用问题。
为什么?:避免在嵌套控制器视图中调用$parent。
<!-- 避免 -->
<div ng-controller="Customer">
{{ name }}
</div>
<!-- 建议 -->
<div ng-controller="Customer as customer">
{{ customer.name }}
</div>
- controllerAs语法在控制器中使用this,它是和$scope绑定的
为什么?:controllerAs是代替$scope的语法糖。你仍然可以绑定到视图,使用$scope函数。
为什么?:当$scope方法可以不用或者可以写在factory中时,帮助你避免在控制器中使用它。考虑在factory中使用$scope或者在需要时使用在controller中。例如,当使用$emit、$broadcast或者$on发布和订阅事件时,考虑把这些使用移动到factory中,然后再controller中执行。
/* 避免 */ function Customer ($scope) { $scope.name = {}; $scope.sendMessage = function () { }; }
/* 建议 */ function Customer ($scope) { $scope.name = {}; $scope.sendMessage = function () { }; }
- controllerAs中使用vm:当使用controllerAs语法时,使用this的捕获变量。选择一个始终一致的变量名例如vm,代表ViewModel(视图模型)。
为什么?:关键词this是上下文相关的,当在controller内部的一个函数中使用时可能会改变它的上下文。在一开始捕获this的上下文可以避免这个问题。
/* 避免 */ function Customer () { this.name = {}; this.sendMessage = function () { }; }
/* 建议 */ function Customer () { var vm = this; vm.name = {}; vm.sendMessage = function () { }; }
- 注意:通过在这行代码上面加注释,你可以你可以避免一些jshin的t警告。
/* jshint validthis: true */ var vm = this;
-
绑定的成员变量放到最前面:把要绑定的成员变量有序地放到控制器的最开始位置,不要分散到controller的实现过程中。
为什么?:把绑定的成员放到最前面使它读起来容易并且帮助你立刻看出那个成员变量可以被绑定或者用在视图中。
为什么?:写一个匿名函数很容易,但那时当这些函数的内容超过一行时会使函数的可读性降低。在下面定义命名函数,然后再上面通过成员变量的方式进行赋值可以提高程序可读性。
/* 避免 */ function Sessions() { var vm = this; vm.gotoSession = function() { /* ... */ }; vm.refresh = function() { /* ... */ }; vm.search = function() { /* ... */ }; vm.sessions = []; vm.title = 'Sessions';
/* 建议 */ 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() { /* */ }
- 延迟控制器逻辑:通过委托给services和factories来延迟控制器中的逻辑
为什么?:逻辑写在服务中通过函数暴露出来可以被多个控制器重用。
为什么?:service中的逻辑可以更容易被在单元测试中被隔离,虽然可调用的逻辑可以在控制器中被模拟。
为什么?:从控制器总移除依赖并隐藏并隐藏执行细节
/* 避免 */ function Order ($http, $q) { var vm = this; vm.checkCredit = checkCredit; vm.total = 0; function checkCredit () { var orderTotal = vm.total; return $http.get('api/creditcheck').then(function (data) { var remaining = data.remaining; return $q.when(!!(remaining > orderTotal)); }); }; }
/* 建议 */ function Order (creditService) { var vm = this; vm.checkCredit = checkCredit; vm.total = 0; function checkCredit () { return creditService.check(); }; }
- 分配控制器:当一个控制器必须配有一个视图并且每一个组件都可能会被被其他控制器或者views重用,在它的route下单独定义控制器
·注意:如果一个视图通过其他途径加载,则可以使用ng-controller="Avengers as vm"语法
为什么?:将route中的控制器配对允许不同的routes去执行不同的控制器和视图。当使用ng-controller把控制器分配到视图中时,视图总是会和同一个控制器相关联。
/* 避免 - 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' }); }
<div ng-controller="Avengers as vm">
</div>
/* 建议 */ // route-config.js angular .module('app') .config(config); function config ($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm' }); }
<div>
</div>
服务(Service)
- 单例:服务使用new关键字实例化,并使用this关键字来调用公共方法和变量。我一贯建议使用Factory。
- 注意:所有的AngularJs的Service都是单例。这意味着在每个注入器中,一个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) { /* */ } }; }
回到目录
工厂(Factory)
- 单一职责原则(SRP):Factory应该具有单一职责,根据上下文封装。一个Factory不能有多个目的,你要另外创建一个Factory才行。
- 单例:Factory是一个单例,并返回一个包含服务的成员的对象。
- 注意:所有的AngularJs服务都是单例。
- 公共成员定义在代码段顶部:使用从Revealing Module Pattern派生的技术,把可服务中调用的公共成员写在代码段顶部。
为什么?:这样可以使代码更具可读性,并且帮助你马上找出服务的哪些成员可以被调用和进行单元测试(或者叫它模拟)
为什么?:当代码过长的时候很有用,可以避免通过下拉滚动条来找到公共成员。
为什么?:随处定义代码可能轻松些,但是当这些函数超过一行,就会降低可读性还有无谓的拖动滚动条。使用带返回值的服务把课调用的接口的内容定义在底部,变量的定义放在顶部,更具可读性。
/* 避免 */ function dataService () { var someValue = ''; function save () { /* */ }; function validate () { /* */ }; return { save: save, someValue: someValue, validate: validate }; }
/* 建议 */ function dataService () { var someValue = ''; var service = { save: save, someValue: someValue, validate: validate }; return service; function save () { /* */ }; function validate () { /* */ }; }
- This way bindings are mirrored across the host object, primitive values cannot update alone using the revealing module pattern(译者:求大神翻译==!)
- 通过定义函数来隐藏细节代码:可访问的成员变量写在Factory代码段的顶部。函数的具体实现写在后面。
为什么?:把var a 移到var b前面程序会坏掉因为a 依赖b?像上面这样做你将不会再因此烦恼。
为什么?:函数表达式中顺序是有影响的。
/** * 避免 * 使用函数表达式 */ 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; }
/** * 建议 * 使用函数声明 * 并且把可用的变量写在顶部 */ 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)
- 分离数据获取过程:把数据操作和交互交给Factory。让数据服务(Data Services)来负责XHR调用、本地内存、或者其它的数据操作。
/* 建议 */ // 数据服务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); } } }
- 注意:下面为你展示对上面Factory的调用
/* 建议 */ // 控制器调用dataservice 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:当调用数据服务时使用了会返回Promise的服务比如$http,也让它作为你调用的函数的返回值。
为什么?:你可以把这些Promise连在一起,然后当数据调用完成并且Promise被解决或者拒绝后进行下一步操作。
/* 建议 */ activate(); function activate() { /** * 第一步 * 向getAvengers函数索要avenger数据并等待Promise */ return getAvengers().then(function() { /** * 第四步 * 在最后一个Promise完成后做些什么事情 */ logger.info('Activated Avengers View'); }); } function getAvengers() { /** * 第二步 * 向数据服务索要数据并等待Promise */ return dataservice.getAvengers() .then(function (data) { /** * 第三步 * 设置数据并解决Promise */ vm.avengers = data; return vm.avengers; }); }(译者:未完待续。。。)