总体思路:
- Controller 用来组织元数据
- parse 方法将 Controller 中的 metadata 提取出来,得到 Controller-Metadata-Node
- transform 方法将 Controller-Metadata-Node 转为 requestListeners
利用 Class 来组织元数据,利用装饰器和 Reflect 注入元数据信息
@Controller("/user") // 注入 baseUrl: '/user'
class UserController {
@Post("/register") // 注入 POST: '/register'
public register() {}
@Get("/login") // 注入 GET: '/login'
public login() {}
}
现在利用 Reflect 来实现装饰器 Controller、Get、Post
在此之前需要定义 constants
因为 Reflect 需要 metadataKey 来索引 metadata
export namespace KEY {
export const enum Controller {
BaseUrl = "controller:baseUrl",
GET = "method:get",
POST = "method:post"
}
}
实现装饰器部分
1. @Controller
用来注入 baseUrl 元数据
export function Controller(path?: string): ClassDecorator {
return target =>
Reflector.defineMetadata(KEY.Controller.BaseUrl, path, target);
}
+ DecoratorFactory
@Post 和@Get 相似代码太多,抽离一个 Factory 方法 输入 Method type 和路由 path,返回一个 MethodDecorator
export function DecoratorFactory(
type: KEY.Controller,
path: string
): MethodDecorator {
return (target, key) => Reflector.defineMetadata(type, path, target, key);
}
2. @Post、@Get
用来注入 method 类型、路由 path、对应的响应方法名(propertyKey)
export function Get(path?: string): MethodDecorator {
return DecoratorFactory(KEY.Controller.GET, path);
}
export function Post(path?: string): MethodDecorator {
return DecoratorFactory(KEY.Controller.POST, path);
}
利用上述装饰器可以将 requestListener 需要的 path、method、callbackName 等信息定义在 Controller 上
parse
解决如何提取出 Controller 中的 metadata 及其转化为 Controller-Metadata-Node
首先明确,输入和输入的 type
输入一个 Class,返回一个 Controller-Metadata-Node
// 伪代码
function parse(Controller: { new (): any }): Controller-Metadata-Node
定义 type
// GET和POST还有别的method懒得写了
export type Method = "GET" | "POST";
// 路由
export type Routes = Array<{
method: Method;
path: string;
callback: Function;
}>;
// Controller-Metadata-Node
export interface Controller {
baseUrl: string;
routes: Routes;
}
实现 parse
// parse函数实现
// 输入一个Class类型,输出Controller-Metadata-Node
export function parse(Controller: { new (): any }): Controller {
// 提取baseUrl
const baseUrl = Reflector.getMetadata<string>(
KEY.Controller.BaseUrl,
Controller
);
// 实例化
const target = new Controller();
// 获取实例的所有方法callbackNames
const methods = Object.keys(Object.getPrototypeOf(target));
// 遍历所有callbackNames
// 匹配对应的method并输出
const routes = methods.reduce((receiver, key) => {
resolve(target, key, "GET", receiver);
resolve(target, key, "POST", receiver);
return receiver;
}, []);
// 返回Controller-Metadata-Node
return {
baseUrl,
routes
};
}
说一下 resolve 函数做了什么
+ resolve
reduce 的辅助函数,用于匹配 method 与 metadata
export function resolve(
target: Object,
key: string,
method: Method,
receiver: Routes
) {
// 获取target.key上methodKey对应的metadata
const path = Reflector.getMetadata<string>(
mapMethodToKey(method),
target,
key
);
// 如果method对应的path存在,则往receiver中push一个route
if (path) receiver.push({ method, path, callback: target[pathToProp(path)] });
}
这里需要实现两个 util 函数
+ mapMethodToKey 、pathToProp
mapMethodToKey 用来进行 Pattern matching pathToProp 用来处理 path 的前缀
export function mapMethodToKey(method: Method): KEY.Controller {
switch (method) {
case "GET":
return KEY.Controller.GET;
case "POST":
return KEY.Controller.POST;
default:
throw new TypeError();
}
}
export function pathToProp(path: string) {
if (path.startsWith("/")) {
return path.slice(1);
}
return path;
}
transform
将 Controller-Metadata-Node 转为 requestListeners(Units)
首先明确,输入和输入的 type
输入一个 Controller-Metadata-Node 返回 Units
// requestListener需要的信息
export interface Unit {
url: string;
callback: Function;
method: string;
}
// 实现transform
export function transform(controller: Controller): Unit[] {
return controller.routes.map<Unit>(({ path, callback, method }) => ({
url: controller.baseUrl + path,
callback,
method
}));
}
transform 这一步做的有点少,其实应该直接转为 requestListeners
+ mapUnitToJob
用来把 transform 得到的 units 转为 requestListeners
这里用的是 Koa 所以:
// 得到requestListeners序列
export function mapUnitToJob(units: Unit[]): Job<Context>[] {
return units.map<Job>(unit => async (ctx, next) => {
const { url, method } = ctx.request;
if (url === unit.url && method === unit.method) {
// 混入ctx
await unit.callback.apply(
Object.assign(unit.origin, { ContextService: ctx })
);
} else {
await next();
}
});
}
利用 Koa-compose 就可以组合 requestListeners 序列
+ ContextService
@Injectable()
export class ContextService {
public request: Context["request"];
public response: Context["response"];
}
只要注入 ContextService 就可以获取 koa-context 信息
例如
@Controller("/user")
class UserController {
constructor(private ContextService: ContextService) {}
@Get("/login")
public login() {
this.ContextService.response.end("login");
}
@Get("/hello")
public hello() {
this.ContextService.response.end("hello");
}
}
现在有了定义 metadata 的 decorators、parser 和 transformer
最后需要的就是一个 Factory 类,将这几个过程有序组织起来
Factory
export class Factory {
public constructor(private modules: Array<{ new (): any }>) {
// 将classes转为units
this.units = [].concat(...this.modules.map(mod => transform(parse(mod))));
}
public instance: KoaBody;
private units: Unit[];
public create() {
// 将units转为requestListeners,实例化一个koa-app,koa.use...
this.instance = Koa().use(compose(...mapUnitToJob(this.units)));
// 返回koa实例
return this.instance;
}
}
demo
@Controller("/user")
class UserController {
constructor(private ContextService: ContextService) {}
@Get("/login")
public login() {
this.ContextService.response.end("login");
}
@Get("/hello")
public hello() {
this.ContextService.response.end("hello");
}
}
new Factory([UserController])
.create()
.listen(3001, () => console.log("http://localhost:3001"));
现在只是实现了从 Controllers 到 requestListeners,关于 Service 注入在下一篇文章分享。