前言

本文主要讲解如何使用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规范,我们列出如下需求:

  • 使用@Path声明RESTful服务路由
  • 使用@GET/@POST/@DELETE/@PUT声明子路由
  • 使用@PathParam,@QueryParam,@HeaderParam,@CookieParam,@FormParam,来接受服务参数

3 实现思路

在ES6和TypeScript中有新特性:装饰器(Decorator),正好我们可以借助它实现我们的需求。至于装饰器用法,可以参考我的上一篇文章

上图中左边是Java中定义RESTful代码,右边是Express代码,其实他们本质上是一一对应的。我们只要在装饰器的定义中实现Express 路由即可。

继续思考,我们Express 路由到底是放到那个注解中实现呢?

我们知道不同装饰器(类/方法/参数)执行顺序不同:

参数装饰器先执行,然后方法最后类装饰器

根据这个特性我们应该将核心实现放到类装饰器Path中执行是不是就可以了呢?

其实不是,我们看如下代码,我们在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

上述服务代码会执行吗?也就是说

如果仅仅导入模块,而没有使用该模块的话,Node.js是否会加载这个模块呢,换句话说这个模块会执行吗?答案是NO。

为啥呀?因为Node.js对其做了优化,只有一个模块被真正用到才会加载。

上有政策,下有对策。我们就在模块引用一下。

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请求方法和参数存储到类的原型对象中,以便后续可以去到。

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路由注册(回调函数参数处理,返回值处理等)

基于TypeScript装饰器定义Express RESTful 服务的更多相关文章

  1. typescript装饰器定义 类装饰器 属性装饰器 装饰器工厂

    /* 装饰器:装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,属性或参数上,可以修改类的行为. 通俗的讲装饰器就是一个方法,可以注入到类.方法.属性参数上来扩展类.属性.方法.参数的功能. 常 ...

  2. 从C#到TypeScript - 装饰器

    总目录 从C#到TypeScript - 类型 从C#到TypeScript - 高级类型 从C#到TypeScript - 变量 从C#到TypeScript - 接口 从C#到TypeScript ...

  3. Angular 个人深究(一)【Angular中的Typescript 装饰器】

    Angular 个人深究[Angular中的Typescript 装饰器] 最近进入一个新的前端项目,为了能够更好地了解Angular框架,想到要研究底层代码. 注:本人前端小白一枚,文章旨在记录自己 ...

  4. 第7.27节 Python案例详解: @property装饰器定义属性访问方法getter、setter、deleter

    上节详细介绍了利用@property装饰器定义属性的语法,本节通过具体案例来进一步说明. 一.    案例说明 本节的案例是定义Rectangle(长方形)类,为了说明问题,除构造函数外,其他方法都只 ...

  5. 第7.26节 Python中的@property装饰器定义属性访问方法getter、setter、deleter 详解

    第7.26节 Python中的@property装饰器定义属性访问方法getter.setter.deleter 详解 一.    引言 Python中的装饰器在前面接触过,老猿还没有深入展开介绍装饰 ...

  6. Python使用property函数和使用@property装饰器定义属性访问方法的异同点分析

    Python使用property函数和使用@property装饰器都能定义属性的get.set及delete的访问方法,他们的相同点主要如下三点: 1.定义这些方法后,代码中对相关属性的访问实际上都会 ...

  7. TypeScript装饰器(decorators)

    装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,可以修改类的行为. 装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被 ...

  8. TypeScript 装饰器

    装饰器(Decorators)可用来装饰类,属性,及方法,甚至是函数的参数,以改变和控制这些对象的表现,获得一些功能. 装饰器以 @expression 形式呈现在被装饰对象的前面或者上方,其中 ex ...

  9. TypeScript 装饰器的执行原理

    装饰器本质上提供了对被装饰对象 Property​ Descriptor 的操作,在运行时被调用. 因为对于同一对象来说,可同时运用多个装饰器,然后装饰器中又可对被装饰对象进行任意的修改甚至是替换掉实 ...

随机推荐

  1. ES6 Proxy和Reflect(下)

    construct() construct方法用于拦截new命令. var handler = { construct (target, args) { return new target(...ar ...

  2. UWP Windows历史上最漂亮的UWP框架出炉!!!

    UWP Windows历史上最漂亮的UWP框架出炉!!! 本框架基于微软的开源项目WTS开发,并在其基础上增加了FDS(流畅设计元素,高光.亚克力等).多语言系统.沉浸式体验(扩展内容到标题栏) 同时 ...

  3. CSS图片翻转动画技术详解

    因为不断有人问我,现在我补充一下:IE是支持这种技术的!尽管会很麻烦.需要做的是旋转front和back元素,而不是旋转整个容器元素.如果你使用的是最新版的IE,可以忽略这一节.IE10+是支持的,I ...

  4. 使用 mysql PDO 防止sql注入

    技巧: 1. php升级到5.3.6+,生产环境强烈建议升级到php 5.3.9+ php 5.4+,php 5.3.8存在致命的hash碰撞漏洞. 2. 若使用php 5.3.6+, 请在在PDO的 ...

  5. Python学习_12_方法和类定制

    方法 在上一篇随笔中,简单提到了类的某些方法:__init__()等的调用,并简要说明方法和函数的区别. 方法是在类内部定义的函数,方法也是对象,所以方法是类的属性,这就是为什么说实例的方法存在于类定 ...

  6. ORA-00600[17059]错误

    ORA-00600[17059] ORA-00600[17059]错误大部分都是因为高版本导致,对于本库的分析:因为该库的 shared pool老化比较频繁,到我介入的时候,发现相关该类此sql已经 ...

  7. 程序包管理rpm、yum与简单编译安装程序

    Linux程序包管理 Linux中软件的安装主要有两种形式:一种是直接下载源代码包自行编译后安装,另一种直接获取rpm软件包进行安装. 程序的组成部分: 二进制程序:程序的主体文件,比如我们运行一个l ...

  8. Python day 7(2) 类和实例(1)

    一:面向对象编程 1  面向对象编程--Object Oriented Programming,简称OOP,是一种程序设计思想.OOP把对象作为程序的基本单元,一个对象包含了数据和操作数据的函数. 2 ...

  9. 【bird-front】全自动数据表格组件bird-grid

    bird-grid是bird-front前端框架中实现的全自动数据表格组件.组件内部处理数据加载.分页.排序.查询.新增.编辑等一系列操作.让业务表格的开发从繁复的增删查改中脱离出来,编码简洁却又功能 ...

  10. css3实现梯形三角

    近期移动端项目中,图片很多 移动端尽量少图片,以便提升加载速度!这时候css3可以大放光芒比如梯形的背景图 --------------------------------- ------------ ...