前言
最近作者有在用Nestjs开发nodejs程序。
nestjs是构建在Express(默认)或Fastify之上的nodejs框架,借助Typescript和decorator提供了AOP(面向切面)、DI(依赖注入)等编程范式。
今天就来介绍,如何通过Typescript的decorator来为Express实现简单的依赖注入。
创建项目
初始化项目:
// 安装typescript
npm install -g typescript
// 初始化项目
npm init -y
tsc --init
安装依赖:
yarn add express
yarn add @types/express -D
同时我们需要将tsconfig
中的experimentalDecorators
选项设置为true
,以使用装饰器:
{
"compilerOptions": {
"experimentalDecorators": true,
}
}
Controller装饰器
Express
的使用方法相信很多小伙伴都很熟悉:
const express = require('express')
const app = express()
const port = 3000
app.get('/main/hello', (req, res) => res.send('Hello World!'))
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
而在改造之后,希望得到的等效的写法是:
// index.ts
start({
controllers: [MainController],
port: 3000,
});
// controllers.ts
@Controller('main')
export class MainController {
@Get('hello')
hello(req, res) {
res.send('Hello World!');
}
}
设置元数据
为了设置与获取元数据,我们将安装reflect-metadata
库(关于reflect-metadata
大家可以参考语法提案和git仓库):
yarn add reflect-metadata -D
我们将借助其提供的Reflect.defineMetadata
和Reflect.getMetadata
来实现元数据的存取。
defineMetadata
和getMetadata
在类和方法的装饰器上使用时分别为以下形式:
Reflect.defineMetadata(key, value, target); // 类
Reflect.defineMetadata(key, value, target, propertyKey); // 方法
Reflect.getMetadata(key, target); // 类
Reflect.getMetadata(key, target, propertyKey); // 方法
于是,我们便可以实现Controller
和Get
装饰器,用来存储请求路径和http方法的元数据:
// framework.ts
export function Controller(rootPath: string = '') {
return (target) => {
Reflect.defineMetadata('ROOT_PATH', rootPath, target);
}
}
function getControllerDecorator(method) {
return (path: string = '') => {
return (target, propertyKey: string) => {
Reflect.defineMetadata('HTTP_METHOD', method, target, propertyKey);
Reflect.defineMetadata('PATH', path, target, propertyKey);
}
}
};
export const Get = getControllerDecorator('get');
获取元数据
之后,实现start
方法:
// framework.ts
import * as express from 'express';
export const app = express();
export function start({ controllers, port = 3000 }: IConfig) {
for (const cls of controllers) {
const rootPath = Reflect.getMetadata('ROOT_PATH', cls);
const instance = new cls();
for (const propertyKey of Object.getOwnPropertyNames(cls.prototype)) {
const method = Reflect.getMetadata('HTTP_METHOD', instance, propertyKey);
const path = Reflect.getMetadata('PATH', instance, propertyKey);
if (!method) continue;
app[method](`${rootPath}/${path}`, instance[propertyKey].bind(instance));
}
}
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
}
以上代码也比较容易理解。我们遍历controllers
数组,其中每一项为用Controller
装饰器装饰过的控制器类,从类中我们获取路径前缀,创建类的实例后,遍历每个实例的方法,如果方法被Get
、Post
之类的装饰器装饰过的话,就注册到Express
实例上。
依赖注入
以上我们实现了Controller
装饰器,不过还没有实现依赖注入。
我们希望实现依赖注入后,可以这样使用:
// controller.ts
@Controller('/math')
export class MathController {
constructor(
private readonly math: MathService,
) {}
@Get('add')
add(req, res) {
res.send({ result: this.math.add(1, 2) });
}
}
// service.ts
@Provider()
export class MathService {
add(a, b) {
return a + b;
}
}
显而易见的,在我们实现的start
方法中,需要修改new cls()
这部分的代码,以正确的注入依赖的实例。
那问题来了,我们怎么知道控制器依赖了哪些东西呢?
这下,就轮到emitDecoratorMetadata
出场了。修改tsconfig.json
,将emitDecoratorMetadata
设置为true:
{
"compilerOptions": {
"emitDecoratorMetadata": true,
}
}
先不急着实现功能,随便写个Provider
装饰器,先用tsc
命令编译一下,再编译出的controller.js
中,我们可以找到类似如下的代码:
MathController = __decorate([
framework_1.Controller('/math'),
__metadata("design:paramtypes", [service_1.MathService])
], MathController);
哇哦,ts为我们处理好了依赖列表,并存在了design:paramtypes
这个元数据内。
接下来,我们只要通过这个元数据获取类并生成实例就行了:
// framework.ts
const providerMap: WeakMap<any, Object> = new WeakMap();
export function Provider() {
return (target) => {
providerMap.set(target, null);
};
}
function getInstance(cls) {
const dependencies = (Reflect.getMetadata('design:paramtypes', cls) || []).map(cls => {
const instance = providerMap.get(cls);
if (instance === null) providerMap.set(cls, getInstance(cls));
return providerMap.get(cls);
});
return new cls(...dependencies);
}
export function start({ controllers, port = 3000 }: IConfig) {
for (const cls of controllers) {
const instance = getInstance(cls);
...
}
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
}
getInstance
方法用来处理依赖并生成实例。其内部递归的调用了getInstance
,这样一个Provider
可以去依赖其他Provider
。同时这里我们使用了一个类和实例的WeakMap
,这样每个Provider
都是单例模式。我们可以通过在Provider
装饰器中传入参数来改造这一行为,这里就不展开了。