前言
前端全栈方向,避不开要学习一个成熟的后端框架, nodejs 属于基建,公司一般都会采用一些上层框架来进行开发。比较热门的有 express
、Nestjs
、Meteor
、Koajs
、fastify
。
众多框架中,Nestjs 排行第二,也是 star 增长最快的 TypeScript 的后端框架。也就意味着,如果你需要使用一个 Typescript 后端框架进行开发,Nestjs 当上不二之选。
Nestjs
中使用最多就是依赖注入
技术,如果还不了解这一实现原理,学习起来会非常痛苦,该技术早就 Angular
中已有广泛应用,写法类似 Java Spring。
现阶段函数式编程颇为流行,
Vue3
的 Composition api 以及React
的 Hooks ,一度使我以为函数式编程才是前端开发的趋势。可看完NestJs
的使用面向对象装饰器,突然让我眼前一亮。
下面是 NestJs 一个简单的使用例子:
@Controller('cats')
export class CatsController {
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
接下来我将用非常通俗的例子来解析其实现原理。
概述
看完本文你会学到什么?
-
了解什么是控制反转 & 依赖注入。
-
通俗易懂的例子带你学会实现依赖注入。
-
了解什么是
Reflect Metadat
以及如何使用。
接下来我将分别讲述 控制反转/依赖注入
、IOC Container
、使用装饰器实现依赖注入
、Reflect Metadat
,一步步带你了解其原理和实现。
控制反转 LOC & 依赖注入 DI
首先解释一下什么是控制反转和依赖注入,控制反转是一种设计模式
,目的是解耦
。而依赖注入是控制反转的一种技术手段。
下面用一个通俗的例子来具体说明:
员工小明每天都需要利用电脑完成工作,写代码,发邮件等。他现在有一台 Win 笔记本电脑。
class User {
constructor (private name: string) {}
work() {
const win = new WinComputer()
win.open()
}
}
class WinComputer {
open () {}
}
const user = new User('xiaomin')
user.work()
有一天小明 Win 电脑坏了,需要换成 Mac 电脑
```ts
class User {
constructor (private name: string) {}
work() {
const win = new MacComputer()
win.open()
}
}
class MacComputer {
open () {}
}
const user = new User('xiaomin')
user.work()
上面的改造虽然是解决问题,但是每次切换电脑都要对原始类进行修改,极其不方便。
从上面可以看出,用户在自己的类中控制着电脑,换电脑就必须重构代码。
这个时候我们就会想到将电脑抽象出来,让用户类依赖的是一个抽象类的实例,而非一个具体实例。
class User {
constructor (private name: string, private computer: ) {}
work() {
this.computer.open()
}
}
abstract class Computer {
open () {}
}
class WinComputer extends Computer{
open () {
console.log('win start')
}
}
class MacComputer extends Computer{
open () {
console.log('mac start')
}
}
const user = new User('xiaomin', new WinComputer())
// if Win Error
// const user = new User('xiaomin', new MacComputer())
电脑类是抽离出来了,可是每天我们到公司还是需要先打开 Win
电脑,看看能不能开机,不能开机再切换 Mac
电脑... 可能以后还会有 Linux
电脑,还是还不方便呀。就不能一到工位就有人帮我检查好哪台电脑是好的?
当然可以,来了一个网管员,天天帮你管理电脑,然后每天早上检查你的哪台电脑能开机,坏的就送去维修。你到公司了只需要安心工作就可以了,其他电脑的事情完全不用你管。
class User {
constructor (private name: string, private computer: Computer) {}
work() {
this.computer.open()
}
}
class Admin () {
getComputed () {
const win = new WinComputer()
if (win.open) {
return win
} else {
return new MacComputer()
}
}
}
const admin = new Admin()
const user = new User('xiaomi', admin.getComputed())
// 再也不用自己管理电脑,work 执行失败就找 Admin
user.work()
这就是控制反转
,把你的控制电脑的权限转交给其他人,让别人替你去管理。上面 User
类将电脑的管理权转交给 Admin
,通过 admin.getComputed()
去获取电脑。
const user = new User('xiaomi', admin.getComputed())
而 User
依赖一个 Computer
实例,必须在 constructor
注入依赖才能正常工作,而我们将 admin.getComputed()
作为依赖项注入到 User 中,这个过程就是 依赖注入
。
简单来说,依赖注入就是将一个对象所依赖的其他对象通过构造函数、方法参数或者属性注入到该对象中,而不是在该对象内部直接创建这些依赖对象。这样做的好处是,我们可以更容易地替换依赖对象,从而实现代码的可测试性和可扩展性。
什么是IOC Container?
上面我们实现了将Admin
获取的电脑实例注入User
类中。
有没有一个通用
的容器,把上面的注入过程封装起来,自动将任意服务提供者注入到控制器中?
上面的 User
类相当于 控制器(controller)
上面的 Admin
类相当于 服务提供者(service)
function container (controller, service) {
// 这里面是将 service 自动注入到 controller 中
}
所以实现IOC的组件或者框架,我们可以叫它IOC Container。
使用装饰器实现依赖注入
如果你还不了解装饰器,可以先暂停,先看看装饰器相关的文档。
上面我们并没有真正去实现一个通用的 依赖注入
容器,下面我将用装饰器去简单实现一个 LOC
。
初步设想如下:
-
使用
InjectTable
装饰器标记的服务,收集到一个公共容器中。 -
使用
Inject
装饰器将服务注入到目标类中。
简单框架:
@InjecTable('admin')
class Admin {
open () {}
}
@Inject('admin')
class User () {
work () {
this.admin.open()
}
}
const user = new User()
user.work()
我们要实现的原理其实很简单:InjectTable
负责收集服务,Inject
负责将收集到的服务注入到目标类中。首先我们将 InjectTable
标记的类都统一存在一个 容器里,需要的时候取出来放入到目标类的成员中,如:User['admin'] = new (容器.Admin)
, 这样就完成了将服务实例挂载到目标类的过程。
因为
User
类中的成员 admin 上有装饰器@inject()
,在 User 定义的时候就会执行装饰器方法,将 Admin 的实例挂载在User
类中。
const map = new Map()
function InjecTable (key) {
// target 类
return function (target, propertyKey, descriptor) {
map.set(key, target)
}
}
function Inject (key) {
return function (target, propertyKey, descriptor) {
const item = map.get(key)
target[key] = new item()
}
}
@InjecTable('admin')
class Admin {
open () {}
}
@Inject('admin')
class User () {
work () {
this.admin.open()
}
}
const user = new User()
user.work()
上面 Inject
相当于把 Admin 实例挂载在了 User 的原型上。 User 类上就多了一个 admin 的成员属性,这样,work 方法就能直接调用 this.admin.open()
,使用 Admin 的能力。
不过你有没有发现一个问题,就是上面的实现中,我们必须要指定一个注入的 key,用这个 key 去 Map 里取具体的服务类,还会用 key 作为注入成员属性的 name。有没有办法我在类里定义一个成员,就可以根据属性类型自动注入?如下:
class User () {
@Inject()
admin: Admin
work () {
this.admin.open()
}
}
这里我们指定了 admin 成员的接口类型为一个 Admin。是不是可以直接通过这个类型,再去 Map 里找,找到了再注入进来?
问题是我们要如何获取 User 类中 admin: Admin
的类型?可以使用 Reflect Metadata 来获取。
Reflect.getMetadata('design:type', User, 'admin') // Admin
Reflect Metadat
什么是 Reflect Metadat ?Reflect Metadata 是 ES7 的一个提案,它主要用来在声明的时候添加和读取元数据。
相关 API 可以先去了解 GitHub - rbuckton/reflect-metadata: Prototype for a Metadata Reflection API for ECMAScript
示例:
@Reflect.metadata('key1', '这是类元数据')
class Test {
@Reflect.metadata('key2', '这是方法元数据')
public hello(): string {
return 'hello world';
}
}
console.log(Reflect.getMetadata('key1', Test)); // '这是类元数据'
console.log(Reflect.getMetadata('key2', new Test(), 'hello')); // '是方法元数据'
简单来说就是通过反射向类、属性、方法添加附加信息
。如上,我们将 Test 类上添加了一个“字符串”
,同时通过 Reflect.getMetadata
我们可以从外部取出我们定义在类上的“字符串”
,这里的 “字符串”
可以是任意值,也可以是对象。
上面我们分别在类和方法上定义了两个元数据:
- key1 --- '这是类元数据'
- key2 --- '这是方法元数据'
我们通过自定义的 key 获取自定义的元数据信息
,其实还有三个内置的 key 可以获取元数据。
- design:type --- 获取属性类型
- design:paramtypes --- 获取方法参数类型
- design:returntype --- 获取方法返回类型
class User () {
@Inject()
admin: Admin
work (time: Time): Boolean {
this.admin.open()
}
}
console.log(Reflect.getMetadata('design:type', User, 'admin'));
// Admin
console.log(Reflect.getMetadata('design:paramtypes', User, 'work'));
// [Time]
console.log(Reflect.getMetadata('design:returntype', User, 'work'));
// Boolean
Ok, 理解完 Reflect Metadata 是什么后,我们继续回到上面的依赖注入实现, 现在可以通过
Reflect Metadata 拿到成员类型后就好办了。
完整代码如下:
@InjecTable('admin')
class Admin {
open () {}
}
class User () {
@Inject()
admin: Admin
work () {
this.admin.open()
}
}
const user = new User()
user.work()
// ----------------------- 装饰器 ----------------------------
const SERVER_CONTAINER = 'servers'
function InjecTable (key) {
// target 类
return function (target, propertyKey, descriptor) {
// map.set(key, target)
Reflect.defineMetadata(
SERVER_CONTAINER, // meta key
key, // meta value
target, // meta Class
propertyKey // meta Class attr
)
}
}
function Inject (key) {
return function (target, propertyKey, descriptor) {
//const item = map.get(key)
// target[key] = new item()
const type = Reflect.getMetadata('design:type', target, propertyKey)
target[propertyKey] = new type()
}
}
同样的原理,我们也可以通过构造函数参数上注入 利用:Reflect.getMetadata('design:paramtypes', User.constructor)
class User () {
constructor (@Inject() private admin: Admin) {
}
work () {
this.admin.open()
}
}
总结
上面我们首先介绍了控制反转和依赖注入,再介绍了什么是 LOC 容器,然后再使用装饰器实现了一个简单的 LOC 容器。中间利用了 Reflect Metadata 来实现获取类型。最终实现了完整的依赖注入流程。
当然,上面的实现很简单,存在很多问题,例如不能注入多个服务。每个服务注入时都还 new 一个实例出来,没有使用单例模式,缓存实例等等。