Google Closure 用户使用的 AngularJS 风格指南

这是主要为 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 作为反例。)

1 Angular 语言规则

使用 Closure 的 goog.require 和 goog.provide 管理依赖项

为你的项目选择一个命名空间,并使用 goog.provide 和 goog.require。

goog.provide('hello.about.AboutCtrl');
goog.provide('hello.versions.Versions');

为什么? Google BUILD 规则与 closure provide/require 集成得很好。

模块

你的主应用程序模块应该位于你的根客户端目录中。 除了定义模块的地方外,永远不要更改模块。

模块既可以在与其组件相同的文件中定义(这对于仅包含一个服务的模块来说效果很好),也可以在单独的文件中定义以将各个部分连接在一起。

为什么? 对于任何想要将其作为可重用组件包含的人来说,模块应该是一致的。 如果一个模块的含义会因包含的文件而异,则它是不一致的。

模块应该使用 Angular 模块的 “name” 属性引用其他模块

例如

// 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 属性可以避免重复字符串。

使用通用的 externs 文件

这最大限度地允许 JS 编译器在存在来自 Angular 的外部提供的类型时强制执行类型安全,并且意味着你无需担心 Angular 变量以令人困惑的方式被混淆。

给 Google 外部的读者的提示:当前的 externs 文件位于 Google 内部目录中,但可以在 github 上找到一个示例 这里

JSCompiler 标志

提醒:根据 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",

控制器和 Scope

控制器是类。 方法应该在 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.providermodule.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);

2 Angular 风格规则

为 Angular 属性和服务保留 $

不要使用 $ 作为你自己的对象属性和服务标识符的前缀。 考虑 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 的应用程序中的此限制。

3 Angular 提示、技巧和最佳实践

这些不是严格的风格指南规则,但在此处作为参考提供给在 Google 开始使用 Angular 的人员。

测试

Angular 专为测试驱动开发而设计。

推荐的单元测试设置是 Jasmine + Karma(尽管你可以使用 closure 测试或 js_test)

Angular 提供了简单的适配器来加载模块并在 Jasmine 测试中使用注入器。

考虑使用应用程序结构的推荐实践

这个 目录结构文档 描述了如何使用嵌套子目录中的控制器和“components”目录中的所有组件(例如服务和指令)来构建你的应用程序。

注意 scope 继承的工作方式

请参阅 Scope 原型继承的细微之处

使用 @ngInject 进行简单的依赖注入编译

这消除了添加 myCtrl['$inject'] = ... 以防止缩小破坏 Angular 的依赖注入的需要。

用法

/**
 * My controller.
 * @param {!angular.$http} $http
 * @param {!my.app.myService} myService
 * @constructor
 * @export
 * @ngInject
 */
my.app.MyCtrl = function($http, myService) {
  //...
};

4 最佳实践链接和文档

上次修改时间:2013 年 2 月 7 日