AngularJS 风格指南编码规范

发布于 2020-12-23 22:57:18 字数 18296 浏览 1284 评论 0

本风格指南的目的是展示 AngularJS 应用的最佳实践和风格指南。这只是风格指南的草案,主要目的是通过交流以消除分歧,进而被社区广泛采纳。本版本是翻译自英文原版,在遵循下面的指南之前请确认你看到的是比较新的版本。

概览

目录结构

由于一个大型的 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元素的命名约定

元素命名风格实例用途
ModuleslowerCamelCaseangularApp
ControllersFunctionality + 'Ctrl'AdminCtrl
DirectiveslowerCamelCaseuserInfo
FilterslowerCamelCaseuserFilter
ServicesUpperCamelCaseUserconstructor
ServiceslowerCamelCasedataFactoryothers

其他

  • 使用:
    • $timeout 替代 setTimeout
    • $interval instead of setInterval
    • $window 替代 window
    • $document 替代 document
    • $http 替代 $.ajax

这将使你更易于在测试时处理代码异常 (例如:你在 setTimeout 中忘记 $scope.$apply)

使用如下工具自动化你的工作流 * Yeoman * Gulp * Grunt * Bower

  • 使用 promise ($q) 而非回调。这将使你的代码更加优雅、直观,并且免于回调地狱。
  • 尽可能使用 $resource 而非 $http。更高的抽象可以避免冗余。
  • 使用AngularJS的预压缩版 (像 ngminng-annotate) 避免在压缩之后出现问题。
  • 不要使用全局变量或函数。通过依赖注入解决所有依赖,这可以减少 bug ,规避很多测试时的麻烦。
  • 为避免使用全局变量或函数,可以借助 Grunt 或 Gulp 把你的代码放到一个立即执行的函数表达式(IIFE)中。可用的插件有 grunt-wrapgulp-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 构造函数,所以应该用首字母大写的驼峰命名法(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 项目中。
  • 为你的指令添加自定义前缀以免与第三方指令冲突。
  • 不要使用 ngui 前缀,因为这些备用于 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压缩),这个办法是很有用的。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84960 人气
更多

推荐作者

沧笙踏歌

文章 0 评论 0

山田美奈子

文章 0 评论 0

佚名

文章 0 评论 0

岁月无声

文章 0 评论 0

暗藏城府

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文