Angular DI 是怎么工作的?
栏目: JavaScript · 发布时间: 5年前
内容简介:这又是一篇短命的(short-lived)描述当下技术框架的博客文章。 DI(依赖注入、Dependency injection)是一种设计模式,常用来提升代码的可复用性、健壮性和可测试性。 DI 在前端的流行很大程度上归功于它在 Angular1.x 中的应用。 Angular 2 以后借助 TypeScript 的类型分析,可以干脆省去了 Angular1.x 中冗余的依赖声明。 本文用来解释这个魔法是怎么工作的,以及相关的标准化 Proposal 和实现 Trick。对于不熟悉依赖注入的同学可以参考我
这又是一篇短命的(short-lived)描述当下技术框架的博客文章。 DI(依赖注入、Dependency injection)是一种设计模式,常用来提升代码的可复用性、健壮性和可测试性。 DI 在前端的流行很大程度上归功于它在 Angular1.x 中的应用。 Angular 2 以后借助 TypeScript 的类型分析,可以干脆省去了 Angular1.x 中冗余的依赖声明。 本文用来解释这个魔法是怎么工作的,以及相关的标准化 Proposal 和实现 Trick。
对于不熟悉依赖注入的同学可以参考我的另外两篇文章 什么时候应该使用依赖注入 和 JavaScript 依赖注入实现 , 或者 Wikipedia: DI 。这里还有一篇 Angular 的教程: https://angular.io/guide/dependency-injection
一个例子
Angular1.x
为了方便说明,我们先给一个 Angular1 Dependency Injection 的例子:
angular.module('bar').controller('Bar', function (Foo) {})
在使用上述组件(Bar)时,Angular DI 会创建一个 Foo 并用来初始化 Bar 的实例。
Angular 使用 Function.prototype.toString
来获取控制器 Bar 的参数需要哪些类型。
因此如果上述代码经过 uglify 之后依赖注入会不起作用。因此有另一种写法:
// 因为字符串字面量不会被 uglify,相当于通过字符串字面量来声明依赖 angular.module('bar').controller('Bar', ['Foo', function (Foo) {}])
Angular with TypeScript
再看 TypeScript 下的 Angular2 代码,省去了这个字面量的声明:
import { Foo } from './foo' @Component({...}) class Bar { constructor(private foo: Foo) {} }
上述 TypeScript 代码中 Bar 只依赖 Foo 的类型, 这意味着 TypeScript 编译上述代码得到的文件中没有对 Foo 的引用。 这里的魔法在于:Angular DI 是怎么在运行时知道 Bar 的第一个参数类型是 Foo?
装饰器
装饰器是一种用来提供面向切面编程功能的一种语法构造。 目前是一个 Stage2 Proposal ,还没有进入 ECMA 标准。
在 TypeScript 可以打开 experimentalDecorators
选项来使用。
它的原理就是调用自定义的一个高阶函数,来操作被装饰的函数。
可以参考 TypeScript 教程: https://www.typescriptlang.org/docs/handbook/decorators.html
。
比如上面的 Component 装饰器,它会在运行时对 Bar 做一个转换。比如这样:
var __decorator = function() {/*...*/} var Bar = function (foo) { this.foo = foo } Bar = __decorate([Component], Bar) function Component(Bar) { // 对 Bar 进行一些操作,甚至返回一个新的类替代原来的 Bar }
借助于装饰器的机制,我们就有机会在 Component 函数中标记 Bar 的依赖信息。
如果你是分模块编译的,可能需要避免重复输出 __decorator
函数的定义。
设置 importHelpers
编译器参数可以禁止它输出,
然后在运行时统一引入 tslib
。
当然如果你在使用 Angular,则应该引入 Angular Polyfill。
获取依赖信息
恰好 TypeScript 提供来一个 emitDecoratorMetadata
开关来产出编译时的类型信息给装饰器。
目前只提供三种类型信息:
- “design:type”
- “design:paramtypes”
- “design:returntype”。
参考这个 Issue https://github.com/Microsoft/TypeScript/issues/2577
。
打开 emitDecoratorMetadata
开关后编译后代码会多这样一部分内容:
var Bar = __decorate([ __metadata('design:paramtypes', [foo_1.Foo]) ], Bar);
可以看到它把 Bar 的参数类型 Foo 传给来 __metadata
这个 Helper。
这里和 Angular1.x 的不同还在于这里存的类型就是对函数 Foo 的引用,不是某种序列化后的字符串。
这意味着 Angular DI 不依赖于序列化后的字符串和被注入函数之间的对应关系;
但是也意味着这个类型只能是 class、string、object 这样的既存在于编译期也存在于运行时的类型。
比如 Interface 和 Type 这样的类型信息就无法记录,参考: https://github.com/microsoft/TypeScript/issues/3015#issuecomment-98852421
接下来的问题就是, __metadata
怎么存储这个类型信息 Foo
,最终怎么让 Angular DI 读取到。
由于 Angular DI 容器需要在运行时使用这个信息,这就需要 JavaScript 的反射机制。
Metadata Reflection API
反射 用来在运行时检查和操作对象,是 ES6 标准的一部分。 而 Reflect Metadata 提案 则是让 Reflect API 可以提供对类型的元数据进行操作的方法, 需要引入 Polyfill: reflect-metadata 。
元数据反射机制的灵感来源是 C#、 Java 之类的语言。
它们可以在源代码中通过一些标注来给类型设置元数据,并用反射 API 在运行时读取。
总之 Reflect 就是用来存这些元数据(类型信息)的地方。
下面看一下 __metadata
是怎样使用 Metadata Reflection API 的:
var __metadata = this && this.__metadata || function (k, v) { if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') return Reflect.metadata(k, v); };
可以看到 __metadata
Helper 把类型信息存到来 Reflect.metadata
API 中。
Angular DI 中获取类型信息
既然类型信息已经存入 Reflect API,Angular DI 就可以在创建 Bar 的时候拿到它的参数类型信息了。比如这样:
function create(Bar) { const types = Reflect.getMetadata('design:paramtypes', Bar) const args = types.map(Type => new Type()) return new Bar(...args) }
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 负载均衡 (一) 工作模式以及工作原理
- 学习,工作,养生利器 --- 番茄工作法的正确打开方式
- 性能大比拼-真实世界工作负载vs实验室综合工作负载
- [JWFD开源工作流]JWFD开源工作流-矩阵引擎设计初步
- 学完Python好找工作吗?为什么有人学完找不到工作?
- 学完Python好找工作吗?为什么有人学完找不到工作?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。