Angular 1.6.x最佳实践总结

栏目: 编程语言 · AngularJS · 发布时间: 6年前

内容简介:开发小组在2015年11月的时候,就已经开始尝试使用对于React的组件化思想,笔者本人非常推崇,但是遗憾facebook并未提供出解决组件间通讯的官方实现,其Virtual DOM与Webpack.sourcemap结合使用后,debug变成一件非常困难的事情,并未在实际开发中体现其性能和效率上的优势。且因为社区驱动的reflux、redux的存在,实质上又为开发带来了额外的复杂度。更具有决定因素的是,截至在2015年底React依然停留在0.14.x版本,技术栈本身还处于不断成熟的过程,API也一直在

开发小组在2015年11月的时候,就已经开始尝试使用 webpack + babel + react + reflux 技术栈,但是团队对这种编译式前端开发的反馈并不友好,一方面webpack 1.x版本的打包效率仍然较差,每次保存操作后页面reload速度蜗牛一样缓慢,非常影响开发过程中的心情愉悦指数。另一方面,team的同学们对于传统 jQuery + backbone + handlebar + requirejs 开发方式带有思维惯性,不太能接受JSX和ES6模块化的写法。

对于React的组件化思想,笔者本人非常推崇,但是遗憾facebook并未提供出解决组件间通讯的官方实现,其Virtual DOM与Webpack.sourcemap结合使用后,debug变成一件非常困难的事情,并未在实际开发中体现其性能和效率上的优势。且因为社区驱动的reflux、redux的存在,实质上又为开发带来了额外的复杂度。更具有决定因素的是,截至在2015年底React依然停留在0.14.x版本,技术栈本身还处于不断成熟的过程,API也一直在调整与变化。最终从技术成熟度的角度考量,还是稳妥的选择了Angular 1.6.x版本。

在React进入15.x版本之后,穿插使用其完成了一个称为 Saga 的新项目,新增的context属性结合Redux使用,可以简化组件间通信不少的工作量。

早在2013年beta版发布的时候,Angular就被视为一件神奇的事物,虽然双向绑定的性能问题一直遭到开发人员诟病,但Google从2013年至今一直给予比较完善的支持,形成了成熟的API文档的同时,也提供了大量的最佳实践原则。而Gulp这样基于事件流的前端自动化 工具 的兴起,简化了前、后端技术架构分离后,前端宿主环境的开发、打包、模拟数据的问题。

截至到目前为止,前端小组的同学已经使用Angular近1年半的时间,其间经历了5个项目、2款产品的考验,积累了许多实践经验,仅在这里做一些总结和分享。

2017年,Webpack2、Vue2、Angular4的相继发布,编译式的前端开发已经成为大势所趋,且单页面场景下Angular在性能和组件化解耦方面暴露出非常多不足,目前勤智的前端小组正在全面转向Vue2。

项目结构

目录build、release主要放置编译、打包压缩后的前端代码,mocks里是基于express编写的模拟restful接口,与前端页面服务分处于不同端口不同域下,因此express中需要进行CORS跨域处理。partials目录下是全部工程代码,内部模块组织结构如下:

Angular 1.6.x最佳实践总结

将javascript业务、css样式、html表现分离开编写,css采用Less进行预编译,使用gulp先合并全部Less后再处理成css,便于colors、resets等变量全局共享。使用 script.style.view. 前缀便于在vscode或atom中组织代码层次,以体现更加直观、优雅的项目结构。

Angular 1.6.x最佳实践总结

Index索引页

一个非常传统的index.html,但是内置了URL的配置模块,方便实施人员根据现场服务环境,对后端URL地址进行修改。但更好的实践是单独将其作为一个config.js文件外部引入,代价是需要调整打包策略,避免gulp对config.js进行代码混淆和压缩操作。

<!DOCTYPE html>
<html lang="zh-CN" ng-app="app" ng-strict-di>

<head>
  <title>Angular Demo</title>
  <meta name="renderer" content="webkit" />
  <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <link rel="icon" href="assets/favicon.ico" type="image/png" />
  <!-- base -->
  <link href="libraries/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
  <link href="libraries/animate/animate.min.css" rel="stylesheet" />
  <!-- jquery plugin -->
  <link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
  <link href="libraries/admin/css/AdminLTE.css" rel="stylesheet" />
  <link href="libraries/admin/css/skins/skin-red-light.css" rel="stylesheet" />
  <!-- angular plugin -->
  <link href="libraries/angular-tree-control/css/tree-control.css" rel="stylesheet" />
  <link href="libraries/angular-ui-tree/angular-ui-tree.min.css" rel="stylesheet" />
  <!-- bundle -->
  <link href="bundles/styles.css" rel="stylesheet" />
</head>

<body class="fixed skin-red-light layout-top-nav">
  <div id="app" ui-view></div>
  <!-- base -->
  <script src="libraries/jquery/jquery.min.js"></script>
  <script src="libraries/lodash/lodash.min.js"></script>
  <script src="libraries/moment/moment-with-locales.min.js"></script>
  <!-- jquery plugin -->
  <script src="libraries/bootstrap/js/bootstrap.min.js"></script>
  <script src="libraries/admin/js/app.js"></script>
  <script src="libraries/jquery-fastclick/fastclick.min.js"></script>
  <script src="libraries/jquery-slimscroll/jquery.slimscroll.min.js"></script>
  <!-- angular -->
  <script src="libraries/angular/angular.js"></script>
  <script src="libraries/angular/angular-animate.js"></script>
  <script src="libraries/angular/angular-messages.js"></script>
  <script src="libraries/angular/angular-aria.js"></script>
  <script src="libraries/angular/i18n/angular-locale_zh-cn.js"></script>
  <script src="libraries/angular/angular-sanitize.js"></script>
  <!-- angular plugin -->
  <script src="libraries/angular-router/angular-ui-router.js"></script>
  <script src="libraries/angular-ui-select/select.min.js"></script>
  <script src="libraries/angular-ui/ui-bootstrap-tpls.js"></script>
  <!-- bundle -->
  <script>
    angular.module("app.common", []).constant("URL", {
      "master": "http://192.168.13.77:8081/test"
      "slave": "http://192.168.13.77:8080/test"
    });
  </script>
  <script src="bundles/scripts.js"></script>
</body>

</html>

App启动点

该文件是整个项目的程序入口点,gulp自动化压缩后会作为bundle.js文件最顶部的一段代码,因此这里开启Javascript严格模式后全局有效。每个js文件都使用立即调用函数表达式 IIFEImmediately-Invoked Function Expression )进行封装,防止局部变量泄露到全局。 runconfig 代码块编写为函数名称进行引用,从而避免JS函数过度嵌套后,影响代码的可读性。

"use strict";

(function () {

  angular.module("app", [
      // library
      "ngAnimate", "ngSanitize", "ui.router", "ui.bootstrap",
      // custom
      "app.common", "app.login", "app.layout",
    ])
    .config(config)
    .run(run)

  /* config block */
  config.$inject = ["$qProvider", "$stateProvider", "$urlRouterProvider", "$httpProvider"];

  function config($qProvider, $stateProvider, $urlRouterProvider, $httpProvider) {};

  /* run block */
  run.$inject = ["$rootScope"];

  function run($rootScope) {}

})();

Module模块

为了更直观的体现 Angular模块化 的概念,会将如下代码新建为单独的 a.module.js 文件,主要用于模块的依赖声明,以及嵌套路由配置。其中,路由使用了 ui-router 提供的方案,父级路由使用 abstract 属性和 template:"<ui-view/>" 来实现与子路由的松耦合。

Angular module中的路由配置是整份前端代码的切割点,通过它完成了整个单页面应用在源码层面的文件切分。更重要的是,通过 controllerAs 的使用,在接下来的控制器中,通过 this 指针代替传统 $scope 的写法,有助于避免在有嵌套的controller中调用 $parent ,有效防止作用域污染的发生。

(function () {
  
  angular
    .module("app.template", []);

  angular
    .module("app.template")
    .config(templateConfig);

  templateConfig.$inject = ["$stateProvider"];

  function templateConfig($stateProvider) {
    $stateProvider
      .state("template", {
        parent: "layout",
        abstract: true,
        url: "/template",
        template: "<ui-view/>"
      })
      // Judged
      .state("template.judged", {
        parent: "template",
        url: "/judged",
        templateUrl: "partials/template/judge/view.html",
        controller: "TemplateJudgeController",
        controllerAs: "Judge"
      })
      // Trial
      .state("template.trial", {
        parent: "template",
        url: "/trial",
        templateUrl: "partials/template/trial/view.html",
        controller: "TemplateTrialController",
        controllerAs: "Trial"
      })
  }

})();

Controller控制器

控制器中,通过 $inject属性注解 手动实现依赖注入,避免代码压缩合并后出现依赖丢失的情况。将 this 指针赋值给 vm 对象(view model),其它方法和属性都以子对象的形式挂载到vm下面,使其更加整洁和面向对象。

由于Angular会自动绑定未在HTML中声明的属性,因此约定将所有双向绑定属性声明到vm对象当中,且通过赋予其默认值来表达所属数据类型。

下面代码中的 activate() 函数主要用来放置controller的初始化逻辑。

(function () {

  angular
    .module("app.login")
    .controller("LoginController", LoginController);

  LoginController.$inject = ["loginService", "$state", "verify", "$uibModal", "$scope"];

  function LoginController(loginService, $state, verify, $uibModal, $scope) {
    var vm = this;

    // initialization
    function activate() {};

    // two-way data binding on view
    vm.account = {
      username: "",
      password: "",
      message: "",
      onSubmit: function () {
        loginService.auth({
            username: vm.account.username,
            password: vm.account.password
          })
          .then(function (result) {
            if (loginService.validate(result)) {
              vm.account.message = result.message;
            }
          })
      }
    };

    // initialization function invoked
    activate();
  };

})();

Service服务

主要用来放置数据操作和数据交互的逻辑,例如: 负责XHR请求、本地存储、内存存储和其它任何数据操作 。最后,Service通过返回一个对象来组织这些服务。通常情况下,项目每个模块只拥有一个controller,但是可以存在多个service,Angular的设计理念就是寄希望通过service来完成业务的复用,这一点主要继承了传统MVC的分层思想。

Angular的service总是单例的,这意味每个 injector 都只有一个实例化的 service

(function () {

  angular
    .module("app.login")
    .service("loginService", loginService);

  loginService.$inject = ["$http", "URL"];

  function loginService($http, URL) {
    var path = URL.master;
    return {
      auth: function (data) {
        return $http.post(
            path + "/login", data
          )
          .catch(function (error) {
            console.error(error);
          })
      },
      validate: function () {
        // it is a login validator
      }
    }
  };

})();

Directive指令

指令的命名需要使用一个短小、唯一、具有描述性的前缀( 比例企业名称 ),可以通过在指令配置对象中使用 controllerAs 属性,取得与控制器中vm同样的用法,

使用controllerAs属性时,如果需要把父级作用域绑定到指令controller属性指定的作用域时,可以使用 bindToController=true

(function () {

  /**
   * @type directive
   * @name uinika-scroll
   * @param uinika-scroll-offset
   * @description auto resize base on overflow-y
   */
  angular.module("app.common")
    .directive("uinikaScroll", uinikaScroll);

  uinikaScroll.$inject = ["$window", "$document"];

  function uinikaScroll($window, $document) {
    return {
      restrict: "ACE",
      scope: {
        offset: "@uinikaScrollOffset"
      },
      link: function link(scope, element, attrs) {
        var _window = $($window);
        var _document = $($document);
        var _offset = $window.parseInt(scope.offset);

        format();

        _window.resize(function () {
          format();
        })

        function format() {
          // scroll
          element.css({
            "overflow-y": "scroll"
          });
          // size
          var contentHeight = _window.height() || 0;
          var navbarHeight = _document.find(".main-header").outerHeight() || 0;
          var boxheaderHeight = _document.find(".box-header").outerHeight() || 0;
          // calculate
          element.outerHeight((contentHeight - navbarHeight - boxheaderHeight) - _offset);
        }
      }
    };
  };

  /**
   * @type directive
   * @name slim-scroll
   * @param slim-scroll-offset slim-scroll-width slim-scroll-color slim-scroll-edge
   * @description auto resize base on slimscroll
   */
  angular.module("app.common")
    .directive("slimScroll", slimScroll);

  slimScroll.$inject = ["$window", "$document"];

  function slimScroll($window, $document) {
    return {
      restrict: "ACE",
      scope: {
        height: "@slimScrollOffset",
        size: "@slimScrollWidth",
        color: "@slimScrollColor",
        distance: "@slimScrollEdge"
      },
      link: function link(scope, element, attrs) {
        var _window = $($window);
        var _document = $($document);
        // property
        var _height = $window.parseInt(scope.height);
        var _size = scope.size ? (scope.size + "px") : ("6px");
        var _color = scope.color ? (scope.color + "") : ("red");
        var _distance = scope.distance ? (scope.distance + "px") : ("0px");

        format();

        _window.resize(function () {
          format();
        });

        function format() {
          // size
          var contentHeight = _window.height() || 0;
          var navbarHeight = _document.find(".main-header").outerHeight() || 0;
          var boxheaderHeight = _document.find(".box-header").outerHeight() || 0;
          // calculate
          element.slimScroll({
            height: ((contentHeight - navbarHeight - boxheaderHeight) - _height) + "px",
            color: _color,
            size: _size,
            distance: _distance,
            alwaysVisible: true
          });
        };

      }
    };
  };
})();

Filter过滤器

过滤输出给用户的表达式值,可用于 viewcontrollerservice

(function () {

  angular.module("app.common")
    .filter("trim", trim);

  trim.$inject = ["$sce"];

  function trim($sce) {
    return function (input) {
      if (typeof input === "string" && input)
        input.replace(/\s/g, "")
      return out;
    }
  };

})();

最佳实践是只通过filter筛选指定的对象属性,而非扫描对象本身,避免带来糟糕的性能问题。

JWT权限控制

为了适配移动端浏览器,采用JWT( JSON Web Token,一种JSON风格的轻量级的授权和身份认证规范 )作为前后端交互时的权限控制协议。主要是考虑前后端分离之后,在不借助cookie和session的场景下( 部分移动端浏览器未实现相关特性 ),使浏览器端发起的每个HTTP请求都能正确携带权限信息,以便于服务器端进行有效行拦截。

单页面场景下,权限认证需要关注如下3个核心问题:

  1. 登陆校验成功后,持有服务器端生成的token令牌。
  2. 每次HTTP请求都需要持有该token令牌的信息。
  3. 登陆超时和访问未授权页面的处理(通常为跳转)。

如下代码为项目入口点 config代码块 的配置,用于获取并放置token令牌,以及处理登陆超时的跳转。

config.$inject = ["$qProvider", "$stateProvider", "$urlRouterProvider", "$httpProvider"];

function config($qProvider, $stateProvider, $urlRouterProvider, $httpProvider) {
  $qProvider.errorOnUnhandledRejections(false);
  $urlRouterProvider.otherwise("/login");
  $stateProvider
    .state("login", {
      url: "/login",
      templateUrl: "partials/login/view.html",
      controller: "LoginController",
      controllerAs: "Login"
    })
    .state("layout", {
      url: "/layout",
      templateUrl: "partials/layout/view.html",
      controller: "LayoutController",
      controllerAs: "Layout"
    })

  /** HTTP Interceptor */
  $httpProvider.interceptors.push(interceptor);
  interceptor.$inject = ["$q", "$location"];

  function interceptor($q, $location) {
    return {
      "request": function (config) {
        var token = sessionStorage.token;
        if (token) {
          config.headers = _.assign({}, {
            "Authorization": "uinika " + token
          }, config.headers)
        };
        return config;
      },
      "response": function (response) {
        $q.when(response, function (result) {
          if (response.data && response.data.head && typeof response.data === "object") {
            if (result.data.head.status === 202) {
              sessionStorage.message = "登录超时,请重新登录!";
              $location.url("/uinika");
            };
          };
        });
        return response;
      }
    };
  };

};

如下代码为项目入口点 run代码块 的配置,主要是处理未登陆访问授权页面的跳转。

run.$inject = ["$rootScope"];

function run($rootScope) {
  // router listener
  $rootScope.$on("$stateChangeStart",
    function (event, toState, toParams, fromState, fromParams) {
      if (toState.name !== "login") {
        if (!sessionStorage.token) {
          window.location.href = "/uinika";
        };
      };
    });
};

Module中的Run和Config

Run和Config分别是Aangular模块加载的2个生命周期:

  1. Config :首先执行的是Config阶段,该阶段发生在provider注册和配置的时候,主要用于连接并注册好所有数据源,因此 providerconstant 都可以注入到Config代码块中,但是其它不确定是否初始化完成的服务不能注入进来。
  2. run :其次开始进入Run阶段,该阶段发生在injector创建完成之后,主要用于启动应用,是Angular中最接近C语言main方法的概念。为了避免在模块启动之后再进行配置操作,所以只允许注入 servicevalueconstant

$location的配置

$location 服务是Angular对浏览器原生 window.location 的封装,可以通过 $locationProvider 进行配置。

$locationProvider.html5Mode(true).hashPrefix("*");
  • Hashbang模式http://localhost:5008/#!/login?user=hank
  • HTML5模式http://localhost:5008/login?user=hank

Hashbang是指由 #! 构成的字符串,类UNIX系统会将Hashbang后内容作为解释器指令进行解析。

跨控制器的事件交互

当Angular当中同一张页面存在多个控制器时( 例如使用了嵌套路由 ),可以通过scope的事件机制进行通信。

  • $scope.$on(name, listener); 监听给定类型的事件。
  • $scope.$emit(name, args); 派发事件,可以携带参数。
  • $scope.$broadcast(name, args); 派发事件,可以携带参数。
// 事件广播
$scope.$broadcast("SEARCH", vm.search.input);

// 监听事件
function activate() {
  $scope.$on("SEARCH", function (event, data) {
    vm.menu.fetch();
  });
};

事件相关的处理( $on() 方法 ),可以统一写在每个控制器的初始化方法 activate() 当中。

Angular的HTML模板编译步骤

Angular中HTML模板的编译会经历下面3个步骤:

  1. $compile遍历DOM查找匹配的Angular指令。

  2. 当DOM上的所有指令被识别, $compile 会按其 priority 属性的优先级进行排序,接下来指令的 compile 函数被执行( 每个compile函数都拥有1次修改DOM的机会 ),每一条指令的 compile 函数都将返回一个 link 函数,这些函数最后会被合并到一个统一的链接函数当中。

  3. $compile 通过这个被合并的链接函数,依次调用每个指令的 link 函数,注册监听器到HTML元素,以及在 scope 中设置 $watch ,最后完成 scopetemplate 的双向绑定。

Angular 1.6.x最佳实践总结

var $compile = ...; // injected into your code
var scope = ...;
var parent = ...; // DOM element where the compiled template can be appended

var html = "<div ng-bind="exp"></div>";

// Step 1: parse HTML into DOM element
var template = angular.element(html);

// Step 2: compile the template
var linkFn = $compile(template);

// Step 3: link the compiled template with the scope.
var element = linkFn(scope);

// Step 4: Append to DOM (optional)
parent.appendChild(element);

上面是Angular官方文档中非常具有信息量的一段demo代码。

指令complie()和link()的区别

将compile和link分为两个阶段主要是出于性能的考虑,让多次Model的变化只引发一次DOM结构的改变。

  • compile() :对HTML模板自身进行转换,仅在编译阶段执行一次。
function compile(tElement, tAttrs, transclude) {
  return {
    pre: function preLink(scope, iElement, iAttrs, controller) { ... },
    post: function postLink(scope, iElement, iAttrs, controller) { ... }
  }
  // or
  return function postLink( ... ) { ... }
}
  • link() : 在View和Model之间进行动态关联,将会被执行多次,只能在未定义 compile 的场景下使用。
function link(scope, iElement, iAttrs, controller, transcludeFn) {
  link: {
    pre: function preLink(scope, iElement, iAttrs, controller) { ... },
    post: function postLink(scope, iElement, iAttrs, controller) { ... }
  }
  // or
  link: function postLink( ... ) { ... }
}

$digest循环

Angular增强了浏览器原生的事件循环( event loop )机制,将事件循环分为 JavaScript原生Angular可执行上下文 两部分,只有进入Angular可执行上下文才能够使用双向数据绑定、异常处理、属性观察等特性。

可以在JavaScript中通过 $apply() 方法进入到Angular可执行上下文,在大多数 controllerservice 当中 $apply() 已经被隐式的调用,只有在整合第3方类库需要自定义事件时才会显式使用 $apply()

Angular 1.6.x最佳实践总结

  1. 调用 scope.$apply(stimulusFn) 进入Angular可执行上下文,作为参数的stimulusFn函数就是需要运行在Angular可执行上下文内的代码。
  2. Angular执行stimulusFn()函数,该函数通常会对应用状态进行修改。
  3. Angular进入$digest循环,$digest循环又分为$evalAsync队列和$watch列表2个较小的循环。$digest循环会迭代执行直到Model状态稳定下来,即$evalAsync队列为空且$watch列表中检测不到任何变化的时候。
  4. $evalAsync队列主要用来异步处理Angular执行上下文之外的任务( 例如基于当前scope对表达式进行异步渲染 ),这一过程将会发生在浏览器视图渲染之前,从而避免视图闪烁。
  5. $watch列表是一个在最后一次迭代之后,依然可能发生变化的表达式集合。一旦检测到变化发生, $watch() 函数将会被调用,并使用改变后的值对DOM进行更新。
  6. 当$digest循环结束后,执行流程离开Angular和JavaScript上下文。

综上所述,$digest循环作为Angular的脏值检查机制,繁重的实现过程造成其性能开销较大。因此,视图层绑定数据时,最佳实践原则是尽量使用 :: 一次性绑定,以便减少不必要的$digest循环。

<span>{{::username}}</span>

如何理解Provider

Provider用于创建可以由injector依赖注入的服务,Provider需要通过auto模块中的$provide服务进行创建,Provider拥有 provider()value()factory()service()constant()decorator() 六种创建方式。

Angular 1.6.x最佳实践总结

  1. provider(name, provider) 该方式必须实现一个 $get 方法,是其它Provide创建方式的核心( 不包括Constant )。
  2. constant(name, obj) 定义常量,可以被注入到任何地方,但是不能被decorator装饰,也不能被注入其它service。
  3. value(name, obj) 可以是任意数据类型,不能被注入到config,但可以被decorator装饰。
  4. factory(name, fn) 创建可注入的普通函数,可以return任意值,实质是只拥有$get方法的provider。
  5. service(name, Fn) 创建可注入的构造函数,调用时会通过 new 关键字,可以不用return任何值,它在AngularJS中是单例的。
  6. decorator(name, decorFn) 用来装饰其他provider,可以中断服务的创建流程,然后重写或者修改服务的行为。但Constant不能被装饰,因为Constant并非provider()创建。
(function () {

  var module = angular.module("app.test", [])
    .config(testConfig)
    .controller("TestController", TestController);

  /* Config */
  testConfig.$inject = ["$provide"];

  function testConfig($provide) {

    // 模块上定义provider
    var provider = $provide.provider("message", function () {
      var message;
      return {
        setMessage: function (infomation) {
          message = infomation;
        },
        $get: function () {
          return {
            infomation: "This is a " + message
          }
        }
      }
    });
    provider.setMessage("Provider!"); // 设置Provider需要返回的消息

    // config中定义factory
    $provide.factory("factory", function () {
      return {
        reminder: "This is a Factory!"
      }
    });
    // config中定义service
    $provide.service("service", function () {
      this.warning = "This is a Service!";
    });

    // config中定义constant
    $provide.constant("CONSTANT", "This is a Constant!");
    // config中定义value
    $provide.value("value", "This is a Value!");

    // config上定义decorator
    $provide.decorator("value", decorator);
    decorator.$inject = ["$delegate"]; // $delegate是需要被修饰服务的实例,即上面语句中声明的value
    function decorator($delegate) {
      return $delegate + " with Decorator!!"; // 对value进行修饰
    };

  };

  /* Controller */
  TestController.$inject = ["message", "CONSTANT", "value", "factory", "service"];

  function TestController(message, CONSTANT, value, factory, service) {
    // controller当中调用各个provider
    console.info(message.infomation);
    console.info(CONSTANT);
    console.info(value);
    console.info(factory.reminder);
    console.info(service.warning);
  };

  /**
   * This is a Provider!
   * This is a Constant!
   * This is a Value! with Decorator!!
   * This is a Factory!
   * This is a Service!
   */

})();

除了在config当中通过 $provide 定义provider之外,还可以通过module上提供的语法糖方法更加方便的建立provider,出于代码松耦合以及分块编写的考虑,这里推荐使用语法糖的方式去提供provider。

(function () {

  var module = angular.module("app.test", [])
    .config(testConfig)
    .controller("TestController", TestController);

  testConfig.$inject = ["$provide", "messageProvider"];

  function testConfig($provide, messageProvider) {
    messageProvider.setMessage("Provider!"); // provider可以被注入到config中
  };

  // module上定义provider
  module.provider("message", function () {
    var message;
    return {
      setMessage: function (infomation) {
        message = infomation;
      },
      $get: function () {
        return {
          infomation: "This is a " + message
        }
      }
    }
  });

  // module上定义factory
  module.factory("factory", function () {
    return {
      reminder: "This is a Factory!"
    }
  });

  // module上定义service
  module.service("service", function () {
    this.warning = "This is a Service!";
  });

  module.constant("CONSTANT", "This is a Constant!"); // module上定义constant
  module.value("value", "This is a Value!"); // module上定义value

  // module上定义decorator
  module.decorator("value", decorator);
  decorator.$inject = ["$delegate"];
  function decorator($delegate) {
    return $delegate + " with Decorator!!";
  };

  TestController.$inject = ["message", "CONSTANT", "value", "factory", "service"];

  function TestController(message, CONSTANT, value, factory, service) {
    // controller当中调用各个provider
    console.info(message.infomation);
    console.info(CONSTANT);
    console.info(value);
    console.info(factory.reminder);
    console.info(service.warning);
  };

  /**
   * This is a Provider!
   * This is a Constant!
   * This is a Value! with Decorator!!
   * This is a Factory!
   * This is a Service!
   */

})();

Angular当中的$q

一个promises/deferred对象的Promises/A+兼容实现,受到 Kris Kowal”s Q 启发但并未实现全部功能,$q能够以如下2种流行的方式进行使用。

  1. Kris Kowal”s Q或者jQuery的Deferred对象:首先通过 $q.defer() 创建一个deferred对象,然后通过deferred对象的promise属性转换为Promise对象。
function asyncGreet(name) {
  var deferred = $q.defer();

  setTimeout(function() {
    deferred.notify("About to greet " + name + ".");
    if (okToGreet(name)) {
      deferred.resolve("Hello, " + name + "!");
    } else {
      deferred.reject("Greeting " + name + " is not allowed.");
    }
  }, 1000);

  return deferred.promise;
}
  1. 类似ES6原生Promise的方式:$q作为构造函数,接收 resolver() 函数作为第1个参数, $q(resolver) 将会返回一个新建的Promise对象。
$q(function(resolve, reject) {
  setTimeout(function() {
    if (okToGreet(name)) {
      resolve("Hello, " + name + "!");
    } else {
      reject("Greeting " + name + " is not allowed.");
    }
  }, 1000);
});

出于team逐步向ES6标准演进的考虑,这里推荐使用ES6原生风格的Promise写法。

不容忽视的$sce服务

$sce用于在Angular中提供严格的上下文转义( SCE, Strict Contextual Escaping )服务,从而避免XSS( 跨站脚本攻击 ), clickjacking( 点击劫持 )等安全性问题,$sce服务目前支持的上下文类型有5种。

(function () {

  angular.module("app.common")
    .filter("uinikaTrustHtml", uinikaTrustHtml);

  uinikaTrustHtml.$inject = ["$sce"];

  function uinikaTrustHtml($sce) {
    return function (val) {
      return $sce.trustAsHtml(val);
    };
  };

})();

最佳实践原则:项目中所有用户可以自由编辑的位置,都需要通过 $sce 进行无害化处理。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Spark技术内幕

Spark技术内幕

张安站 / 机械工业出版社 / 2015-9-1

Spark是不断壮大的大数据分析解决方案家族中备受关注的新增成员。它不仅为分布式数据集的处理提供一个有效框架,而且以高效的方式处理分布式数据集。它支持实时处理、流处理和批处理,提供了AllinOne的统一解决方案,使得Spark极具竞争力。 本书以源码为基础,深入分析Spark内核的设计理念和架构实现,系统讲解各个核心模块的实现,为性能调优、二次开发和系统运维提供理论支持;本文最后以项目实战......一起来看看 《Spark技术内幕》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具