基于TypeScript裝飾器定義Express RESTful 服務(wù)

前言

本文主要講解如何使用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)我們的需求。至于裝飾器用法,可以參考我的上一篇文章

20180107195916

上圖中左邊是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]);
}
image 6
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ù)處理,返回值處理等)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,697評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,351評論 25 708
  • 2018年4月4星期三 今天因去縣公司開會,上午放學(xué)沒有安時接欣瑞??墒俏疫€沒到家,就給我打來了電說牙疼,并且疼的...
    永遠幸福_cfea閱讀 205評論 0 0
  • 許久, 沒有自我! 想說的話,想做的事, 都默默的埋葬在心中。 忙忙碌碌, 碌碌無為…… 有你! 珍好! 接下來,...
    愛茶匠紅豆閱讀 173評論 0 0
  • 本節(jié)知識點 組件標簽 模板標簽用的`` 概述 <component></component>標簽是vue自定義的標...
    我擁抱著我的未來閱讀 3,568評論 2 0

友情鏈接更多精彩內(nèi)容