AngularJs规范

简介

本风格指南的目的是展示AngularJS应用的最佳实践和风格指南。 这些最佳实践来自于:

  1. AngularJS项目源码
  2. 本人阅读过的源码和文章
  3. 本人的实践经历

说明1: 这只是风格指南的草案,主要目的是通过交流以消除分歧,进而被社区广泛采纳。
说明2: 本版本是翻译自英文原版,在遵循下面的指南之前请确认你看到的是比较新的版本。

在本指南中不会包含基本的JavaScript开发指南。这些基本的指南可以在下面的列表中找到:

  1. Google's JavaScript style guide
  2. Mozilla's JavaScript style guide
  3. GitHub's JavaScript style guide
  4. Douglas Crockford's JavaScript style guide
  5. 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 项目模板,如 Yeomanng-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
    • $interval instead of setInterval
    • $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 而非 ngInitngInit 只有在一种情况下的使用是合适的:用来给 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 。

    有两种常见的组织模块的方式:

    1. 按照功能组织
    2. 按照组件类型组织

    当前并无太大差别,但前者更加清晰。同时,如果 lazy-loading modules 被实现的话 (当前并未列入 AngularJS 的路线图),这种方式将改善应用的性能。

控制器

  • 不要在控制器里操作 DOM,这会让你的控制器难以测试,而且违背了关注点分离原则。应该通过指令操作 DOM。
  • 通过控制器完成的功能命名控制器 (如:购物卡,主页,控制板),并以字符串Ctrl结尾。
  • 控制器是纯 Javascript 构造函数,所以应该用首字母大写的驼峰命名法(HomePageCtrlShoppingCartCtrl,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 。

过滤器

  • 使用小写字母开头的驼峰法命名过滤器。
  • 尽可能使过滤器精简。过滤器在 $digest loop 中被频繁调用,过于复杂的过滤器将使得整个应用缓慢。
  • 在过滤器中只做一件事。更加复杂的操作可以用 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的单线程特性,$digest loop 只能在单一线程进行)
      • 当监听集合时, 如果不是必要的话不要深度监听. 最好使用 $watchCollection, 对监听的表达式和之前表达式的值进行浅层的检测.
      • 当没有变量被 $timeout 回调函数所影响时,在 $timeout 设置第三个参数为 false 来跳过 $digest 循环.
      • 当面对超大不太改变的集合, 使用 immutable data structures.
    • 用打包、缓存html模板文件到你的主js文件中,减少网络请求, 可以用 grunt-html2js / gulp-html2js. 详见 这里 和 这里 。 在项目有很多小html模板并可以放进主js文件中时(通过minify和gzip压缩),这个办法是很有用的。

 

posted @ 2016-09-23 15:47  齐大齐  阅读(2161)  评论(0编辑  收藏  举报