基于TypeScript裝飾器定義Express RESTful 服務

前言

本文主要講解如何使用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),正好我們可以借助它實現我們的需求呀潭。至于裝飾器用法钉迷,可以參考我的上一篇文章

20180107195916

上圖中左邊是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]);
}
image 6
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路由注冊(回調函數參數處理,返回值處理等)
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末怕午,一起剝皮案震驚了整個濱河市废登,隨后出現的幾起案子,更是在濱河造成了極大的恐慌郁惜,老刑警劉巖堡距,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異兆蕉,居然都是意外死亡羽戒,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門虎韵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來易稠,“玉大人,你說我怎么就攤上這事劝术∷醵啵” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵养晋,是天一觀的道長衬吆。 經常有香客問我,道長绳泉,這世上最難降的妖魔是什么逊抡? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮零酪,結果婚禮上冒嫡,老公的妹妹穿的比我還像新娘。我一直安慰自己四苇,他們只是感情好孝凌,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著月腋,像睡著了一般蟀架。 火紅的嫁衣襯著肌膚如雪瓣赂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天片拍,我揣著相機與錄音煌集,去河邊找鬼。 笑死捌省,一個胖子當著我的面吹牛苫纤,可吹牛的內容都是我干的。 我是一名探鬼主播纲缓,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼卷拘,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祝高?” 一聲冷哼從身側響起恭金,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎褂策,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體颓屑,經...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡斤寂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了揪惦。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片遍搞。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖器腋,靈堂內的尸體忽然破棺而出溪猿,到底是詐尸還是另有隱情,我是刑警寧澤纫塌,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布诊县,位于F島的核電站,受9級特大地震影響措左,放射性物質發(fā)生泄漏依痊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一怎披、第九天 我趴在偏房一處隱蔽的房頂上張望胸嘁。 院中可真熱鬧,春花似錦凉逛、人聲如沸性宏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽毫胜。三九已至书斜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間指蚁,已是汗流浹背菩佑。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留凝化,地道東北人稍坯。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像搓劫,于是被迫代替她去往敵國和親瞧哟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內容

  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理枪向,服務發(fā)現勤揩,斷路器,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,754評論 25 707
  • 2018年4月4星期三 今天因去縣公司開會秘蛔,上午放學沒有安時接欣瑞陨亡。可是我還沒到家深员,就給我打來了電說牙疼负蠕,并且疼的...
    永遠幸福_cfea閱讀 191評論 0 0
  • 許久, 沒有自我倦畅! 想說的話遮糖,想做的事, 都默默的埋葬在心中叠赐。 忙忙碌碌欲账, 碌碌無為…… 有你! 珍好芭概! 接下來赛不,...
    愛茶匠紅豆閱讀 152評論 0 0
  • 本節(jié)知識點 組件標簽 模板標簽用的`` 概述 <component></component>標簽是vue自定義的標...
    我擁抱著我的未來閱讀 3,364評論 2 0