这是主要为 Google 工程师编写的文档的外部版本。 它描述了在 Google 内部使用的、使用 Closure 的 AngularJS 应用的推荐风格。 更广泛的 AngularJS 社区成员可以自由地应用(或不应用)这些建议,以满足他们自己的用例。
本文档描述了 google3 中 AngularJS 应用的风格。 本指南补充并扩展了 Google JavaScript 风格指南。
风格说明:AngularJS 外部网页上的示例和许多外部应用都是以一种自由使用闭包、倾向于函数式继承且不经常使用 JavaScript 类型的风格编写的。 Google 遵循更严格的 Javascript 风格,以支持 JSCompiler 优化和大型代码库 - 请参阅 javascript-style 邮件列表。 这不是一个特定于 Angular 的问题,在本风格指南中不再进一步讨论。 (但如果你想进一步阅读:Martin Fowler 关于闭包, 更长的描述, closure 书的附录 A 对继承模式进行了很好的描述,并解释了它为什么偏爱伪经典模式, Javascript, the Good Parts 作为反例。)
为你的项目选择一个命名空间,并使用 goog.provide 和 goog.require。
goog.provide('hello.about.AboutCtrl'); goog.provide('hello.versions.Versions');
为什么? Google BUILD 规则与 closure provide/require 集成得很好。
你的主应用程序模块应该位于你的根客户端目录中。 除了定义模块的地方外,永远不要更改模块。
模块既可以在与其组件相同的文件中定义(这对于仅包含一个服务的模块来说效果很好),也可以在单独的文件中定义以将各个部分连接在一起。
为什么? 对于任何想要将其作为可重用组件包含的人来说,模块应该是一致的。 如果一个模块的含义会因包含的文件而异,则它是不一致的。
例如
// file submodulea.js: goog.provide('my.submoduleA'); my.submoduleA = angular.module('my.submoduleA', []); // ... // file app.js goog.require('my.submoduleA'); Yes: my.application.module = angular.module('hello', [my.submoduleA.name]); No: my.application.module = angular.module('hello', ['my.submoduleA']);
为什么? 使用 my.submoduleA 的属性可以防止 Closure 预提交失败,因为它会抱怨该文件已被 require 但从未使用过。 使用 .name 属性可以避免重复字符串。
这最大限度地允许 JS 编译器在存在来自 Angular 的外部提供的类型时强制执行类型安全,并且意味着你无需担心 Angular 变量以令人困惑的方式被混淆。
给 Google 外部的读者的提示:当前的 externs 文件位于 Google 内部目录中,但可以在 github 上找到一个示例 这里。
提醒:根据 JS 风格指南,面向客户的代码必须进行编译。
推荐:使用 JSCompiler(默认情况下与 js_binary 配合使用的 closure 编译器)以及 //javascript/angular/build_defs/build_defs 中的 ANGULAR_COMPILER_FLAGS_FULL 作为你的基本标志。
注意 - 如果你正在为方法使用 @export,则需要添加编译器标志
"--generate_exports",
如果你正在为属性使用 @export,则需要添加标志
"--generate_exports", "--remove_unused_prototype_props_in_externs=false", "--export_local_property_definitions",
控制器是类。 方法应该在 MyCtrl.prototype 上定义。
Google Angular 应用程序应使用 'controller as' 风格将控制器导出到 scope 上。 这在 Angular 1.2 中已完全实现,并且可以在 Angular 1.2 之前的版本中进行模仿。
在 Angular 1.2 之前,它看起来像
/** * Home controller. * * @param {!angular.Scope} $scope * @constructor * @ngInject * @export */ hello.mainpage.HomeCtrl = function($scope) { /** @export */ $scope.homeCtrl = this; // This is a bridge until Angular 1.2 controller-as /** * @type {string} * @export */ this.myColor = 'blue'; }; /** * @param {number} a * @param {number} b * @export */ hello.mainpage.HomeCtrl.prototype.add = function(a, b) { return a + b; };
以及模板
<div ng-controller="hello.mainpage.HomeCtrl"/> <span ng-class="homeCtrl.myColor">I'm in a color!</span> <span>{{homeCtrl.add(5, 6)}}</span> </div>
在 Angular 1.2 之后,它看起来像
/** * Home controller. * * @constructor * @ngInject * @export */ hello.mainpage.HomeCtrl = function() { /** * @type {string} * @export */ this.myColor = 'blue'; }; /** * @param {number} a * @param {number} b * @export */ hello.mainpage.HomeCtrl.prototype.add = function(a, b) { return a + b; };
如果你使用属性重命名进行编译,请使用 @export 注释公开属性和方法。 记住也要 @export 构造函数。
以及在模板中
<div ng-controller="hello.mainpage.HomeCtrl as homeCtrl"/> <span ng-class="homeCtrl.myColor">I'm in a color!</span> <span>{{homeCtrl.add(5, 6)}}</span> </div>
为什么? 将方法和属性直接放在控制器上,而不是构建 scope 对象,更符合 Google Closure 类风格。 此外,使用 'controller as' 可以清楚地知道你在访问哪个控制器,当多个控制器应用于一个元素时。 由于绑定中始终有一个 '.',因此你无需担心原型继承会屏蔽基本类型。
所有 DOM 操作都应该在指令内部完成。 指令应该保持小巧并使用组合。 定义指令的文件应该 goog.provide 一个静态函数,该函数返回指令定义对象。
goog.provide('hello.pane.paneDirective'); /** * Description and usage * @return {angular.Directive} Directive definition object. */ hello.pane.paneDirective = function() { // ... };
例外:DOM 操作可能会在服务中发生,用于与视图的其余部分断开连接的 DOM 元素,例如对话框或键盘快捷键。
使用 module.service
在模块上注册的服务是类。 除非你需要执行超出创建类的新实例的初始化之外的其他操作,否则请使用 module.service
而不是 module.provider
或 module.factory
。
/** * @param {!angular.$http} $http The Angular http service. * @constructor */ hello.request.Request = function($http) { /** @type {!angular.$http} */ this.http_ = $http; }; hello.request.Request.prototype.get = function() {/*...*/};
在模块中
module.service('request', hello.request.Request);
不要使用 $ 作为你自己的对象属性和服务标识符的前缀。 考虑 AngularJS 和 jQuery 保留的这种命名风格。
是
$scope.myModel = { value: 'foo' } myModule.service('myService', function() { /*...*/ }); var MyCtrl = function($http) {this.http_ = $http;};
否
$scope.$myModel = { value: 'foo' } // BAD $scope.myModel = { $value: 'foo' } // BAD myModule.service('$myService', function() { ... }); // BAD var MyCtrl = function($http) {this.$http_ = $http;}; // BAD
为什么? 区分 Angular / jQuery 内置函数和你自己添加的内容很有用。 此外,$ 不是 JS 风格指南中可接受的变量名字符。
对于自定义元素(例如 <ng-include src="template"></ng-include>
),IE8 需要特殊支持(类似 html5shiv 的 hack)才能启用 css 样式。 请注意针对旧版本 IE 的应用程序中的此限制。
这些不是严格的风格指南规则,但在此处作为参考提供给在 Google 开始使用 Angular 的人员。
Angular 专为测试驱动开发而设计。
推荐的单元测试设置是 Jasmine + Karma(尽管你可以使用 closure 测试或 js_test)
Angular 提供了简单的适配器来加载模块并在 Jasmine 测试中使用注入器。
这个 目录结构文档 描述了如何使用嵌套子目录中的控制器和“components”目录中的所有组件(例如服务和指令)来构建你的应用程序。
请参阅 Scope 原型继承的细微之处
这消除了添加 myCtrl['$inject'] = ...
以防止缩小破坏 Angular 的依赖注入的需要。
用法
/** * My controller. * @param {!angular.$http} $http * @param {!my.app.myService} myService * @constructor * @export * @ngInject */ my.app.MyCtrl = function($http, myService) { //... };