加载中...

AngularJS之代码风格36条建议【一】(九)


前言

其实在新学一门知识时,我们应该注意下怎么书写代码更加规范,从开始就注意养成一个良好的习惯无论是对于bug的查找还是走人后别人熟悉代码都是非常好的,利人利己的事情何乐而不为呢,关于AngularJS中的代码风格分为几节来阐述。希望对打算学习AngularJS的新手或者已经在路上的老手有那么一丢丢的帮助也是可以的。

普遍规则

tips 01(定义一个组件脚本文件时,建议此文件的代码少于400行)

(1)有利于单元测试和模拟测试。

(2)增加可读性、可维护性、避免和团队在源代码控制上的冲突。

(3)当在文件中组合组件时,可能会共享变量、依赖不需要的耦合从而避免潜在的bugs。

避免如下这样做:

angular
    .module('app', ['ngRoute'])
    .controller('SomeController', SomeController)
    .factory('someFactory', someFactory);

function SomeController() { }

function someFactory() { }

相同的组件应该分为各自的文件(推荐如下做):

// app.module.js
angular
    .module('app', ['ngRoute']);
    angular
    .module('app')
    .controller('SomeController', SomeController);

function SomeController() { }
angular
    .module('app')
    .factory('someFactory', someFactory);

function someFactory() { }

JavaScript作用域

tips 02 (包含Angular的组件应该作为匿名函数立即被调用)

(1)匿名函数移除了全局作用域中的变量,能够避免变量冲突以及变量长期存在于内存中。

(2)当代码经过捆绑和压缩到单个文件中,并将其文件部署到生产服务器中时会产生全局变量的冲突。

避免如下这样做:

angular
    .module('app')
    .factory('logger', logger);

function logger() { }

angular
    .module('app')
    .factory('storage', storage);

function storage() { }

推荐如下做:

(function() {
    'use strict';

    angular
        .module('app')
        .factory('logger', logger);

    function logger() { }
})();
(function() {
    'use strict';

    angular
        .module('app')
        .factory('storage', storage);

    function storage() { }
})();

定义模块

tips 03 (声明模块时不要用变量来返回)

一个文件中的组件,很少使用需要引入一个变量的模块。

避免如下这样做:

var app = angular.module('app', [
    'ngAnimate',
    'ngRoute',
    'app.shared',
    'app.dashboard'
]);

推荐如下做:

angular
    .module('app', [
        'ngAnimate',
        'ngRoute',
        'app.shared',
        'app.dashboard'
    ]);

使用模块

tips 04(使用模块时避免使用变量代替的应该是链式语法)

将使代码更加可读,避免变量的冲突和泄漏。

避免如下这样做:

var app = angular.module('app');
app.controller('SomeController', SomeController);

function SomeController() { }

推荐如下这样做:

angular
    .module('app')
    .controller('SomeController', SomeController);

function SomeController() { }

命名函数vs匿名函数

tips 05 (使用命名函数来作为函数的回调而非匿名函数)

使代码易读,易于调试且降低嵌套代码的回调量。

避免如下这样做:

angular
    .module('app')
    .controller('DashboardController', function() { })
    .factory('logger', function() { });

推荐如下这样做:

angular
    .module('app')
    .controller('DashboardController', DashboardController);

function DashboardController() { }
angular
    .module('app')
    .factory('logger', logger);

function logger() { }

控制器 

tips 06(使用controllerAs语法代替$scope语法)

避免如下:

<div ng-controller="CustomerController">
    {{ name }}
</div>

推荐如下:

<div ng-controller="CustomerController as customer">
    {{ customer.name }}
</div>

tips 07(使用控制器内部使用controllerAs语法代替$scope语法即再内部用this代替$scope)

避免如下:

function CustomerController($scope) {
    $scope.name = {};
    $scope.sendMessage = function() { };
}

推荐如下:

function CustomerController() {
    this.name = {};
    this.sendMessage = function() { };
}

tips 08(使用VM代替controllerAs语法即使用一个变量来捕获this,如VM,它代表ViewModel。)

this关键字代表上下文,在控制器内部使用函数时可能会改变它的上下文,用一个变量来捕获this能够避免面临这样的问题。

避免如下:

function CustomerController() {
    this.name = {};
    this.sendMessage = function() { };
}

推荐如下:

function CustomerController() {
    var vm = this;
    vm.name = {};
    vm.sendMessage = function() { };
}

tips 09(在控制器的最顶部按照字母大小来排序,而非通过控制器代码来进行扩展)

(1)在顶部绑定成员易于阅读同时帮助我们识别可以在控制器中绑定的成员并在视图中使用。

(2)使用匿名函数虽然可能,但是一旦代码量超过一定数量则降低了代码的可阅读性。

避免如下:

function SessionsController() {
    var vm = this;

    vm.gotoSession = function() {
      /* ... */
    };
    vm.refresh = function() {
      /* ... */
    };
    vm.search = function() {
      /* ... */
    };
    vm.sessions = [];
    vm.title = 'Sessions';
}

推荐如下:

function SessionsController() {
    var vm = this;

    vm.gotoSession = gotoSession;
    vm.refresh = refresh;
    vm.search = search;
    vm.sessions = [];
    vm.title = 'Sessions';

    ////////////

    function gotoSession() {
      /* */
    }

    function refresh() {
      /* */
    }

    function search() {
      /* */
    }
}

tips 10 (使用声明式函数来隐藏实现细节)

使用声明式函数来隐藏实现细节,并保持绑定的成员在顶部。当在控制器中需要绑定一个函数时,指向它到一个函数式声明紧接着在下面。即将成员绑定在顶部且使用声明式函数。

避免如下:

function AvengersController(avengersService, logger) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    var activate = function() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    var getAvengers = function() {
        return avengersService.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }

    vm.getAvengers = getAvengers;

    activate();
}

推荐如下:

function AvengersController(avengersService, logger) {
    var vm = this;
    vm.avengers = [];
    vm.getAvengers = getAvengers;
    vm.title = 'Avengers';

    activate();

    function activate() {
        return getAvengers().then(function() {
            logger.info('Activated Avengers View');
        });
    }

    function getAvengers() {
        return avengersService.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        });
    }
}

tips 11(在控制器中通过服务和工厂将业务逻辑导入其中)

(1)业务逻辑可能在多个控制器中被重用,将服务通过函数进行暴露。

(2)在单元测试中,业务逻辑更容易被隔离,在控制器中进行调用时更容易被模拟。

(3)消除了依赖且在控制器中隐藏了实现的细节。

避免如下:

function OrderController($http, $q, config, userInfo) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit() {
        var settings = {};
        return $http.get(settings)
            .then(function(data) {
               vm.isCreditOk = vm.total <= maxRemainingAmount
            })
            .catch(function(error) {
            });
    };
}

推荐如下:

function OrderController(creditService) {
    var vm = this;
    vm.checkCredit = checkCredit;
    vm.isCreditOk;
    vm.total = 0;

    function checkCredit() {
       return creditService.isOrderTotalOk(vm.total)
          .then(function(isOk) { vm.isCreditOk = isOk; })
          .catch(showError);
    };
}

tips 12 (保持控制器关注)

对一个视图定义一个控制器,对于其他控制器不要重用控制器,代替的是将重用逻辑移到工厂以此来保持控制器简单,更多的是关注视图。

tips 13(分配控制器)

当控制器必须和一个视图配对并且组件会被其他控制器和视图重用时,通过路由来定义控制器。

避免如下:

angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
          templateUrl: 'avengers.html'
        });
}


<div ng-controller="AvengersController as vm">
</div>

推荐如下:

angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'Avengers',
            controllerAs: 'vm'
        });
}

<div>
</div>

服务

tips 14(单例)

服务被初始化通过new关键字,使用this关键字来修饰方法和变量,因为所有的服务是单例对象,所以对于每个injector的服务只有唯一的实例。

推荐如下:

// service
angular
    .module('app')
    .service('logger', logger);

function logger() {
  this.logError = function(msg) {
    /* */
  };
}
// factory
angular
    .module('app')
    .factory('logger', logger);

function logger() {
    return {
        logError: function(msg) {
          /* */
        }
   };
}

工厂

tips 15(将访问成员置顶)

(1)在顶部暴露要调用的服务的成员,加强可读性以及单元测试。

(2)当文件足够大时,可能需要滚动才能看到其暴露的函数。

(3)通过服务定义的接口在代码量超过100行时避免降低代码的可阅读性和造成更多的滚动。

避免如下:

function dataService() {
  var someValue = '';
  function save() {
    /* */
  };
  function validate() {
    /* */
  };

  return {
      save: save,
      someValue: someValue,
      validate: validate
  };
}

推荐如下:

function dataService() {
    var someValue = '';
    var service = {
        save: save,
        someValue: someValue,
        validate: validate
    };
    return service;

    ////////////

    function save() {
        /* */
    };

    function validate() {
        /* */
    };
}

服务

tips 16 (重构服务)

对于数据操作和将数据与工厂进行交互时重构逻辑,使数据服务负责ajax等或其他操作。

推荐如下:

angular
    .module('app.core')
    .factory('dataservice', dataservice);

dataservice.$inject = ['$http', 'logger'];

function dataservice($http, logger) {
    return {
        getAvengers: getAvengers
    };

    function getAvengers() {
        return $http.get('/api/maa')
            .then(getAvengersComplete)
            .catch(getAvengersFailed);

        function getAvengersComplete(response) {
            return response.data.results;
        }

        function getAvengersFailed(error) {
            logger.error('XHR Failed for getAvengers.' + error.data);
        }
    }
}

指令

tips 17(为每个指令定义一个文件,并以此指令命名)

(1)很容易将所有指令混合在一个文件中,但是很难对于共享跨应用程序或者共享模块等。

(2)每一个文件一个指令利于代码的可维护性。

避免如下:

angular
    .module('app.widgets')

    .directive('orderCalendarRange', orderCalendarRange)

    .directive('salesCustomerInfo', salesCustomerInfo)

function orderCalendarRange() {

}

function salesCustomerInfo() {
}

推荐如下:

angular
    .module('sales.order')
    .directive('acmeOrderCalendarRange', orderCalendarRange);

function orderCalendarRange() {
}
angular
    .module('sales.widgets')
    .directive('acmeSalesCustomerInfo', salesCustomerInfo);

function salesCustomerInfo() {
}

tips 18 (提供唯一的指令前缀)

提供一个短的、唯一的、描述性的指令前缀。例如cnblogsIngUserInfo,则在html中被声明为cnblogs-ing-user-info。

可以用唯一的指令前缀来标识指令的背景和来源,例如上述的cnblogsIngUserInfo,cnblogs代表博客园,而Ing代表闪存,User代表用户,info代表信息。

tips 19(对元素和特性进行约束) 

在AngularJS 1.3+默认的是EA,在此之下需要用Restrict来进行限制。

避免如下:

<div class="my-calendar-range"></div>

推荐如下:

<my-calendar-range></my-calendar-range>
<div my-calendar-range></div>

tips 20 (在指令中使用controllerAs语法与控制器和视图中使用该语法要一致)

推荐如下:

<div my-example max="77"></div>
angular
    .module('app')
    .directive('myExample', myExample);

function myExample() {
    var directive = {
        restrict: 'EA',
        templateUrl: 'app/feature/example.directive.html',
        scope: {
            max: '='
        },
        controller: ExampleController,
        controllerAs: 'vm'
    };

    return directive;
}

function ExampleController() {
    var vm = this;
    vm.min = 3;
    console.log('CTRL: vm.min = %s', vm.min);
    console.log('CTRL: vm.max = %s', vm.max);
}
<!-- example.directive.html -->
<div>hello world</div>
<div>max={{vm.max}}<input ng-model="vm.max"/></div>
<div>min={{vm.min}}<input ng-model="vm.min"/></div>

tips 21(在指令添加属性bindToController = true)

当在指令中使用controllerAs语法时,若我们想绑定外部作用域到指令的控制器的作用域令bindToController = true。

如上述tips 20初始化文本值为vm.max为undifined,若设置bindToController = true,则vm.max = 77;

解析promise 

tips 22(控制器激活promise)

在一个activate函数中来启动控制器的逻辑。

(1)在一致的地方放置启动逻辑有利于问题的定位以及测试,同时避免在跨控制器中传播激活逻辑。

(2)控制器激活可以更方便地重用刷新的控制器或者视图逻辑,保持逻辑在一起,使得更快加载视图。

避免如下:

function AvengersController(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    dataservice.getAvengers().then(function(data) {
        vm.avengers = data;
        return vm.avengers;
    });
}

推荐如下:

function AvengersController(dataservice) {
    var vm = this;
    vm.avengers = [];
    vm.title = 'Avengers';

    activate();

    ////////////

    function activate() {
        return dataservice.getAvengers().then(function(data) {
            vm.avengers = data;
            return vm.avengers;
        })
    }
}

tips 23(路由解析promise) 

在控制器被激活之前,若控制器依赖于promise需要被解析时,在控制器逻辑执行之前通过$routerProvider来解析这些依赖。在控制器激活之前,如果需要依据条件来取消路由,通过路由解析来进行。

(1)在控制器加载之前之前它可能需要获取数据,数据可能来源于自定义的工厂或$http的promise。使用路由解析使得promise在控制器逻辑执行之前被解析,因此它可能根据在promise的数据来采取不同的动作。

(2)在路由和控制器的激活函数中的代码执行之后,视图开始被正确加载,当激活promise解析时,数据绑定开始进行。

避免如下:

angular
    .module('app')
    .controller('AvengersController', AvengersController);

function AvengersController(movieService) {
    var vm = this;
    // unresolved
    vm.movies;
    // resolved asynchronously
    movieService.getMovies().then(function(response) {
        vm.movies = response.movies;
    });
}

推荐如下:

// route-config.js
angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'AvengersController',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: function(movieService) {
                    return movieService.getMovies();
                }
            }
        });
}

// avengers.js
angular
    .module('app')
    .controller('AvengersController', AvengersController);

AvengersController.$inject = ['moviesPrepService'];
function AvengersController(moviesPrepService) {
    var vm = this;
    vm.movies = moviesPrepService.movies;
}

或者推荐如下操作(更易于调试和处理依赖注入):

// route-config.js
angular
    .module('app')
    .config(config);

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'AvengersController',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: moviesPrepService
            }
        });
}

function moviesPrepService(movieService) {
    return movieService.getMovies();
}

// avengers.js
angular
    .module('app')
    .controller('AvengersController', AvengersController);

AvengersController.$inject = ['moviesPrepService'];
function AvengersController(moviesPrepService) {
      var vm = this;
      vm.movies = moviesPrepService.movies;
}

tips 24(用promise来处理异常) 

一个promise的catch模块必须要返回一个reject的promise来在promise链中维护异常。

在服务或者工厂中一定要处理异常。

(1)如果一个catch模块没有返回一个reject的promise,那么此时这个promise的调用者不知道异常的出现,接着调用者的then然后被执行,用户根本不知道发生了什么。

(2)避免隐藏的错误以及误导用户。

避免如下:

function getCustomer(id) {
    return $http.get('/api/customer/' + id)
        .then(getCustomerComplete)
        .catch(getCustomerFailed);

    function getCustomerComplete(data, status, headers, config) {
        return data.data;
    }

    function getCustomerFailed(e) {
        var newMessage = 'XHR Failed for getCustomer'
        if (e.data && e.data.description) {
          newMessage = newMessage + '\n' + e.data.description;
        }
        e.data.description = newMessage;
        logger.error(newMessage);
        // ***
        // Notice there is no return of the rejected promise
        // ***
    }
}

推荐如下:

function getCustomer(id) {
    return $http.get('/api/customer/' + id)
        .then(getCustomerComplete)
        .catch(getCustomerFailed);

    function getCustomerComplete(data, status, headers, config) {
        return data.data;
    }

    function getCustomerFailed(e) {
        var newMessage = 'XHR Failed for getCustomer'
        if (e.data && e.data.description) {
          newMessage = newMessage + '\n' + e.data.description;
        }
        e.data.description = newMessage;
        logger.error(newMessage);
        return $q.reject(e);
    }
}

手动标注依赖注入

tips 25(手动识别依赖)

使用$inject来识别AngularJS组件中的依赖。

避免如下:

angular
    .module('app')
    .controller('DashboardController',
        ['$location', '$routeParams', 'common', 'dataservice',
            function Dashboard($location, $routeParams, common, dataservice) {}
        ]);

避免如下:

angular
  .module('app')
  .controller('DashboardController',
      ['$location', '$routeParams', 'common', 'dataservice', Dashboard]);

function Dashboard($location, $routeParams, common, dataservice) {
}

推荐如下:

angular
    .module('app')
    .controller('DashboardController', DashboardController);

DashboardController.$inject = ['$location', '$routeParams', 'common', 'dataservice'];

function DashboardController($location, $routeParams, common, dataservice) {
}

注意:当函数是如下一个返回语句,此时$inject可能无法访问(例如在指令中),此时解决这个问题的办法是将$inject移动到控制器的外面。

tips 26($inject无效的情况)

避免如下:

function outer() {
    var ddo = {
        controller: DashboardPanelController,
        controllerAs: 'vm'
    };
    return ddo;

    DashboardPanelController.$inject = ['logger']; // Unreachable
    function DashboardPanelController(logger) {
    }
}

推荐如下:

function outer() {
    var ddo = {
        controller: DashboardPanelController,
        controllerAs: 'vm'
    };
    return ddo;
}

DashboardPanelController.$inject = ['logger'];
function DashboardPanelController(logger) {
}

tips 27(手动解析路由依赖)

使用$inject来手动识别Angular组件的路由解析依赖。

推荐如下:

function config($routeProvider) {
    $routeProvider
        .when('/avengers', {
            templateUrl: 'avengers.html',
            controller: 'AvengersController',
            controllerAs: 'vm',
            resolve: {
                moviesPrepService: moviesPrepService
            }
        });
}

moviesPrepService.$inject = ['movieService'];
function moviesPrepService(movieService) {
    return movieService.getMovies();
}

异常处理 

tips 28(用decorators来配置处理异常)

配置时使用$provider服务,当异常出现时在$exceptionHandler中使用decorator来处理异常。

提供一致的方式在运行时来处理未捕获的异常。

推荐如下:

angular
    .module('blocks.exception')
    .config(exceptionConfig);

exceptionConfig.$inject = ['$provide'];

function exceptionConfig($provide) {
    $provide.decorator('$exceptionHandler', extendExceptionHandler);
}

extendExceptionHandler.$inject = ['$delegate', 'toastr'];

function extendExceptionHandler($delegate, toastr) {
    return function(exception, cause) {
        $delegate(exception, cause);
        var errorData = {
            exception: exception,
            cause: cause
        };
        toastr.error(exception.msg, errorData);
    };
}

tips 29(创建工厂并暴露其接口来捕获异常) 

在代码执行过程中可能会抛出异常,我们需要提供统一的方式来捕获异常。

推荐如下:

angular
    .module('blocks.exception')
    .factory('exception', exception);

exception.$inject = ['logger'];

function exception(logger) {
    var service = {
        catcher: catcher
    };
    return service;

    function catcher(message) {
        return function(reason) {
            logger.error(message, reason);
        };
    }
}

tips  30(使用$document和$window代替document和window)

在AngularJS中存在$document和$window两个服务来代替document和window利于模拟和测试。

tips 31(使用$interval和$timeout代替interval和timeout)

 在AngularJS中存在$interval和$timeout两个服务来代替interval和timeout利于测试和处理Angular的digest生命周期从而保持数据同步绑定。

命名

通过使用统一的命名方式来为所有组件命名,推荐的方式为feature.type.js。如下:

文件名:cnblogs.controller.js。

注册的组件名:CnblogsController。

tips 32(文件命名的特点)

避免如下:

// Controllers
avengers.js
avengers.controller.js
avengersController.js

// Services/Factories
logger.js
logger.service.js
loggerService.js

推荐如下:

// controllers
avengers.controller.js
avengers.controller.spec.js

// services/factories
logger.service.js
logger.service.spec.js

// constants
constants.js

// module definition
avengers.module.js

// routes
avengers.routes.js
avengers.routes.spec.js

// configuration
avengers.config.js

// directives
avenger-profile.directive.js
avenger-profile.directive.spec.js

tips 33(控制器命名后缀为Controller)

控制器命名后缀是最常用且更明确、具体的描述。

推荐如下:

angular
    .module
    .controller('AvengersController', AvengersController);

function AvengersController() { }

tips 34(工厂和服务命名) 

根据其特征来统一为所有服务和工厂来命名,使用骆驼风格来命名。避免工厂和服务前缀使用$。

(1)提供一致的方式来快速识别和引用工厂。

(2)避免命名冲突。

(3)清除服务名称,如logger,不需要其后缀。

推荐如下:

// logger.service.js
angular
    .module
    .factory('logger', logger);

function logger() { }
// credit.service.js
angular
    .module
    .factory('creditService', creditService);

function creditService() { }

// customer.service.js
angular
    .module
    .service('customerService', customerService);

function customerService() { }

tips 36(指令组件命名)

通过使用骆驼风格来为指令组件统一命名,使用短前缀来描述这个区域信息(例如:前缀可为公司名称或者项目名称)。

提供统一的方式来快速识别和引用组件

推荐如下:

// cnblogs-profile.directive.js
angular
    .module
    .directive('xxCnblogsProfile', xxCnblogsrProfile);

// usage is <xx-cnblogs-profile> </xx-cnblogs-profile>

function xxCnblogsProfile() { }

总结

本节我们讲了在AngularJS中的代码风格,我们可以一定不需要这样做,但是我们推荐这样做。


还没有评论.