背景
最近开始学习使用express来写api,按照官方文档写出如下的代码
// index.ts
import express from 'express'
import { Express, Request, Response } from 'express'
const app: Express = express()
const port = 3000
app.get('/', (req: Request, res: Response) => {
res.status(200).json({
message: 'ok'
})
})
app.get('/user/getUser', (req: Request, res: Response) => {
// 业务逻辑.........
res.status(200).json({
message: 'getUser'
})
})
// 随着业务的拓展,这里会有很多的接口............
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
随着业务逻辑当增多,把所有的api平铺在一块,这显然不是我想要的
一、初步改进-按业务分文件
很容易就会想到,可以把同样类型业务的api抽出来单独放到文件中,形成这样的目录的目录结构,这显然是有助于拓展和维护的
// User.ts
import { Express, Request, Response } from 'express'
export default class User {
app: Express
constructor(app: Express) {
this.app = app
}
getUserById() {
this.app.get('/user/getUser', (req: Request, res: Response) => {
// 业务逻辑.........
res.status(200).json({
message: 'getUser'
})
})
}
getUserList() {}
addUser() {}
deleteUser() {}
}
// 在index.ts中注册User文件中的接口
import User from './api/User'
const user = new User(app)
for (const key in user) {
if (typeof user[key] === 'function' && key !== 'app') {
user[key]()
}
}
但是,我们会发现,在User.ts中,每个接口都要写一遍app.get,或者app.post来注册api,这显然还是产生了大量的重复代码,怎么继续优化使得我只在User类中写业务代码?
二、用类装饰器和reflect-metedata来彻底将业务代码和非业务代码剥离
1. 先看下优化过的目录结构
这里新增了router.ts和decorators/http.ts 两个文件
- api:业务接口存放当目录,一个文件用一个类来代表一个业务
- decorators:装饰器目录,http.ts主要对api里面的类进行装饰
- index.ts:项目入口文件
- router.ts:路由,用来解析api里的类,注册接口到express的app实例中
2. 核心代码
- index.ts
import express from 'express'
import { Express, Request, Response } from 'express'
import 'reflect-metadata'
import router from './router'
const app: Express = express()
const port = 3000
router(app)
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
- http.ts
import { Request, Response } from 'express'
const httpMethodKey = Symbol('router:httpMethod')
// 注意这里是typescript5的装饰器写法
// 请求类型装饰器
const httpMethod = function (method: 'get' | 'post') {
return function (target: any, context: any) {
// 挂载元数据,请求类型
Reflect.defineMetadata(httpMethodKey, method, target, context.name)
}
}
const httpPathKey = Symbol('router:httpPath')
// 请求地址装饰器
const path = function (path: string) {
return function (target: any, context: any) {
// 1. 覆盖原方法
const replaceMethod = (req: Request, res: Response): any => {
const p = Object.assign({}, req.query, req.body)
let result = target.call(this, p)
res.status(200).send(result)
return result
}
// 2. 挂载元数据,请求地址
Reflect.defineMetadata(httpPathKey, path, replaceMethod, context.name)
return replaceMethod
}
}
export { httpMethodKey, httpMethod, httpPathKey, path }
- router.ts
import { Express, Request, Response } from 'express'
import { httpMethodKey, httpPathKey } from './decorators/http'
import User from './api/User'
// 路由
export default function router(app: Express) {
let user = new User()
for (let methodName in user) {
let method = user[methodName]
if (typeof method !== 'function') break
// 解析元数据
let httpMethod = Reflect.getMetadata(httpMethodKey, method, methodName)
let httpPath = Reflect.getMetadata(httpPathKey, method, methodName)
// 注册api
if (httpMethod && httpPath) app[httpMethod](httpPath, method)
}
}
- User.ts
import { httpMethod, path } from '../decorators/http'
export default class User {
@httpMethod('get')
@path('/getUserById')
getUserById(param: any, ...Args: any[]): any {
// 参数是query和body拼接的参数
console.log(param)
//........这里只需要写业务代码,接口函数的包装交由装饰器,接口注册交由router
// return值就是接口返回值
return {
name: 'xiaoming',
age: 10
}
}
@httpMethod('get')
@path('/getUserList')
getUserList(param: any, ...Args: any[]) {
return [
{
name: 'xiaoming',
age: 10
},
{
name: 'xiaohua',
age: 20
},
{
name: 'xiaoli',
age: 30
}
]
}
@httpMethod('post')
addUser() {}
@httpMethod('post')
deleteUser() {}
}
完整代码参考express-router-demo(gitee.com)
测试
通过!
这里为接口路由的管理提供了一种简单的思路,希望大家多指点改进。