首先,我们构造一个最简单的测试用例,仅仅是请求 /users
,并返回 ‘user list’
import express, { Express } from 'express';
import { Server } from 'node:http';
import supertest from 'supertest';
describe('ResourceServlet', () => {
let app: Express;
let server: Server;
beforeAll(() => {
app = express();
server = app.listen(3000);
app.get('/users', (req, res) => {
res.send('user list');
});
});
afterAll(() => {
server.close();
});
it('should return user list when fetch "users"', async () => {
const response = await supertest(app).get('/users');
expect(response.text).toBe('user list');
});
});
接着,我们吧获取 user list
的的逻辑,封装到 UserResource.findAll 中,这样子,我们的预想的架构愿景,就变成了,拿到 @Path 装饰的类,并根据路由找到请求方法。
import express, { Express } from 'express';
import { Server } from 'node:http';
import supertest from 'supertest';
import { Get, Path } from './decorators';
describe('ResourceServlet', () => {
let app: Express;
let server: Server;
beforeAll(() => {
app = express();
server = app.listen(3000);
app.get('/users', async (req, res) => {
const result = await new UserResource().findAll()
res.send(result);
});
});
afterAll(() => {
server.close();
});
it('should return user list when fetch "users"', async () => {
const response = await supertest(app).get('/users');
expect(response.text).toBe('user list');
});
});
@Path('/users')
class UserResource {
@Get()
async findAll() {
return 'user list';
}
}
添加 dispatch
import express, { Express, Request, Response } from 'express';
import { Server } from 'node:http';
import supertest from 'supertest';
import { Get, HTTP_METHOD_METADATA, Path, PATH_METADATA } from './decorators';
import { Class } from './core';
describe('ResourceServlet', () => {
let app: Express;
let server: Server;
beforeAll(() => {
app = express();
server = app.listen(3000);
app.get('/users', async (req, res) => {
const application = new TestApplication();
const servlet = new ResourceServlet(application);
await servlet.handle(req, res);
});
});
afterAll(() => {
server.close();
});
it('should return user list when fetch "users"', async () => {
const response = await supertest(app).get('/users');
expect(response.text).toBe('user list');
});
});
class ResourceServlet {
constructor(private readonly application: TestApplication) {
}
async handle(req: Request, res: Response) {
const rootResources = [...this.application.getClasses().values()].filter(c => {
return Reflect.hasMetadata(PATH_METADATA, c);
});
const result = await this.dispatch(req, rootResources);
res.send(result);
}
async dispatch(req: Request, rootResources: Class[]) {
const rootResource = rootResources[0];
const instance: any = new rootResource();
const methods = Object.getOwnPropertyNames(rootResource.prototype)
.filter(key => Reflect.hasMetadata(HTTP_METHOD_METADATA, instance, key))
.filter(key => {
const httpMethod = Reflect.getMetadata(HTTP_METHOD_METADATA, instance, key);
const endPath = Reflect.getMetadata(PATH_METADATA, instance, key);
return req.method === httpMethod && req.path.endsWith(endPath);
});
return await instance[methods[0]]();
}
}
class TestApplication {
getClasses(): Set<Class> {
return new Set([UserResource]);
}
}
@Path('/users')
class UserResource {
@Get()
async findAll() {
return 'user list';
}
}
现在我们的 res.send(result); 场景太单一了,我们现实的业务,会根据 Head 中不同的 media type,去向前端返回不同格式的数据。这里我们可以抽象一个 bodywriter
import express, { Express, Request, Response } from 'express';
import { Server } from 'node:http';
import supertest from 'supertest';
import { Get, HTTP_METHOD_METADATA, Path, PATH_METADATA, Provider, PROVIDER_METADATA } from './decorators';
import { BodyWriter, Class, OutResponse } from './core';
describe('ResourceServlet', () => {
let app: Express;
let server: Server;
beforeAll(() => {
app = express();
server = app.listen(3000);
app.get('/users', async (req, res) => {
const application = new TestApplication();
const servlet = new ResourceServlet(application);
await servlet.handle(req, res);
});
});
afterAll(() => {
server.close();
});
it('should return user list when fetch "users"', async () => {
const response = await supertest(app).get('/users');
expect(response.text).toBe('user list');
});
});
class ResourceServlet {
constructor(private readonly application: TestApplication) {
}
async handle(req: Request, res: Response) {
const rootResources = this.application.getClasses().filter(c => {
return Reflect.hasMetadata(PATH_METADATA, c);
});
const outResponse = await this.dispatch(req, rootResources);
const providers = new TestProviders(this.application);
const writer = providers.getMessageBodyWriter(outResponse.mediaType);
await writer.write(outResponse.result, res);
}
async dispatch(req: Request, rootResources: Class[]): Promise<OutResponse> {
const rootResource = rootResources[0];
const instance: any = new rootResource();
const methodPropertyKeys = Object.getOwnPropertyNames(rootResource.prototype)
.filter(key => Reflect.hasMetadata(HTTP_METHOD_METADATA, instance, key))
.filter(key => {
const httpMethod = Reflect.getMetadata(HTTP_METHOD_METADATA, instance, key);
const endPath = Reflect.getMetadata(PATH_METADATA, instance, key);
return req.method === httpMethod && req.path.endsWith(endPath);
});
const result = await instance[methodPropertyKeys[0]]();
return new OutResponse(result, 'string');
}
}
class TestProviders {
constructor(private readonly application: TestApplication) {
}
getMessageBodyWriter<T>(mediaType: string): BodyWriter<T> {
const writers = this.application.getClasses().filter(c => Reflect.hasMetadata(PROVIDER_METADATA, c)) as unknown as BodyWriter<any>[];
return writers.filter(writer => new writer().canWrite(mediaType)).map(writer => new writer())[0];
}
}
class TestApplication {
getClasses(): Class[] {
return [UserResource, StringMessageBodyWriter];
}
}
@Path('/users')
class UserResource {
@Get()
async findAll() {
return 'user list';
}
}
@Provider()
class StringMessageBodyWriter implements BodyWriter<string> {
canWrite(accept: string): boolean {
return accept === 'string';
}
async write(data: string, res: Response): Promise<void> {
res.send(data);
}
}
现在我们的代码中,有些没有必要的实例创建,比如 BodyWriter 全局不管怎样,我们都应该拿到同一个实例。自然地,我们可以借助依赖注入,进行全局实例的管理。
比如在 inversify js 的帮助下,获取 body writer 可以这么做
class TestProviders {
private readonly container = new Container();
private writers: BodyWriter<any>[] = [];
constructor(private readonly application: TestApplication) {
const writerClasses = this.application.getClasses().filter(c => Reflect.hasMetadata(PROVIDER_METADATA, c));
writerClasses.forEach(writerClass => {
this.container.bind(writerClass).toSelf().inSingletonScope();
this.writers.push(this.container.get(writerClass) as BodyWriter<any>);
});
}
getMessageBodyWriter<T>(mediaType: string): BodyWriter<T> {
return this.writers.filter(writer => writer.canWrite(mediaType))[0];
}
}