huguangju / wiki

A curated list of awesome lists of JavaScript, CSS and Nodejs on Github
74 stars 32 forks source link

[译] 探索Angular 1.5 之component() 方法 #11

Open huguangju opened 8 years ago

huguangju commented 8 years ago

Angular 1.5 引入了 .component() 辅助方法, 它的定义比 .directive() 更简单。 .component() 允许开发者以更接近Angular2的方式写Angular1的代码,方便以后无痛地升级到Angular2。

.component().directive() 使用了更加简洁抽象的语法。

.directive().component()

以下是它们的语法差异:

// before
module.directive(name, fn);

// after
module.component(name, options);

先用Angular 1.4.x 写一个简单的计数器,下边再用 v1.5.0 重构它。

.directive('counter', function counter() {
  return {
    scope: {},
    bindToController: {
      count: '='
    },
    controller: function () {
      function increment() {
        this.count++;
      }
      function decrement() {
        this.count--;
      }
      this.increment = increment;
      this.decrement = decrement;
    },
    controllerAs: 'counter',
    template: [
      '<div class="todo">',
        '<input type="text" ng-model="counter.count">',
        '<button type="button" ng-click="counter.decrement();">-</button>',
        '<button type="button" ng-click="counter.increment();">+</button>',
      '</div>'
    ].join('')
  };
});

jsfiddle : https://jsfiddle.net/toddmotto/avdezer7/

方法名改变,并且Function 参数变为 Object

开始自上而下重构这个示例:

// before
.directive('counter', function counter() {
  return {

  };
});

// after
.component('counter', {

});

.directive 中本质上需要返回一个函数,而 .component 只需要传一个对象了

scopebindToController 变为 bindings

// before
.directive('counter', function counter() {
  return {
    // scope 用于创建独立作用域或继承父级作用域
    // 由于这个选项用来创建独立作用域基本上是不可或缺的,所以每次都要写就很繁琐了
    scope: {},
    // bindToController 可以直接定义那些想传入独立作用域的属性,并把它们绑定到controller上
    bindToController: {
      count: '='
    }
  };
});

// after
.component('counter', {
  // 用bindings可以简单地定义要传递哪些属性到component中,且并component拥有独立作用域
  bindings: {
    count: '='
  }
});

ControllercontrollerAs 的变化

在定义controller的方式上倒没有什么变化,唯一一点不同就是 controllerAs 多了一个默认值: $ctrl

在1.4中直接定义 controller

{
  ...
  controller: function () {}
  ...
}

或者在别的地方定义,此处引用

{
  ...
  controller: 'otherCtrl'
  ...
}

又或者用 controllerAs 起别名

{
  ...
  controller: 'otherCtrl'
  controllerAs: 'other'
  ...
}

然后就可以在模板中使用 other.prop之类来访问Controller实例。

.component() 中就不会那么麻烦了,在我们没明确指定Controller实例别名时,它会自动用3种方式创建 controllerAs 属性,Angular中相关源码如下:

controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',

其中第一个 identifierForController 会中controller属性为字符串时(controller: 'SomeCtrl as something'), 提取 as 后边的名字,源码如下:

var CNTRL_REG = /^(\S+)(\s+as\s+(\w+))?$/;
function identifierForController(controller, ident) {
  if (ident && isString(ident)) return ident;
  if (isString(controller)) {
    var match = CNTRL_REG.exec(controller);
    if (match) return match[3];
  }
}

第二个 controllerAs 用于当controller属性为function时的情况。

第三个 '$ctrl' 默认值让我们可以忽略掉 controllerAs

.component('test', {
  controller: function () {
    // 所以可以直接在模板中通过 $ctrl.testing访问
    this.testing = 123;
  }
});

说了那么多,终于可以在重构中把 controllerAs 干掉了:

// before
.directive('counter', function counter() {
  return {
    scope: {},
    bindToController: {
      count: '='
    },
    controller: function () {
      ...
    },
    controllerAs: 'counter'
  };
});

// after
.component('counter', {
  bindings: {
    count: '='
  },
  controller: function () {
    ...
  }
});

require 继承

{
  ...
  require: {
    parent: '^^parentComponent'
  },
  controller: function () {
    // 用 this.parent 访问依赖对象(在controller的parent属性上绑定)
    this.parent.foo();
  }
  ...
}

单向绑定

用新的表达式语法创建独立作用域:

{
  ...
  bindings: {
    oneWay: '<',
    twoWay: '='
  },
  ...
}

参见:One-way data-binding in Angular 1.5

Lifecycle hooks

Each component has a well-defined set of lifecycle hooks, read the full article here. 每个组件都预定义了一组 Lifecycle hooks(生命周期钩子):

参见:Comprehensive dive into Angular 1.5 lifecycle hooks

关闭独立作用域

Component 始终都会创建独立作用域,相关源码部分如下:

{
  ...
  scope: {},
  ...
}

无状态的components

参见:Stateless Angular components

基本上我们可以只用 templatebindings:

var NameComponent = {
  bindings: {
    name: '<',
    age: '<'
  },
  template: [
    '<div>',
      '<p>Name: </p>',
      '<p>Age: </p>',
    '</div>'
  ].join('')
};

angular
  .module('app', [])
  .component('nameComponent', NameComponent);

源码比较

https://github.com/angular/angular.js/blob/v1.5.7/src/ng/compile.js#L1112-L1165

  this.component = function registerComponent(name, options) {
    var controller = options.controller || function() {};

    function factory($injector) {
      function makeInjectable(fn) {
        if (isFunction(fn) || isArray(fn)) {
          return function(tElement, tAttrs) {
            return $injector.invoke(fn, this, {$element: tElement, $attrs: tAttrs});
          };
        } else {
          return fn;
        }
      }

      var template = (!options.template && !options.templateUrl ? '' : options.template);
      var ddo = {
        controller: controller,
        controllerAs: identifierForController(options.controller) || options.controllerAs || '$ctrl',
        template: makeInjectable(template),
        templateUrl: makeInjectable(options.templateUrl),
        transclude: options.transclude,
        scope: {},
        bindToController: options.bindings || {},
        restrict: 'E',
        require: options.require
      };

      // Copy annotations (starting with $) over to the DDO
      forEach(options, function(val, key) {
        if (key.charAt(0) === '$') ddo[key] = val;
      });

      return ddo;
    }

    // TODO(pete) remove the following `forEach` before we release 1.6.0
    // The component-router@0.2.0 looks for the annotations on the controller constructor
    // Nothing in Angular looks for annotations on the factory function but we can't remove
    // it from 1.5.x yet.

    // Copy any annotation properties (starting with $) over to the factory and controller constructor functions
    // These could be used by libraries such as the new component router
    forEach(options, function(val, key) {
      if (key.charAt(0) === '$') {
        factory[key] = val;
        // Don't try to copy over annotations to named controller
        if (isFunction(controller)) controller[key] = val;
      }
    });

    factory.$inject = ['$injector'];

    return this.directive(name, factory);
  };

升级到Angular 2

用这种方式写组件很容易地升级到Angular 2。用 ECMAScript 5 和新模板语法写的示例:

var Counter = ng
.Component({
  selector: 'counter',
  template: [
    '<div class="todo">',
      '<input type="text" [(ng-model)]="count">',
      '<button type="button" (click)="decrement();">-</button>',
      '<button type="button" (click)="increment();">+</button>',
    '</div>'
  ].join('')
})
.Class({
  constructor: function () {
    this.count = 0;
  },
  increment: function () {
    this.count++;
  },
  decrement: function () {
    this.count--;
  }
});

原文 https://toddmotto.com/exploring-the-angular-1-5-component-method/

usernameisMan commented 7 years ago

不错不错 写得不错!

freewind commented 6 years ago

在标题上写明是“翻译”可能会更好一些