前言
本文主要講解如何使用TypeScript裝飾器定義Express路由瞄勾。文中出現的代碼經過簡化不能直接運行蝗柔,完整代碼的請戳:https://github.com/WinfredWang/express-decorator
1 為什么使用裝飾器
當我們在使用Express時宣蔚,經常要暴露RESTful服務岛宦,代碼如下:
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)聲明服務失尖。例:
@Path("/myResource")
public class SomeResource {
@GET
public String doGetAsPlainText() {
...
}
@GET
public String doGetAsHtml() {
...
}
}
使用這種方式聲明的服務非常簡潔方便较剃,免去了寫一坨重復代碼之苦眉孩,而且看起來更加清晰个绍,那我們看看在Node.js中如何做。
2 需求
參照jax-rs
規(guī)范浪汪,我們列出如下需求:
- 使用
@Path
聲明RESTful服務路由 - 使用
@GET/@POST/@DELETE/@PUT
聲明子路由 - 使用
@PathParam巴柿,@QueryParam,@HeaderParam死遭,@CookieParam广恢,@FormParam,
來接受服務參數
3 實現思路
在ES6和TypeScript中有新特性:裝飾器(Decorator)
,正好我們可以借助它實現我們的需求呀潭。至于裝飾器用法钉迷,可以參考我的上一篇文章。
上圖中左邊是Java中定義RESTful代碼钠署,右邊是Express代碼糠聪,其實他們本質上是一一對應的。我們只要在裝飾器的定義中實現Express 路由即可谐鼎。
繼續(xù)思考舰蟆,我們Express 路由到底是放到那個注解中實現呢?
我們知道不同裝飾器(類/方法/參數)執(zhí)行順序不同:
參數裝飾器先執(zhí)行,然后方法最后類裝飾器
根據這個特性我們應該將核心實現放到類裝飾器Path
中執(zhí)行是不是就可以了呢身害?
其實不是味悄,我們看如下代碼,我們在user-service.ts
中定義了UserService
服務题造。
@Path("/user")
class UserService {
@GET("/{id}")
public getUsers(@PathParam("id") id: string) {
// TODO
}
}
我們定義好了服務傍菇,然后想讓Node.js模塊加載,我們必須在工程入口模塊(main.ts)中導入上述文件
main.ts代碼:
import { HelloService } from './hello-service'
// TODO
上述服務代碼會執(zhí)行嗎界赔?也就是說
如果僅僅導入模塊,而沒有使用該模塊的話牵触,Node.js是否會加載這個模塊呢淮悼,換句話說這個模塊會執(zhí)行嗎?答案是NO揽思。
為啥呀袜腥?因為Node.js對其做了優(yōu)化,只有一個模塊被真正用到才會加載钉汗。
上有政策羹令,下有對策。我們就在模塊引用一下损痰。
import { HelloService } from './hello-service'
HelloService; // 就是為了讓Node加載它
這樣好嗎福侈,當然不好。誰知道這是干嘛的卢未。
所以我們應該換了思路肪凛,將Express 注冊路由代碼拿到裝飾器外部,額外提供注冊服務的入口辽社,通過該注冊服務入口伟墙,用戶可以顯式看到有哪些服務。
import { HelloService } from './hello-service';
import {RegisterService } from 'xxx';
RegisterService([HelloService]);//注冊服務
4 裝飾器核心代碼
基于上面的思考滴铅,我們在裝飾器的實現中只是單純地存儲RESTful url以及參數即可戳葵,剩下服務注冊工作交給RegisterService
去做。
Path裝飾器實現
function Path(baseUrl: string) {
return function (target) {
target.prototype.$Meta = {
baseUrl: baseUrl
}
}
}
這里我們將RESTful路由存儲到類的原型中汉匙,以便服務實例化時能獲取到拱烁。
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等實現
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
});
}
}
上述就裝飾自身代碼,本質上就是講路由盹兢、http請求方法和參數存儲到類的原型對象中邻梆,以便后續(xù)可以去到。
5 注冊服務核心代碼
路由實現
經過上面的分析绎秒,我們可知注冊服務主要將Express中注冊路由交由我們框架處理,核心代碼如下:
function RegisterService(app, service) {
let router = Router();
// 1. 獲取存儲在原型對象中的http請求信息()
let meta = getClazz(service.prototype);
// 2. 實例化服務類
let serviceInstance = new service();
let routes = meta.routes;
for (const methodName in routes) {
let methodMeta = routes[methodName];
let httpMethod = methodMeta.httpMethod;
// 3. 回調函數
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請求參數處理
@GET('/:id', [ testMidware1 ])
list( @PathParam('id') id: string, @QueryParam('name') name: string) {
return {name:"tom", age: 10}
}
用戶編碼時我們期望回調函數中的參數框架自動注入浦妄,而不是讓用戶自己從request
中取,所以在注冊服務代碼中第3處,框架需要出更加參數裝飾器中信息剂娄,從request中取值后注入回調函數中
// 3. 回調函數
let params = extractParameters(req, res, methodMeta['params']);
let fn = (req, res, next) => {
let result = service.prototype[methodName].apply(serviceInstance, params);
res.send(result);
};
// 根據參數類型蠢涝,從request取出對應的值
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}
}
一個服務處理完成后阅懦,總是要向瀏覽器返回值的和二,在回調函數中直接使用return
語句,而不是自己調用response.send方法耳胎, 如下代碼:
// 3. 回調函數
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 總結
以上就是我們框架處理核心代碼惯吕,核心實現主要有兩步:
- 裝飾器本身用來存在路由信息
- 注冊機制實現express路由注冊(回調函數參數處理,返回值處理等)