前言
其實在新學一門知識時,我們應該注意下怎么書寫代碼更加規范,從開始就注意養成一個良好的習慣無論是對於bug的查找還是走人后別人熟悉代碼都是非常好的,利人利己的事情何樂而不為呢,關於AngularJS中的代碼風格分為幾節來闡述。希望對打算學習AngularJS的新手或者已經在路上的老手有那么一丟丟的幫助也是可以的。
普遍規則
tips 01(定義一個組件腳本文件時,建議此文件的代碼少於400行)
(1)有利於單元測試和模擬測試。
(2)增加可讀性、可維護性、避免和團隊在源代碼控制上的沖突。
(3)當在文件中組合組件時,可能會共享變量、依賴不需要的耦合從而避免潛在的bugs。
避免如下這樣做:
angular .module('app', ['ngRoute']) .controller('SomeController', SomeController) .factory('someFactory', someFactory); function SomeController() { } function someFactory() { }
相同的組件應該分為各自的文件(推薦如下做):
// app.module.js angular .module('app', ['ngRoute']);
angular .module('app') .controller('SomeController', SomeController); function SomeController() { }
angular .module('app') .factory('someFactory', someFactory); function someFactory() { }
JavaScript作用域
tips 02 (包含Angular的組件應該作為匿名函數立即被調用)
(1)匿名函數移除了全局作用域中的變量,能夠避免變量沖突以及變量長期存在於內存中。
(2)當代碼經過捆綁和壓縮到單個文件中,並將其文件部署到生產服務器中時會產生全局變量的沖突。
避免如下這樣做:
angular .module('app') .factory('logger', logger); function logger() { } angular .module('app') .factory('storage', storage); function storage() { }
推薦如下做:
(function() { 'use strict'; angular .module('app') .factory('logger', logger); function logger() { } })();
(function() { 'use strict'; angular .module('app') .factory('storage', storage); function storage() { } })();
定義模塊
tips 03 (聲明模塊時不要用變量來返回)
一個文件中的組件,很少使用需要引入一個變量的模塊。
避免如下這樣做:
var app = angular.module('app', [ 'ngAnimate', 'ngRoute', 'app.shared', 'app.dashboard' ]);
推薦如下做:
angular .module('app', [ 'ngAnimate', 'ngRoute', 'app.shared', 'app.dashboard' ]);
使用模塊
tips 04(使用模塊時避免使用變量代替的應該是鏈式語法)
將使代碼更加可讀,避免變量的沖突和泄漏。
避免如下這樣做:
var app = angular.module('app'); app.controller('SomeController', SomeController); function SomeController() { }
推薦如下這樣做:
angular .module('app') .controller('SomeController', SomeController); function SomeController() { }
命名函數vs匿名函數
tips 05 (使用命名函數來作為函數的回調而非匿名函數)
使代碼易讀,易於調試且降低嵌套代碼的回調量。
避免如下這樣做:
angular .module('app') .controller('DashboardController', function() { }) .factory('logger', function() { });
推薦如下這樣做:
angular .module('app') .controller('DashboardController', DashboardController); function DashboardController() { }
angular .module('app') .factory('logger', logger); function logger() { }
控制器
tips 06(使用controllerAs語法代替$scope語法)
避免如下:
<div ng-controller="CustomerController">
{{ name }}
</div>
推薦如下:
<div ng-controller="CustomerController as customer">
{{ customer.name }}
</div>
tips 07(使用控制器內部使用controllerAs語法代替$scope語法即再內部用this代替$scope)
避免如下:
function CustomerController($scope) { $scope.name = {}; $scope.sendMessage = function() { }; }
推薦如下:
function CustomerController() { this.name = {}; this.sendMessage = function() { }; }
tips 08(使用VM代替controllerAs語法即使用一個變量來捕獲this,如VM,它代表ViewModel。)
this關鍵字代表上下文,在控制器內部使用函數時可能會改變它的上下文,用一個變量來捕獲this能夠避免面臨這樣的問題。
避免如下:
function CustomerController() { this.name = {}; this.sendMessage = function() { }; }
推薦如下:
function CustomerController() { var vm = this; vm.name = {}; vm.sendMessage = function() { }; }
tips 09(在控制器的最頂部按照字母大小來排序,而非通過控制器代碼來進行擴展)
(1)在頂部綁定成員易於閱讀同時幫助我們識別可以在控制器中綁定的成員並在視圖中使用。
(2)使用匿名函數雖然可能,但是一旦代碼量超過一定數量則降低了代碼的可閱讀性。
避免如下:
function SessionsController() { var vm = this; vm.gotoSession = function() { /* ... */ }; vm.refresh = function() { /* ... */ }; vm.search = function() { /* ... */ }; vm.sessions = []; vm.title = 'Sessions'; }
推薦如下:
function SessionsController() { var vm = this; vm.gotoSession = gotoSession; vm.refresh = refresh; vm.search = search; vm.sessions = []; vm.title = 'Sessions'; //////////// function gotoSession() { /* */ } function refresh() { /* */ } function search() { /* */ } }
tips 10 (使用聲明式函數來隱藏實現細節)
使用聲明式函數來隱藏實現細節,並保持綁定的成員在頂部。當在控制器中需要綁定一個函數時,指向它到一個函數式聲明緊接着在下面。即將成員綁定在頂部且使用聲明式函數。
避免如下:
function AvengersController(avengersService, 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 avengersService.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } vm.getAvengers = getAvengers; activate(); }
推薦如下:
function AvengersController(avengersService, 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 avengersService.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } }
tips 11(在控制器中通過服務和工廠將業務邏輯導入其中)
(1)業務邏輯可能在多個控制器中被重用,將服務通過函數進行暴露。
(2)在單元測試中,業務邏輯更容易被隔離,在控制器中進行調用時更容易被模擬。
(3)消除了依賴且在控制器中隱藏了實現的細節。
避免如下:
function OrderController($http, $q, config, userInfo) { var vm = this; vm.checkCredit = checkCredit; vm.isCreditOk; vm.total = 0; function checkCredit() { var settings = {}; return $http.get(settings) .then(function(data) { vm.isCreditOk = vm.total <= maxRemainingAmount }) .catch(function(error) { }); }; }
推薦如下:
function OrderController(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(showError); }; }
tips 12 (保持控制器關注)
對一個視圖定義一個控制器,對於其他控制器不要重用控制器,代替的是將重用邏輯移到工廠以此來保持控制器簡單,更多的是關注視圖。
tips 13(分配控制器)
當控制器必須和一個視圖配對並且組件會被其他控制器和視圖重用時,通過路由來定義控制器。
避免如下:
angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html' }); } <div ng-controller="AvengersController as vm"> </div>
推薦如下:
angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm' }); } <div> </div>
服務
tips 14(單例)
服務被初始化通過new關鍵字,使用this關鍵字來修飾方法和變量,因為所有的服務是單例對象,所以對於每個injector的服務只有唯一的實例。
推薦如下:
// 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) { /* */ } }; }
工廠
tips 15(將訪問成員置頂)
(1)在頂部暴露要調用的服務的成員,加強可讀性以及單元測試。
(2)當文件足夠大時,可能需要滾動才能看到其暴露的函數。
(3)通過服務定義的接口在代碼量超過100行時避免降低代碼的可閱讀性和造成更多的滾動。
避免如下:
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() { /* */ }; }
服務
tips 16 (重構服務)
對於數據操作和將數據與工廠進行交互時重構邏輯,使數據服務負責ajax等或其他操作。
推薦如下:
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); } } }
指令
tips 17(為每個指令定義一個文件,並以此指令命名)
(1)很容易將所有指令混合在一個文件中,但是很難對於共享跨應用程序或者共享模塊等。
(2)每一個文件一個指令利於代碼的可維護性。
避免如下:
angular .module('app.widgets') .directive('orderCalendarRange', orderCalendarRange) .directive('salesCustomerInfo', salesCustomerInfo) function orderCalendarRange() { } function salesCustomerInfo() { }
推薦如下:
angular .module('sales.order') .directive('acmeOrderCalendarRange', orderCalendarRange); function orderCalendarRange() { }
angular .module('sales.widgets') .directive('acmeSalesCustomerInfo', salesCustomerInfo); function salesCustomerInfo() { }
tips 18 (提供唯一的指令前綴)
提供一個短的、唯一的、描述性的指令前綴。例如cnblogsIngUserInfo,則在html中被聲明為cnblogs-ing-user-info。
可以用唯一的指令前綴來標識指令的背景和來源,例如上述的cnblogsIngUserInfo,cnblogs代表博客園,而Ing代表閃存,User代表用戶,info代表信息。
tips 19(對元素和特性進行約束)
在AngularJS 1.3+默認的是EA,在此之下需要用Restrict來進行限制。
避免如下:
<div class="my-calendar-range"></div>
推薦如下:
<my-calendar-range></my-calendar-range> <div my-calendar-range></div>
tips 20 (在指令中使用controllerAs語法與控制器和視圖中使用該語法要一致)
推薦如下:
<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' }; 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>
tips 21(在指令添加屬性bindToController = true)
當在指令中使用controllerAs語法時,若我們想綁定外部作用域到指令的控制器的作用域令bindToController = true。
如上述tips 20初始化文本值為vm.max為undifined,若設置bindToController = true,則vm.max = 77;
解析promise
tips 22(控制器激活promise)
在一個activate函數中來啟動控制器的邏輯。
(1)在一致的地方放置啟動邏輯有利於問題的定位以及測試,同時避免在跨控制器中傳播激活邏輯。
(2)控制器激活可以更方便地重用刷新的控制器或者視圖邏輯,保持邏輯在一起,使得更快加載視圖。
避免如下:
function AvengersController(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); }
推薦如下:
function AvengersController(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; activate(); //////////// function activate() { return dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }) } }
tips 23(路由解析promise)
在控制器被激活之前,若控制器依賴於promise需要被解析時,在控制器邏輯執行之前通過$routerProvider來解析這些依賴。在控制器激活之前,如果需要依據條件來取消路由,通過路由解析來進行。
(1)在控制器加載之前之前它可能需要獲取數據,數據可能來源於自定義的工廠或$http的promise。使用路由解析使得promise在控制器邏輯執行之前被解析,因此它可能根據在promise的數據來采取不同的動作。
(2)在路由和控制器的激活函數中的代碼執行之后,視圖開始被正確加載,當激活promise解析時,數據綁定開始進行。
避免如下:
angular .module('app') .controller('AvengersController', AvengersController); function AvengersController(movieService) { var vm = this; // unresolved vm.movies; // resolved asynchronously movieService.getMovies().then(function(response) { vm.movies = response.movies; }); }
推薦如下:
// route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'AvengersController', controllerAs: 'vm', resolve: { moviesPrepService: function(movieService) { return movieService.getMovies(); } } }); } // avengers.js angular .module('app') .controller('AvengersController', AvengersController); AvengersController.$inject = ['moviesPrepService']; function AvengersController(moviesPrepService) { var vm = this; vm.movies = moviesPrepService.movies; }
或者推薦如下操作(更易於調試和處理依賴注入):
// route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'AvengersController', controllerAs: 'vm', resolve: { moviesPrepService: moviesPrepService } }); } function moviesPrepService(movieService) { return movieService.getMovies(); } // avengers.js angular .module('app') .controller('AvengersController', AvengersController); AvengersController.$inject = ['moviesPrepService']; function AvengersController(moviesPrepService) { var vm = this; vm.movies = moviesPrepService.movies; }
tips 24(用promise來處理異常)
一個promise的catch模塊必須要返回一個reject的promise來在promise鏈中維護異常。
在服務或者工廠中一定要處理異常。
(1)如果一個catch模塊沒有返回一個reject的promise,那么此時這個promise的調用者不知道異常的出現,接着調用者的then然后被執行,用戶根本不知道發生了什么。
(2)避免隱藏的錯誤以及誤導用戶。
避免如下:
function getCustomer(id) { return $http.get('/api/customer/' + id) .then(getCustomerComplete) .catch(getCustomerFailed); function getCustomerComplete(data, status, headers, config) { return data.data; } function getCustomerFailed(e) { var newMessage = 'XHR Failed for getCustomer' if (e.data && e.data.description) { newMessage = newMessage + '\n' + e.data.description; } e.data.description = newMessage; logger.error(newMessage); // *** // Notice there is no return of the rejected promise // *** } }
推薦如下:
function getCustomer(id) { return $http.get('/api/customer/' + id) .then(getCustomerComplete) .catch(getCustomerFailed); function getCustomerComplete(data, status, headers, config) { return data.data; } function getCustomerFailed(e) { var newMessage = 'XHR Failed for getCustomer' if (e.data && e.data.description) { newMessage = newMessage + '\n' + e.data.description; } e.data.description = newMessage; logger.error(newMessage); return $q.reject(e); } }
手動標注依賴注入
tips 25(手動識別依賴)
使用$inject來識別AngularJS組件中的依賴。
避免如下:
angular .module('app') .controller('DashboardController', ['$location', '$routeParams', 'common', 'dataservice', function Dashboard($location, $routeParams, common, dataservice) {} ]);
避免如下:
angular .module('app') .controller('DashboardController', ['$location', '$routeParams', 'common', 'dataservice', Dashboard]); function Dashboard($location, $routeParams, common, dataservice) { }
推薦如下:
angular .module('app') .controller('DashboardController', DashboardController); DashboardController.$inject = ['$location', '$routeParams', 'common', 'dataservice']; function DashboardController($location, $routeParams, common, dataservice) { }
注意:當函數是如下一個返回語句,此時$inject可能無法訪問(例如在指令中),此時解決這個問題的辦法是將$inject移動到控制器的外面。
tips 26($inject無效的情況)
避免如下:
function outer() { var ddo = { controller: DashboardPanelController, controllerAs: 'vm' }; return ddo; DashboardPanelController.$inject = ['logger']; // Unreachable function DashboardPanelController(logger) { } }
推薦如下:
function outer() { var ddo = { controller: DashboardPanelController, controllerAs: 'vm' }; return ddo; } DashboardPanelController.$inject = ['logger']; function DashboardPanelController(logger) { }
tips 27(手動解析路由依賴)
使用$inject來手動識別Angular組件的路由解析依賴。
推薦如下:
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(); }
異常處理
tips 28(用decorators來配置處理異常)
配置時使用$provider服務,當異常出現時在$exceptionHandler中使用decorator來處理異常。
提供一致的方式在運行時來處理未捕獲的異常。
推薦如下:
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 }; toastr.error(exception.msg, errorData); }; }
tips 29(創建工廠並暴露其接口來捕獲異常)
在代碼執行過程中可能會拋出異常,我們需要提供統一的方式來捕獲異常。
推薦如下:
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); }; } }
tips 30(使用$document和$window代替document和window)
在AngularJS中存在$document和$window兩個服務來代替document和window利於模擬和測試。
tips 31(使用$interval和$timeout代替interval和timeout)
在AngularJS中存在$interval和$timeout兩個服務來代替interval和timeout利於測試和處理Angular的digest生命周期從而保持數據同步綁定。
命名
通過使用統一的命名方式來為所有組件命名,推薦的方式為feature.type.js。如下:
文件名:cnblogs.controller.js。
注冊的組件名:CnblogsController。
tips 32(文件命名的特點)
避免如下:
// Controllers avengers.js avengers.controller.js avengersController.js // Services/Factories logger.js logger.service.js loggerService.js
推薦如下:
// 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
tips 33(控制器命名后綴為Controller)
控制器命名后綴是最常用且更明確、具體的描述。
推薦如下:
angular .module .controller('AvengersController', AvengersController); function AvengersController() { }
tips 34(工廠和服務命名)
根據其特征來統一為所有服務和工廠來命名,使用駱駝風格來命名。避免工廠和服務前綴使用$。
(1)提供一致的方式來快速識別和引用工廠。
(2)避免命名沖突。
(3)清除服務名稱,如logger,不需要其后綴。
推薦如下:
// logger.service.js angular .module .factory('logger', logger); function logger() { }
// credit.service.js angular .module .factory('creditService', creditService); function creditService() { } // customer.service.js angular .module .service('customerService', customerService); function customerService() { }
tips 36(指令組件命名)
通過使用駱駝風格來為指令組件統一命名,使用短前綴來描述這個區域信息(例如:前綴可為公司名稱或者項目名稱)。
提供統一的方式來快速識別和引用組件
推薦如下:
// cnblogs-profile.directive.js angular .module .directive('xxCnblogsProfile', xxCnblogsrProfile); // usage is <xx-cnblogs-profile> </xx-cnblogs-profile> function xxCnblogsProfile() { }
總結
本節我們講了在AngularJS中的代碼風格,我們可以一定不需要這樣做,但是我們推薦這樣做。