簡介
本風格指南的目的是展示AngularJS應用的最佳實踐和風格指南。 這些最佳實踐來自於:
- AngularJS項目源碼
- 本人閱讀過的源碼和文章
- 本人的實踐經歷
說明1: 這只是風格指南的草案,主要目的是通過交流以消除分歧,進而被社區廣泛采納。
說明2: 本版本是翻譯自英文原版,在遵循下面的指南之前請確認你看到的是比較新的版本。
在本指南中不會包含基本的JavaScript開發指南。這些基本的指南可以在下面的列表中找到:
- Google's JavaScript style guide
- Mozilla's JavaScript style guide
- GitHub's JavaScript style guide
- Douglas Crockford's JavaScript style guide
- Airbnb JavaScript style guide
對於AngularJS開發,推薦 Google's JavaScript style guide.
在AngularJS的Github wiki中有一個相似的章節 ProLoser, 你可以點擊這里查看。
內容目錄
概覽
目錄結構
由於一個大型的AngularJS應用有較多組成部分,所以最好通過分層的目錄結構來組織。 有兩個主流的組織方式:
- 按照類型優先,業務功能其次的組織方式
這種方式的目錄結構看起來如下:
.
├── app
│ ├── app.js
│ ├── controllers
│ │ ├── home
│ │ │ ├── FirstCtrl.js
│ │ │ └── SecondCtrl.js
│ │ └── about
│ │ └── ThirdCtrl.js
│ ├── directives
│ │ ├── home
│ │ │ └── directive1.js
│ │ └── about
│ │ ├── directive2.js
│ │ └── directive3.js
│ ├── filters
│ │ ├── home
│ │ └── about
│ └── services
│ ├── CommonService.js
│ ├── cache
│ │ ├── Cache1.js
│ │ └── Cache2.js
│ └── models
│ ├── Model1.js
│ └── Model2.js
├── partials
├── lib
└── test
- 按照業務功能優先,類型其次的組織方式
如下:
.
├── app
│ ├── app.js
│ ├── common
│ │ ├── controllers
│ │ ├── directives
│ │ ├── filters
│ │ └── services
│ ├── home
│ │ ├── controllers
│ │ │ ├── FirstCtrl.js
│ │ │ └── SecondCtrl.js
│ │ ├── directives
│ │ │ └── directive1.js
│ │ ├── filters
│ │ │ ├── filter1.js
│ │ │ └── filter2.js
│ │ └── services
│ │ ├── service1.js
│ │ └── service2.js
│ └── about
│ ├── controllers
│ │ └── ThirdCtrl.js
│ ├── directives
│ │ ├── directive2.js
│ │ └── directive3.js
│ ├── filters
│ │ └── filter3.js
│ └── services
│ └── service3.js
├── partials
├── lib
└── test
- 當目錄里有多個單詞時, 使用 lisp-case 語法:
app
├── app.js
└── my-complex-module
├── controllers
├── directives
├── filters
└── services
- 在創建指令時,合適的做法是將相關的文件放到同一目錄下 (如:模板文件, CSS/SASS 文件, JavaScript文件)。如果你在整個項目周期都選擇這種組織方式,
app
└── directives
├── directive1
│ ├── directive1.html
│ ├── directive1.js
│ └── directive1.sass
└── directive2
├── directive2.html
├── directive2.js
└── directive2.sass
那么,上述的兩種目錄結構均能適用。
- 組件的單元測試應與組件放置在同一目錄下下。在這種方式下,當改變組件時,更加容易找到對應的測試。同時,單元測試也充當了文檔和示例。
services
├── cache
│ ├── cache1.js
│ └── cache1.spec.js
└── models
├── model1.js
└── model1.spec.js
app.js文件包含路由定義、配置和啟動說明(如果需要的話)。- 每一個 JavaScript 文件應該僅包含 一個組件 。文件名應該以組件名命名。
- 使用 Angular 項目模板,如 Yeoman, ng-boilerplate.
組件命名的約定可以在每個組件中看到。
標記
太長慎讀 把script標簽放在文檔底部。
<!DOCTYPE html>
<html lang="en"> <head> <meta charset="utf-8"> <title>MyApp</title> </head> <body> <div ng-app="myApp"> <div ng-view></div> </div> <script src="angular.js"></script> <script src="app.js"></script> </body> </html>
保持標簽的簡潔並把AngularJS的標簽放在標准HTML屬性后面。這樣提高了代碼可讀性。標准HTML屬性和AngularJS的屬性沒有混到一起,提高了代碼的可維護性。
<form class="frm" ng-submit="login.authenticate()"> <div> <input class="ipt" type="text" placeholder="name" require ng-model="user.name"> </div> </form>
其它的HTML標簽應該遵循下面的指南的 建議
標記
下表展示了各個Angular元素的命名約定
| 元素 | 命名風格 | 實例 | 用途 |
|---|---|---|---|
| Modules | lowerCamelCase | angularApp | |
| Controllers | Functionality + 'Ctrl' | AdminCtrl | |
| Directives | lowerCamelCase | userInfo | |
| Filters | lowerCamelCase | userFilter | |
| Services | UpperCamelCase | User | constructor |
| Services | lowerCamelCase | dataFactory | others |
其他
- 使用:
$timeout替代setTimeout$intervalinstead ofsetInterval$window替代window$document替代document$http替代$.ajax
這將使你更易於在測試時處理代碼異常 (例如:你在 setTimeout 中忘記 $scope.$apply)
使用如下工具自動化你的工作流 * Yeoman * Gulp * Grunt * Bower
- 使用 promise (
$q) 而非回調。這將使你的代碼更加優雅、直觀,並且免於回調地獄。 - 盡可能使用
$resource而非$http。更高的抽象可以避免冗余。 - 使用AngularJS的預壓縮版 (像 ngmin 或 ng-annotate) 避免在壓縮之后出現問題。
- 不要使用全局變量或函數。通過依賴注入解決所有依賴,這可以減少 bug ,規避很多測試時的麻煩。
- 為避免使用全局變量或函數,可以借助 Grunt 或 Gulp 把你的代碼放到一個立即執行的函數表達式(IIFE)中。可用的插件有 grunt-wrap 或 gulp-wrap。下面是 Gulp 的示例:
gulp.src("./src/*.js") .pipe(wrap('(function(){\n"use strict";\n<%= contents %>\n})();')) .pipe(gulp.dest("./dist"));
- 不要污染
$scope。僅添加與視圖相關的函數和變量。 - 使用 controllers 而非
ngInit。ngInit只有在一種情況下的使用是合適的:用來給ngRepeat的特殊屬性賦予一個別名。除此之外, 你應該使用 controllers 而不是ngInit來初始化scope變量。ngInit中的表達式會傳遞給 Angular 的$parse服務,通過詞法分析,語法分析,求值等過程。這會導致:- 對性能的巨大影響,因為解釋器由 Javascript 寫成
- 多數情況下,
$parse服務中對表達式的緩存基本不起作用,因為ngInit表達式經常只有一次求值 - 很容易出錯,因為是模板中寫字符串,沒有針對表達式的語法高亮和進一步的編輯器支持
- 不會拋出運行時錯誤
- 不要使用
$前綴來命名變量, 屬性和方法. 這種前綴是預留給 AngularJS 來使用的. - 當使用 DI 機制來解決依賴關系, 要根據他們的類型進行排序 - AngularJS 內建的依賴要優先, 之后才是你自定義的:
module.factory('Service', function ($rootScope, $timeout, MyCustomDependency1, MyCustomDependency2) { return { //Something }; });
模塊
-
模塊應該用駝峰式命名。為表明模塊
b是模塊a的子模塊, 可以用點號連接:a.b。有兩種常見的組織模塊的方式:
- 按照功能組織
- 按照組件類型組織
當前並無太大差別,但前者更加清晰。同時,如果 lazy-loading modules 被實現的話 (當前並未列入 AngularJS 的路線圖),這種方式將改善應用的性能。
控制器
- 不要在控制器里操作 DOM,這會讓你的控制器難以測試,而且違背了關注點分離原則。應該通過指令操作 DOM。
- 通過控制器完成的功能命名控制器 (如:購物卡,主頁,控制板),並以字符串
Ctrl結尾。 - 控制器是純 Javascript 構造函數,所以應該用首字母大寫的駝峰命名法(
HomePageCtrl,ShoppingCartCtrl,AdminPanelCtrl, 等等)。 - 控制器不應該在全局中定義 (盡管 AngularJS 允許,但污染全局命名空間是個糟糕的實踐)。
-
使用以下語法定義控制器:
function MyCtrl(dependency1, dependency2, ..., dependencyn) { // ... } module.controller('MyCtrl', MyCtrl);為了避免在壓縮代碼時產生問題,你可以使用工具自動生成標准的數組定義式語法,如:ng-annotate (還有 grunt 任務grunt-ng-annotate)
-
使用
controller as語法:<div ng-controller="MainCtrl as main"> {{ main.title }} </div>app.controller('MainCtrl', MainCtrl); function MainCtrl () { this.title = 'Some title'; }使用
controller as主要的優點是:- 創建了一個“獨立”的組件——綁定的屬性不屬於
$scope原型鏈。這是一個很好的實踐,因為$scope原型繼承有一些重要的缺點(這可能是為什么它在 Angular 2 中被移除了):- Scope值的改變會在你不注意的地方有影響。
- 難以重構。
- dot rule'.
- 當你不需要做必須由
$scope完成的操作(比如$scope.$broadcast)時,移除掉了$scope,就是為 Angular2 做好准備。 - 語法上更接近於普通的 JavaScript 構造函數。
想深入了解
controller as,請看: digging-into-angulars-controller-as-syntax - 創建了一個“獨立”的組件——綁定的屬性不屬於
-
如果使用數組定義語法聲明控制器,使用控制器依賴的原名。這將提高代碼的可讀性:
function MyCtrl(s) { // ... } module.controller('MyCtrl', ['$scope', MyCtrl]);下面的代碼更易理解
function MyCtrl($scope) { // ... } module.controller('MyCtrl', ['$scope', MyCtrl]);對於包含大量代碼的需要上下滾動的文件尤其適用。這可能使你忘記某一變量是對應哪一個依賴。
-
盡可能的精簡控制器。將通用函數抽象為獨立的服務。
-
不要再控制器中寫業務邏輯。把業務邏輯交給模型層的服務。 舉個例子:
// 這是把業務邏輯放在控制器的常見做法 angular.module('Store', []) .controller('OrderCtrl', function ($scope) { $scope.items = []; $scope.addToOrder = function (item) { $scope.items.push(item);//-->控制器中的業務邏輯 }; $scope.removeFromOrder = function (item) { $scope.items.splice($scope.items.indexOf(item), 1);//-->控制器中的業務邏輯 }; $scope.totalPrice = function () { return $scope.items.reduce(function (memo, item) { return memo + (item.qty * item.price);//-->控制器中的業務邏輯 }, 0); }; });當你把業務邏輯交給模型層的服務,控制器看起來就會想這樣:(關於 service-model 的實現,參看 'use services as your Model'):
// Order 在此作為一個 'model' angular.module('Store', []) .controller('OrderCtrl', function (Order) { $scope.items = Order.items; $scope.addToOrder = function (item) { Order.addToOrder(item); }; $scope.removeFromOrder = function (item) { Order.removeFromOrder(item); }; $scope.totalPrice = function () { return Order.total(); }; });為什么控制器不應該包含業務邏輯和應用狀態?
- 控制器會在每個視圖中被實例化,在視圖被銷毀時也要同時銷毀
- 控制器是不可重用的——它與視圖有耦合
- Controllers are not meant to be injected
-
需要進行跨控制器通訊時,通過方法引用(通常是子控制器到父控制器的通訊)或者
$emit,$broadcast及$on方法。發送或廣播的消息應該限定在最小的作用域。 -
制定一個通過
$emit,$broadcast發送的消息列表並且仔細的管理以防命名沖突和bug。Example:
// app.js /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Custom events: - 'authorization-message' - description of the message - { user, role, action } - data format - user - a string, which contains the username - role - an ID of the role the user has - action - specific ation the user tries to perform * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ -
在需要格式化數據時將格式化邏輯封裝成 過濾器 並將其聲明為依賴:
function myFormat() { return function () { // ... }; } module.filter('myFormat', myFormat); function MyCtrl($scope, myFormatFilter) { // ... } module.controller('MyCtrl', MyCtrl); -
有內嵌的控制器時使用 "內嵌作用域" (
controllerAs語法):app.js
module.config(function ($routeProvider) { $routeProvider .when('/route', { templateUrl: 'partials/template.html', controller: 'HomeCtrl', controllerAs: 'home' }); });HomeCtrl
function HomeCtrl() { this.bindingValue = 42; }template.html
<div ng-bind="home.bindingValue"></div>
指令
- 使用小寫字母開頭的駝峰法命名指令。
- 在 link function 中使用
scope而非$scope。在 compile 中, 你已經定義參數的 post/pre link functions 將在函數被執行時傳遞, 你無法通過依賴注入改變他們。這種方式同樣應用在 AngularJS 項目中。 - 為你的指令添加自定義前綴以免與第三方指令沖突。
- 不要使用
ng或ui前綴,因為這些備用於 AngularJS 和 AngularJS UI。 - DOM 操作只通過指令完成。
- 為你開發的可復用組件創建獨立作用域。
- 以屬性和元素形式使用指令,而不是注釋和 class。這會使你的代碼可讀性更高。
- 使用
scope.$on('$destroy', fn)來清除。這點在使用第三方指令的時候特別有用。 - 處理不可信的數據時,不要忘記使用
$sce。
過濾器
- 使用小寫字母開頭的駝峰法命名過濾器。
- 盡可能使過濾器精簡。過濾器在
$digestloop 中被頻繁調用,過於復雜的過濾器將使得整個應用緩慢。 - 在過濾器中只做一件事。更加復雜的操作可以用 pipe 串聯多個過濾器來實現。
服務
這個部分包含了 AngularJS 服務組件的相關信息。下面提到的東西與定義服務的具體方式(.provider, .factory,.service 等)無關,除非有特別提到。
-
用駝峰法命名服務。
-
用首字母大寫的駝峰法命名你自己的服務, 把服務寫成構造函數的形式,例如:
function MainCtrl($scope, User) { $scope.user = new User('foo', 42); } module.controller('MainCtrl', MainCtrl); function User(name, age) { this.name = name; this.age = age; } module.factory('User', function () { return User; }); -
用首字母小寫的駝峰法命名其它所有的服務。
-
-
把業務邏輯封裝到服務中,把業務邏輯抽象為服務作為你的
model。例如://Order is the 'model' angular.module('Store') .factory('Order', function () { var add = function (item) { this.items.push (item); }; var remove = function (item) { if (this.items.indexOf(item) > -1) { this.items.splice(this.items.indexOf(item), 1); } }; var total = function () { return this.items.reduce(function (memo, item) { return memo + (item.qty * item.price); }, 0); }; return { items: [], addToOrder: add, removeFromOrder: remove, totalPrice: total }; });如果需要例子展現如何在控制器中使用服務,請參考 'Avoid writing business logic inside controllers'。
-
將業務邏輯封裝成
service而非factory,這樣我們可以更容易在服務間實現“經典式”繼承:function Human() { //body } Human.prototype.talk = function () { return "I'm talking"; }; function Developer() { //body } Developer.prototype = Object.create(Human.prototype); Developer.prototype.code = function () { return "I'm coding"; }; myModule.service('human', Human); myModule.service('developer', Developer); -
使用
$cacheFactory進行會話級別的緩存,緩存網絡請求或復雜運算的結果。 -
如果給定的服務需要配置,把配置相關代碼放在
config回調里,就像這樣:angular.module('demo', []) .config(function ($provide) { $provide.provider('sample', function () { var foo = 42; return { setFoo: function (f) { foo = f; }, $get: function () { return { foo: foo }; } }; }); }); var demo = angular.module('demo'); demo.config(function (sampleProvider) { sampleProvider.setFoo(41); });
模板
- 使用
ng-bind或者ng-cloak而非簡單的{{ }}以防止頁面渲染時的閃爍。 - 避免在模板中使用復雜的表達式。
- 當需要動態設置
的 src時使用ng-src而非src中嵌套{{}}的模板。 - 當需要動態設置的
href時使用ng-href而非href中嵌套{{ }}的模板。 - 通過
ng-style指令配合對象式參數和 scope 變量來動態設置元素樣式,而不是將 scope 變量作為字符串通過{{ }}用於style屬性。
<script> ... $scope.divStyle = { width: 200, position: 'relative' }; ... </script> <div ng-style="divStyle">my beautifully styled div which will work in IE</div>;
路由
- 在視圖展示之前通過
resolve解決依賴。 - 不要在
resolve回調函數中顯式使用RESTful調用。將所有請求放在合適的服務中。這樣你就可以使用緩存和遵循關注點分離原則。
國際化
- 在較新版本的 Angular(>=1.4.0)下,使用內置的 i18n 工具,在較老版本下(<1.4.0),使用
angular-translate。
性能
-
優化 digest cycle
- 只監聽必要的變量。僅在必要時顯式調用
$digest循環(例如:在進行實時通訊時,不要在每次接收到消息時觸發$digest循環)。 - 對於那些只初始化一次並不再改變的內容, 使用一次性 watcher
bindonce(對於早期的 AngularJS)。如果是 AngularJS >=1.3.0 的版本,應使用Angular內置的一次性數據綁定(One-time bindings). - 盡可能使
$watch中的運算簡單。在單個$watch中進行繁雜的運算將使得整個應用變慢(由於JavaScript的單線程特性,$digestloop 只能在單一線程進行) - 當監聽集合時, 如果不是必要的話不要深度監聽. 最好使用
$watchCollection, 對監聽的表達式和之前表達式的值進行淺層的檢測. - 當沒有變量被
$timeout回調函數所影響時,在$timeout設置第三個參數為 false 來跳過$digest循環. - 當面對超大不太改變的集合, 使用 immutable data structures.
- 只監聽必要的變量。僅在必要時顯式調用
-
用打包、緩存html模板文件到你的主js文件中,減少網絡請求, 可以用 grunt-html2js / gulp-html2js. 詳見 這里 和 這里 。 在項目有很多小html模板並可以放進主js文件中時(通過minify和gzip壓縮),這個辦法是很有用的。
