前言
本文主要講解如何使用TypeScript裝飾器定義Express路由。文中出現(xiàn)的代碼經(jīng)過簡化不能直接運行,完整代碼的請戳:https://github.com/WinfredWang/express-decorator
1 為什么使用裝飾器
當(dāng)我們在使用Express時,經(jīng)常要暴露RESTful服務(wù),代碼如下:
var express = require('express');
var app = express();
app.get('/users', function(req, res) {
res.send([{name:'xx'}]);
});
// 路由模塊化寫法
var router = express.Router();
app.get('/users', function(req, res) {
res.send([{name:'xx'}]);
});
熟悉Java WEB童鞋知道jax-rs可以使用標注(annotation)聲明服務(wù)。例:
@Path("/myResource")
public class SomeResource {
@GET
public String doGetAsPlainText() {
...
}
@GET
public String doGetAsHtml() {
...
}
}
使用這種方式聲明的服務(wù)非常簡潔方便,免去了寫一坨重復(fù)代碼之苦,而且看起來更加清晰,那我們看看在Node.js中如何做。
2 需求
參照jax-rs規(guī)范,我們列出如下需求:
- 使用
@Path聲明RESTful服務(wù)路由 - 使用
@GET/@POST/@DELETE/@PUT聲明子路由 - 使用
@PathParam,@QueryParam,@HeaderParam,@CookieParam,@FormParam,來接受服務(wù)參數(shù)
3 實現(xiàn)思路
在ES6和TypeScript中有新特性:裝飾器(Decorator),正好我們可以借助它實現(xiàn)我們的需求。至于裝飾器用法,可以參考我的上一篇文章。

上圖中左邊是Java中定義RESTful代碼,右邊是Express代碼,其實他們本質(zhì)上是一一對應(yīng)的。我們只要在裝飾器的定義中實現(xiàn)Express 路由即可。
繼續(xù)思考,我們Express 路由到底是放到那個注解中實現(xiàn)呢?
我們知道不同裝飾器(類/方法/參數(shù))執(zhí)行順序不同:
參數(shù)裝飾器先執(zhí)行,然后方法最后類裝飾器
根據(jù)這個特性我們應(yīng)該將核心實現(xiàn)放到類裝飾器Path中執(zhí)行是不是就可以了呢?
其實不是,我們看如下代碼,我們在user-service.ts中定義了UserService服務(wù)。
@Path("/user")
class UserService {
@GET("/{id}")
public getUsers(@PathParam("id") id: string) {
// TODO
}
}
我們定義好了服務(wù),然后想讓Node.js模塊加載,我們必須在工程入口模塊(main.ts)中導(dǎo)入上述文件
main.ts代碼:
import { HelloService } from './hello-service'
// TODO
上述服務(wù)代碼會執(zhí)行嗎?也就是說
如果僅僅導(dǎo)入模塊,而沒有使用該模塊的話,Node.js是否會加載這個模塊呢,換句話說這個模塊會執(zhí)行嗎?答案是NO。
為啥呀?因為Node.js對其做了優(yōu)化,只有一個模塊被真正用到才會加載。
上有政策,下有對策。我們就在模塊引用一下。
import { HelloService } from './hello-service'
HelloService; // 就是為了讓Node加載它
這樣好嗎,當(dāng)然不好。誰知道這是干嘛的。
所以我們應(yīng)該換了思路,將Express 注冊路由代碼拿到裝飾器外部,額外提供注冊服務(wù)的入口,通過該注冊服務(wù)入口,用戶可以顯式看到有哪些服務(wù)。
import { HelloService } from './hello-service';
import {RegisterService } from 'xxx';
RegisterService([HelloService]);//注冊服務(wù)
4 裝飾器核心代碼
基于上面的思考,我們在裝飾器的實現(xiàn)中只是單純地存儲RESTful url以及參數(shù)即可,剩下服務(wù)注冊工作交給RegisterService去做。
Path裝飾器實現(xiàn)
function Path(baseUrl: string) {
return function (target) {
target.prototype.$Meta = {
baseUrl: baseUrl
}
}
}
這里我們將RESTful路由存儲到類的原型中,以便服務(wù)實例化時能獲取到。
GET/POST/DELETE/PUT
function GET (url: string) => {
return (target, methodName: string, descriptor: PropertyDescriptor) => {
let meta = getMethod(target, methodName);
meta.subUrl = url;
meta.httpMethod = httpMehod;
}
}
QueryParam/PathParam等實現(xiàn)
function PahtParam(paramType: string) {
return function (target, methodName: string, paramIndex: number) {
let meta = getMethod(target, methodName);
meta.params.push({
name: paramName ? paramName : paramType,
index: paramIndex,
type: paramType
});
}
}
上述就裝飾自身代碼,本質(zhì)上就是講路由、http請求方法和參數(shù)存儲到類的原型對象中,以便后續(xù)可以去到。
5 注冊服務(wù)核心代碼
路由實現(xiàn)
經(jīng)過上面的分析,我們可知注冊服務(wù)主要將Express中注冊路由交由我們框架處理,核心代碼如下:
function RegisterService(app, service) {
let router = Router();
// 1. 獲取存儲在原型對象中的http請求信息()
let meta = getClazz(service.prototype);
// 2. 實例化服務(wù)類
let serviceInstance = new service();
let routes = meta.routes;
for (const methodName in routes) {
let methodMeta = routes[methodName];
let httpMethod = methodMeta.httpMethod;
// 3. 回調(diào)函數(shù)
let fn = (req, res, next) => {
let result = service.prototype[methodName].apply(serviceInstance, params);
res.send(result);
};
// 4. 注冊路由
router[httpMethod].apply(router, methodMeta.subUrl);
}
// 5. 路由中間件
app.use.apply(app, [meta.baseUrl]);
}

http請求參數(shù)處理
@GET('/:id', [ testMidware1 ])
list( @PathParam('id') id: string, @QueryParam('name') name: string) {
return {name:"tom", age: 10}
}
用戶編碼時我們期望回調(diào)函數(shù)中的參數(shù)框架自動注入,而不是讓用戶自己從request中取,所以在注冊服務(wù)代碼中第3處,框架需要出更加參數(shù)裝飾器中信息,從request中取值后注入回調(diào)函數(shù)中
// 3. 回調(diào)函數(shù)
let params = extractParameters(req, res, methodMeta['params']);
let fn = (req, res, next) => {
let result = service.prototype[methodName].apply(serviceInstance, params);
res.send(result);
};
// 根據(jù)參數(shù)類型,從request取出對應(yīng)的值
function extractParameters(req, paramMeta) {
let paramHandlerTpe = {
'query': (paramName: string) => req.query[paramName],
'path': (paramName: string) => req.params[paramName],
'form': (paramName: string) => req.body[paramName],
'cookie': (paramName: string) => req.cookies && req.cookies[paramName],
'header': (paramName) => req.get(paramName),
'request': () => req, // 獲取request/response對象,做一些特別操作
'response': () => res,
}
let args = [];
params.forEach(param => {
args.push(paramHandlerTpe[param.type](param.name))
})
return args;
}
response處理
@GET('/:id', [ testMidware1 ])
list( @PathParam('id') id: string, @QueryParam('name') name: string) {
return {name:"tom", age: 10}
}
一個服務(wù)處理完成后,總是要向瀏覽器返回值的,在回調(diào)函數(shù)中直接使用return語句,而不是自己調(diào)用response.send方法, 如下代碼:
// 3. 回調(diào)函數(shù)
let fn = (req, res, next) => {
let result = service.prototype[methodName].apply(serviceInstance, params);
// 支持promise處理
if (result instanceof Promise) {
result.then(value => {
!res.headersSent && res.send(value);
}).catch(err => {
next(err);
});
} else if (result !== undefined) {
!res.headersSent && res.send(result);
}
};
6 總結(jié)
以上就是我們框架處理核心代碼,核心實現(xiàn)主要有兩步:
- 裝飾器本身用來存在路由信息
- 注冊機制實現(xiàn)express路由注冊(回調(diào)函數(shù)參數(shù)處理,返回值處理等)