直接使用 express, koa 这些开源框架的问题
- 路由划分不明确:一旦项目变大了之后,路由的代码会变得混乱且繁杂
- 代码复用性差
- 代码结构混乱,职责不明确
比如下面这一段使用 express 直接调用的代码:
import express, { Application, Router } from 'express';
const mainRouter: Router = Router();
mainRouter.get('/', (req: Request, res: Response) => {
res.status(200).json({
status: 'Success',
message: 'Hello',
query: req.query,
})
});
mainRouter.post(
'/test-post',
bodyParser.urlencoded({ extended: true }), // 解析urlencoded格式的数据
bodyParser.json(), // 解析 application/json
(req: Request, res: Response) => {
res.status(200).json({
status: 'Success',
message: 'Hello',
body: req.body,
})
});
const routerMap: Map<string, Router> = new Map([
['/', mainRouter],
]);
const app: Application = express();
for (let [prefix, router] of routerMap.entries()) {
app.use(prefix, router);
}
app.listen(15000, () => {
console.log('Server1 is Running at port 15000');
});
关于 MVC 的分层设计
- Controller 提供访问服务器的 API 接口
- 调用接口时,匹配控制器的方法
- 控制器内部调用 service 层的方法
- service 层调用 model 层的方法
- model 层调用数据库
- model 层返回数据给 service 层
- service 层返回数据给 controller 层
- controller 层返回数据给客户端
Controller 提供访问服务器的 API 接口 - 示例代码:
import { Request, Response } from 'express';
import { UserService } from '../services/user.service';
class UserController {
private userService: UserService = new UserService();
public getUserInfo(req: Request, res: Response) {
const userId = req.query.userId;
this.userService.getUserInfo(userId).then((userInfo) => {
res.status(200).json(userInfo);
});
}
}
export default new UserController();
调用接口时,匹配控制器的方法 - 示例代码
import router from '../router';
import userController from '../controllers/user.controller';
router.get('/user', userController.getUserInfo);
控制器内部调用 service 层的方法 - 示例代码
import { UserService } from '../services/user.service';
export class UserService {
public getUserInfo(userId: string): Promise<any> {
return new Promise((resolve, reject) => {
// 调用数据库
resolve({
userId: userId,
userName: '张三',
});
});
}
}
直接使用 MVC 可能存在的问题:
- 不可否认:使用 MVC 架构能对项目的每个操作进行逻辑解耦
- 但实际开发中,我们更希望只关注业务操作本身,其他的事情(e.g 路由注册,控制器注册,sql 操作)我们并不关心
思考:
能不能采用一种约定 > 配置 > 编码的方式来设计一个小轮子,让方法如下调用:
- App 声明 & 配置 & 启动 (链式调用)
import { bootstrapApplication } from '@mvc-pkg';
// 引入服务器的模块
import myModule from './modules/test.module';
bootstrapApplication({
host: 'localhost',
port: 15001,
modules: [myModule],
})
.start();
- 声明服务模块
MyModule
import Module from '@mvc-pkg/app/classes/Module';
// 引入控制器
import Test1Controller from '../controllers/test1.controller';
// 引入服务 ...
// 引入中间件 ...
class MyModule extends Module {
constructor() {
super(MyModule.name);
}
}
export default new MyModule().createController([
Test1Controller,
// ... rest controllers
]);
-
声明控制器模块
Test1Controller, 控制器中非业务模块以装饰器声明为主:@Controller:全局托管的控制器的类装饰器@Cors:允许跨域的类装饰器@Get:GET 请求方法说明的方法装饰器@Post:POST 请求方法说明的方法装饰器@Put:PUT 请求说明的方法装饰器@Delete:DELETE 请求方法说明的方法装饰器@Patch:PATCH 请求方法说明的方法装饰器@PathVariable:路径变量的参数装饰器@Query:查询参数的参数装饰器@Body:请求体的参数装饰器
import {
Body,
Controller,
Cors,
Get,
Post,
Query,
PathVariable
} from '../../mvc-pkg';
import PostBody from '../bean/PostBody';
import StudentBean from '../bean/Student';
@Controller() // 全局托管的控制器
@Cors() // 允许跨域
class TestController {
// @Get('') -> 没有使用 Method 装饰器,方法默认为 Get, 请求路径默认为 ''
public getHelloWorld(): string {
return 'hello world';
}
@Get('/get-json')
public getJson(a: string, b: string) {
console.log(a, b);
return {
msg: '/get-json',
query: {
a,
b,
},
};
}
@Get('/get-query-json')
public getQueryJson(@Query() query: any) {
return new Promise((resolve, reject) => {
resolve({
msg: '/get-query-json',
query,
});
});
}
@Get('/get-student-query-json')
public getStudentQuery(@Query() studentQuery: StudentBean) {
return {
msg: '/get-student-query-json',
studentQuery,
};
}
@Post('/post-json')
public postJson(@Body() body: PostBody) {
return {
msg: '/post-json',
body,
};
}
@Post('/post-body-json')
public postBodyJson(@Body() body: PostBody) {
return {
msg: '/post-body-json',
body,
};
}
@Get('/get-path/:id')
public getPathVariable(@PathVariable() id: string) {
return {
msg: '/get-student-query-json',
id,
};
}
}
export default TestController;
当然可以!这里提供一个编写 node 轮子的思路(仅代表个人观点)。
轮子的架构设计
mvc-pkg
|- app # 应用模块
|- classes
|- Application.ts
|- Module.ts
|- bootstrap.ts # 启动配置
|- index.ts # app 出口
|- http # http 服务模块
|- request
|- decorators
|- @Controller.ts
|- @Cors.ts
|- @Get.ts
|- @Post.ts
|- @Put.ts
|- @Delete.ts
|- @Patch.ts
|- @PathVariable.ts
|- @Query.ts
|- @Body.ts
|- index.ts # http.request 包的出口
|- response
|- decorators
|- @ResponseHeader.ts # 设置响应头
|- index.ts # http.response 包的出口
|- middleware # 中间件模块
|- decorators
|- @Middleware.ts
|- index.ts # 服务包的出口
|- service # 服务模块
|- decorators
|- @Middleware.ts
|- index.ts # 服务包的出口
|- shared # 包内共享木块
|- index.ts # utils 包的出口
|- typings # 包内部的类型声明
|- index.ts # mvc-pkg 包的出口
轮子的功能设计
1. 出口:
在 @pkg-mvc 中,导出所有的模块
export * from './app';
export * from './http';
export * from './shared';
export * from './service';
export * from './middleware';
export type * from './typings';
2. 类型模块
import Module from '../app/classes/Module';
export interface BootstrapApplicationOptions {
host: string;
port: number;
controllers?: Array<Function | Object>;
modules?: Array<Module>;
}
import { Router } from "express";
export type TControllerOptions = undefined | string | {
prefix: string;
};
export type TPathVariableOptions = string | {
name: string;
};
export type TDecorateClass = (
& Function
& { router?: Router }
& { new(...args: any[]): any }
& { [key: string]: any }
);
type TMethodTarget = Record<(string | symbol), any>;
3. 工具模块:
Singleton.ts
function Singleton(): ClassDecorator {
return ((Target: Function & { new(...args: any[]): any }) => {
// 单例模式实现
let _instance: Function & { new(...args: any[]): any } | null = null;
Object.defineProperty(Target, '_instance', {
get: function () {
return _instance;
},
enumerable: true,
})
Object.defineProperty(Target, 'create', {
value: function (...args: any[]) {
if (_instance === null) {
_instance = new Target(...args);
}
return _instance;
},
enumerable: false,
});
return Target;
}) as ClassDecorator;
}
export default Singleton;
Tools.ts
import express, { Application } from 'express';
import bodyParser from 'body-parser';
import { TControllerOptions, TDecorateClass } from '../typings';
class Tools {
static Tools = Tools;
// 获取 express router 的 baseURL (app.use() 的时候使用)
getRouterBasePath(options: TControllerOptions): string {
if (typeof options === 'string') {
return options;
}
if (typeof options === 'object' && options !== null) {
return `${options.prefix}`;
}
return '/';
}
// 获取控制器方法的参数列表
getFnParams(
fnString: string,
methodName: string,
controllerObject: TDecorateClass,
) {
const argList: string[] = [];
fnString
?.replace
?.(/\n/igm, '')
.replace(/((.+?))/, (s: string, k: string) => {
const methodQueryMap: Map<string, number> = controllerObject.$methodQueryMap;
const methodBodyMap: Map<string, number> = controllerObject.$methodBodyMap;
const pathVariableMap: Map<
string | Symbol,
{
index: number;
value: string;
parameterName?: string;
}
> = controllerObject.$pathVariableMap;
const kList = k
.split(',')
.map(item => item.trim())
.map((item, index) => {
if (methodQueryMap && methodQueryMap.has(methodName) && index === methodQueryMap.get(methodName)) {
return '$query';
}
if (methodBodyMap && methodBodyMap.has(methodName) && index === methodBodyMap.get(methodName)) {
return '$body';
}
if (pathVariableMap && pathVariableMap.has(methodName) && index === pathVariableMap.get(methodName).index) {
const pathVariableObject = pathVariableMap.get(methodName);
return `$params:${pathVariableObject.parameterName || item}`;
}
return item;
})
argList.push(...kList);
return s;
});
return argList;
}
createApp(): Application {
const expressApp = express();
expressApp.use(bodyParser.urlencoded({ extended: true }));
expressApp.use(bodyParser.json());
return expressApp;
}
}
export default new Tools();
3. 应用模块
出口
在应用模块中,需要一个核心方法:bootstrapApplication():
import { BootstrapApplicationOptions } from '../typings';
import Application from './classes/Application';
function bootstrapApplication(
options: BootstrapApplicationOptions,
): Application {
return Application.create(options);
}
应用类
应用类有以下几个作用:
- 内置
express(将 express.Application 作为一个观察者来进行使用) - 集成模块 & 依赖
import type { Application as ExpressApp } from 'express';
import type { BootstrapApplicationOptions } from '../../typings';
import Singleton from '../../shared/Singleton';
import tools from '../../shared/Tools';
import Module from './Module';
@Singleton()
class Application {
private static _instance: Application;
private host: string;
private port: number;
private expressApp: ExpressApp;
private _modulesMap: Map<string, Module>;
private constructor(options: Partial<BootstrapApplicationOptions>) {
this.host = options.host || 'localhost';
this.port = options.port || 3000;
this._modulesMap = new Map([
['main', new Module('main')],
]);
options.modules?.forEach?.(m => [
this._modulesMap.set(m.getModuleName(), m)
])
this.expressApp = tools.createApp();
}
public start() {
// ... 注册 Controller
// ... 注册 Service
// ... 注册 Middleware
// 启动服务, 返回应用实例
this.expressApp.listen(this.port, this.host, () => {
console.log(`Server is running at http://${this.host}:${this.port}`);
})
}
public static create(options: Partial<BootstrapApplicationOptions>): Application {
throw new Error('Method not implemented.');
}
public static getInstance(): Application {
return Application._instance;
}
public getExpressApp(): ExpressApp {
return this.expressApp;
}
public getModulesMap(): Map<string, Module> {
return this._modulesMap;
}
public getModule(moduleName: string): Module | null {
return this._modulesMap.get(moduleName);
}
public createController(controllers?: any[]): Application {
const mainModule: Module = this._modulesMap.get('main');
mainModule.createController(controllers);
this._modulesMap.set('main', mainModule);
return this;
}
}
export default Application;
模块类
模块类有以下几个作用:
- 注册 Controller
- 注册 Service
- 注册 Middleware
export default class Module {
constructor(
private _moduleName: string,
private _controllerSet: Set<Object> = new Set(),
private _serviceSet: Set<Object> = new Set(),
private _middlewareSet: Set<Object> = new Set(),
) {}
public getModuleName() {
return this._moduleName;
}
public createController(controllers?: any[]) {
if (Array.isArray(controllers) && controllers.length) {
controllers.forEach(c => {
this._controllerSet.add(c);
});
console.log('====【%s】模块添加了控制器: %s ====', this._moduleName, controllers);
}
return this;
}
public createService(services?: any[]) {
if (Array.isArray(services) && services.length) {
services.forEach(s => {
this._serviceSet.add(s);
});
}
console.log('====【%s】模块添加了服务: %s ====', this._moduleName, services);
return this;
}
public createMiddleware(middlewares?: any[]) {
if (Array.isArray(middlewares) && middlewares.length) {
middlewares.forEach(m => {
this._middlewareSet.add(m);
});
}
return this;
}
}
http 模块
http.request:
index.ts
export { default as Controller } from './decorators/@Controller';
export { default as Cors } from './decorators/@Cors';
export { default as Get } from './decorators/@Get';
export { default as Post } from './decorators/@Post';
export { default as Put } from './decorators/@Put';
export { default as Query } from './decorators/@Query';
export { default as Body } from './decorators/@Body';
export { default as PathVariable } from './decorators/@PathVariable';
decorators/@Controller.ts
import { Router, Request, Response } from 'express';
import Application from '../../../app/classes/Application';
import { TControllerOptions, TDecorateClass } from '../../../typings';
import { tools } from '../../../shared';
function Controller(options?: TControllerOptions) {
return (ControllerClass: TDecorateClass) => {
setImmediate(() => {
const controllerObject: TDecorateClass = new ControllerClass();
const application: Application = Application.getInstance();
const router: Router = Router();
const routerBasePath: string = tools.getRouterBasePath(options);
const keys: string[] = [
...Object.getOwnPropertyNames(controllerObject),
...Object.getOwnPropertyNames(ControllerClass.prototype),
];
keys.forEach((k) => {
if (k === 'constructor') {
return;
}
const controllerMethod = controllerObject[k];
if (typeof controllerMethod === 'function') {
const requestMethod: 'get' | 'post' | 'put' | 'delete' | 'options' | 'patch' =
['get', 'post', 'put', 'delete', 'options', 'patch'].includes(controllerMethod.method)
? controllerMethod.method
: 'get';
const requestPath = controllerMethod.path || '';
const fnParamsList = tools.getFnParams(
controllerMethod.fnString || controllerMethod.toString(),
k,
controllerObject,
);
// TODO add request method & params & pathvariable
router[requestMethod](requestPath, async (req: Request, resp: Response) => {
const paramsList = fnParamsList.map(k => {
if (k === '$query') {
return req.query;
}
if (k === '$body') {
return req.body;
}
if (k.includes('$params')) {
const variableProperty = k.split('$params:')[1];
return req.params[variableProperty];
}
return req.query[k];
});
const result = await controllerMethod.apply(controllerObject, [...paramsList]);
resp.send(result);
});
}
});
application.getExpressApp().use(routerBasePath, router);
ControllerClass.router = router;
});
}
}
export default Controller;
decorators/@Cors.ts
import type { Router, Request, Response, NextFunction } from 'express';
import { TDecorateClass } from '../../../typings';
function Cors() {
return (Target: TDecorateClass) => {
const router: Router = Target.router;
if (router) {
router.use('*', async (req: Request, res: Response, next: NextFunction) => {
// CORS
res.header('Access-Control-Allow-Origin', '*');
// res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
// res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
// intercepts OPTIONS method
if ('OPTIONS' === req.method) {
res.sendStatus(200);
}
await next();
});
}
};
}
export default Cors;
decorators/@Get.ts(@Post,@Put,@Delete同理)
function Get(path: string = '') {
return (
Target: any,
name: string,
descriptor: PropertyDescriptor,
) => {
const fn = descriptor.value;
descriptor.value = function (...args: any[]) {
return fn.apply(this, args);
}
descriptor.value.path = path;
descriptor.value.method = 'get';
descriptor.value.fnString = fn.toString();
}
}
export default Get;
decorators/@Query.ts
import type { TMethodTarget } from '../../../typings';
function Query(): ParameterDecorator {
return (tar, name, index) => {
const target: TMethodTarget = <TMethodTarget> tar;
// const TargetConstructor: Function = target.constructor;
// const fn = target[name];
if (!target.$methodQueryMap) {
target.$methodQueryMap = new Map<string, number>();
}
target.$methodQueryMap.set(name, index);
return target;
}
}
export default Query;
decorators/@Body.ts
import type { TMethodTarget } from '../../../typings';
function Body(): ParameterDecorator {
return (tar, name, index) => {
const target: TMethodTarget = <TMethodTarget> tar;
// const TargetConstructor: Function = target.constructor;
// const fn = target[name];
if (!target.$methodBodyMap) {
target.$methodBodyMap = new Map<string, number>();
}
target.$methodBodyMap.set(name, index);
return target;
}
}
export default Body;
decorators/@PathVariable.ts
import { TPathVariableOptions } from '../../../typings';
function PathVariable(opt?: TPathVariableOptions): ParameterDecorator {
return (tar, name, index) => {
/**
* Map {
[methodName: string]: {
argIndex: number,
argValue: params.value,
}
}
*/
const target = <typeof tar & {
$pathVariableMap: Map<
string | Symbol,
{
index: number;
value: string;
parameterName?: string;
}
>
}>tar;
if (!target.$pathVariableMap) {
target.$pathVariableMap = new Map();
}
target.$pathVariableMap.set(name, {
index,
value: '',
parameterName: (
typeof opt === 'object' && opt !== null ?
opt?.name
: typeof opt === 'string'
? opt
: ''
),
});
};
}
export default PathVariable;
http.response:
Service 模块
出口:
export { default as Service } from './decorators/@Service';
业务模块:
decorators/@Service.ts:
function Service(
Services: Array<{ new (...args: any[]): any }> = []
): ClassDecorator {
return (Target) => {
for (let Service of Services) {
const serviceInstanceName = Service.name.replace(
/^[A-Z]/, (match) => match.toLowerCase()
);
Target.prototype[serviceInstanceName] = new Service();
}
return Target;
}
}
export default Service;