General idea:
@Controller("/user") // 注入 baseUrl: '/user'
class UserController {
@Post("/register") // 注入 POST: '/register'
public register() {}
@Get("/login") // 注入 GET: '/login'
public login() {}
}
now use Reflect to implement decorators Controller, Get, Post
Before that, you need to define constants.
because Reflect needs metadataKey to index metadata
export namespace KEY {
export const enum Controller {
BaseUrl = "controller:baseUrl",
GET = "method:get",
POST = "method:post"
}
}
ed to inject baseUrl metadata
export function Controller(path?: string): ClassDecorator {
return target =>
Reflector.defineMetadata(KEY.Controller.BaseUrl, path, target);
}
@ Post and @ Get have too much similar code, pull away a Factory method enter Method type and routing path, and return a MethodDecorator
export function DecoratorFactory(
type: KEY.Controller,
path: string
): MethodDecorator {
return (target, key) => Reflector.defineMetadata(type, path, target, key);
}
used to inject method type, routing path, and corresponding response method name (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);
}
Using the above decorator, the path, method, callbackName and other information needed by requestListener can be defined on Controller.
lve how to extract metadata from Controller and convert it to Controller-Metadata-Node First of all, make it clear that the input and the input type Enter a Class and return a Controller-Metadata-Node
// 伪代码
function parse(Controller: { new (): any }): Controller-Metadata-Node
Define 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;
}
Implement 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
};
}
Tell me what the resolve function did.
xiliary function of reduce, which is used to match method and 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)] });
}
Here you need to implement two util functions
mapMethodToKey is used for Pattern matching efixes used by pathToProp to process 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
convert Controller-Metadata-Node to requestListeners (Units) First of all, make it clear that the input and the input type Enter a Controller-Metadata-Node to return 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 does a little bit in this step. In fact, it should be directly converted to requestListeners.
+ mapUnitToJob
used to convert the units obtained by transform into requestListeners Koa is used here, so:
// 得到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();
}
});
}
Using Koa-compose, you can combine requestListeners sequences.
@Injectable()
export class ContextService {
public request: Context["request"];
public response: Context["response"];
}
You can get koa-context information as long as you inject ContextService For example
@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");
}
}
Now there are decorators, parser, and transformer that define metadata The last thing you need is a Factory class to organize these processes in an orderly manner.
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"));
Now just implemented from Controllers to requestListeners, about Service Injection shared in the next article.