文章为在下之前开发时的一些记录与当时的思考, 学习之初的内容总会有所考虑不周, 若是出错还请多多指教.javascript
使用装饰器,和诸如 TS.ED、Nest.js 来帮助您构建面向对象的 Node 应用.java
若是您就是传说中的秋名山五菱老司机,您可能已经见过诸如python
// Spring.
package hello;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class HelloController {
@RequestMapping("/my-jj-burst-url")
public String index() {
return "Greetings from Spring Boot!";
}
}
复制代码
诸如git
// ASP.Net Core.
using Microsoft.AspNetCore.Mvc;
namespace MyAwesomeApp {
[Route("/my-jj-burst-url")]
public class HelloController: Controller {
[HttpGet]
public async Task<string> Index () {
return "Greetings from ASP.Net Core!";
}
}
}
复制代码
诸如es6
# Flask.
from flask import Flask
app = Flask(__name__)
@app.route("/my-jj-burst-url")
def helloController():
return "Greetings from Flask!"
复制代码
所以当您拿到一个这样的 Node.js 代码时github
// Express.
// ./hello-router.js
app.get('/my-jj-burst-url', require('./hello-controller'))
// ./hello-controller.js
module.exports = function helloController (req, res) {
res.send('Greetings from Express!')
}
复制代码
您的心里 OS 实际是web
// Express + TS.ED.
import { Request, Response } from 'express'
import { Controller, Get } from '@tsed/common'
@Controller('/my-jj-burst-url')
class HelloController {
@Get('/')
async greeting (req: Request, res: Response) {
return 'Greetings from Express!'
}
}
复制代码
其实经过一些方式,能够很是方便地在 Node.js 中以这种形式构建您的应用,若是您再配合 TypeScript
,就能够瞬间找回类型安全带来的温馨感.spring
在使用这样的方式后,您可能须要以面向对象的方式来构建您的应用.typescript
这里有一个小巧精致的 Express 应用:express
import * as Express from 'express'
const app = Express()
app.get('/', (req: Express.Request, res: Express.Response) => {
res.send('Hello!')
})
app.listen(3000, '0.0.0.0', (error: Error) => {
if (error) {
console.error('[Error] Failed to start server:', error)
process.exit(1)
}
console.log('[Info] Server is on.')
})
复制代码
如今咱们将把它改形成 OOP、现代化的 Express.
这里使用 TypeScript 进行编写.
若是您打算在 Node 中找对象,装饰器是您得力的美颜助手,它能够提高您的颜值,使您不再会被女嘉宾瞬间灭灯.
装饰器将为您对某个对象作一些额外的事 ♂ 情,而这样的能力对面向对象编程是很是有帮助的:
// 代码引自: http://es6.ruanyifeng.com/#docs/decorator
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
复制代码
请您确认开启了 TS 的 "experimentalDecorators";关于装饰器的内容请您查阅其余文章.
咱们将把一个 Express 程序装饰为一个 Class,每启动一个服务器就 new 程序()
便可,大体效果不出意外应该是:
// 从实现了装饰器的模块引入装饰器.
import { AppServer, Server } from './decorator.server'
// 一个表明 Express 应用的 Class.
@Server({
host: '0.0.0.0',
port: 3000
})
class App extends AppServer {
private onListen () {
console.log('[Info] Server is on.')
}
private onError (error: Error) {
console.error('[Error] Failed to start server:', error)
process.exit(1)
}
}
const app = new App() // 嚯嚯.
console.log(app.app) // 还要能获取到 Express.Application, 这是坠吼的.
app.start()
复制代码
那么装饰器的话,大体搞成这副丑样:
// decorator.server.ts
import * as Express from 'express'
/** * Server 装饰器. * 将一个 Class 转换为 Express.Application 封装类. * * @param {IServerOptions} options */
function Server (options: IServerOptions) {
return function (Constructor: IConstructor) {
// 建立一个 Express.Application.
const serverApp = Express()
// 从 prototype 上获取事件函数.
const { onListen, onError } = Constructor.prototype
// 从装饰器参数获取设置.
const host = options.host || '0.0.0.0'
const port = options.port || 3000
// 建立 Start 方法.
Constructor.prototype.start = function () {
serverApp.listen(port, host, (error: Error) => {
if (error) {
isFunction(onError) && onError(error)
return
}
isFunction(onListen) && onListen()
})
}
// 将 App 挂在至原型.
Constructor.prototype.app = serverApp
return Constructor
}
}
/** * Server 接口定义. * 通过 Server 装饰的 Class 将包含此类型上的属性. * 若需使用则须要显式继承. * * @class AppServer */
class AppServer {
app: Express.Application
start: () => void
}
/** * Server 装饰器函数参数接口. * * @interface IServerOptions */
interface IServerOptions {
host?: string
port?: number
}
/** * 目标是否为函数. * * @param {*} target * @returns {boolean} */
function isFunction (target: any) {
return typeof target === 'function'
}
/** * "类构造函数" 类型定义. * 表明一个 Constructor. */
interface IConstructor {
new (...args: any[]): any
[key: string]: any
}
export {
Server,
AppServer
}
复制代码
能用就行.
控制器的话,咱们但愿是一个 Class,Class 上面的方法即为路由所使用的控制器方法.
方法由 Http Method 装饰器进行装饰,注明路由 URL 与 Method.
使用起来应该长这样:
import { Request, Response } from 'express'
import { Controller, Get } from './decorator.controller'
@Controller('/hello')
class HelloController {
@Get('/')
async index (req: Request, res: Response) {
res.send('Greetings from Hello Controller!')
}
// 加入了一个新的测试函数.
@Get('/wow(/:name)?')
async doge (req: Request, res: Response) {
const name = req.params.name || 'Doge'
res.send(` <span>Wow</span> <br/> <span>Such a controller</span> <br/> <span>Very OOP</span> <br/> <span>Many decorators</span> <br/> <span>Good for you, ${name}!</span> `)
}
}
export {
HelloController
}
复制代码
这样的话,装饰器须要记录传入的 URL 和对应的函数与 Http Method 便可,而后被 @Server
所使用便可.
// decorator.controller.ts
/**
* Controller 装饰器.
* 将一个 Class 装饰为 App 控制器.
*
* @param {string} url
* @returns
*/
function Controller (url: string = '') {
return function (Constructor: IConstructor) {
// 将控制器的 Url 进行保存.
Object.defineProperty(Constructor, '$CONTROLLER_URL', {
enumerable: true,
value: url
})
return Constructor
}
}
/**
* Http Get 方法装饰器.
*
* @param {string} url
* @returns {*}
*/
function Get (url: string = ''): any {
return function (Constructor: IConstructor, name: string, descriptor: PropertyDescriptor) {
// 将 URL 和 Http Method 注册至函数.
const controllerFunc = Constructor[name] as (...args: any[]) => any
// 保存信息, 方法上注册的 url 与 http method.
Object.defineProperty(controllerFunc, '$FUNC_URL', {
enumerable: true,
value: url
})
Object.defineProperty(controllerFunc, '$HTTP_METHOD', {
enumerable: true,
value: 'get'
})
}
}
export {
Controller,
Get
}
/**
* "类构造函数" 类型定义.
* 表明一个 Constructor.
*/
interface IConstructor {
new (...args: any[]): any
[key: string]: any
}
复制代码
编写好 Controller 后,咱们但愿能经过指定文件路径直接将 Controller 引入,像这样:
@Server({
host: '0.0.0.0',
port: 3000,
controllers: [
'./controller.hello.ts' // 指定须要使用的控制器.
]
})
class App extends AppServer {
private onListen () {
console.log('[Info] Server is on.')
}
private onError (error: Error) {
console.error('[Error] Failed to start server:', error)
process.exit(1)
}
}
复制代码
@Server
多了一个 controllers: string[]
属性,用于指定引入的控制器文件;文件引入后的路由初始化由程序自动处理, 兹不兹词?
所以咱们须要对 @Server
多加两句话:
/** * Server 装饰器. * 将一个 Class 转换为 Express.Application 封装类. * * @param {IServerOptions} options */
function Server (options: IServerOptions) {
return function (Constructor: IConstructor) {
// 建立一个 Express.Application.
const serverApp = Express()
// 新的逻辑:
// 从 options.controllers 指定的目录中读取文件并获取控制器对象.
// 并将控制器对象注册至 serverApp.
const controllers = getControllers(options.controllers || [])
controllers.forEach(Controller => registerController(Controller, serverApp))
// ...
}
}
复制代码
两句话的做用大概是:
/**
* 从文件地址读取控制器文件并返回控制器对象的数组.
*
* @param {string[]} controllerFilesPath
* @returns {IConstructor[]}
*/
function getControllers (controllerFilesPath: string[]): IConstructor[] {
const controllerModules: IConstructor[] = []
controllerFilesPath.forEach(filePath => {
// 从控制器文件中读取模块. 模块可能会导出多个控制器, 将进行遍历注册.
// 假设这里的路径是安全的, 例子嘛.
const module = require(filePath)
Object.keys(module).forEach(funcName => {
const controller = module[funcName] as IConstructor
controllerModules.indexOf(controller) < 0 && controllerModules.push(controller)
})
})
return controllerModules
}
/**
* 注册控制器子路由模块至 serverApp.
*
* @param {IConstructor} Controller
* @param {Express.Application} serverApp
*/
function registerController (Controller: IConstructor, serverApp: Express.Application) {
// 建立控制器的子路由模块.
const router = Express.Router()
// 将控制器下的函数进行注册.
Object.getOwnPropertyNames(Controller.prototype)
.filter(funcName => funcName !== 'constructor')
.map(funcName => Controller.prototype[funcName])
.forEach(func => {
const url = func['$FUNC_URL'] as string
const method = func['$HTTP_METHOD'] as string
if (typeof url === 'string' && typeof method === 'string') {
const matcher = (router as any)[method] as any // router.get, router.post, ...
if (matcher) {
// 这里用 call 从新指向 router, Express 中的代码用到了 this.
matcher.call(router, url, (req: Express.Request, res: Express.Response, next: Express.NextFunction) => {
func(req, res, next)
})
}
}
})
const controllerPath = Controller['$CONTROLLER_URL'] as string
serverApp.use(controllerPath, router)
}
复制代码
这样就差很少齐了,运行一下 OK,截图就不上了 🐸
实际上咱们能够作更多的东西,好比加入中间件兹词:
@Controller('/bye')
class ByeController {
@Auth() // 登录请求 Only.
@UseBefore(CheckCSRF) // CSRF 检查.
@Post('/')
async index (req: Request, res: Response) {
res.send('Good bye!')
}
@Get('*')
async redirect (req: Request, res: Response) {
res.redirect('/bye')
}
}
复制代码
或者单独为一个经常使用的中间件定义一个装饰器;再加上依赖注入等功能,让整个应用用起来十分驾轻就熟.
详细逻辑再也不举例,我看各位老司机已经开始飙车了 🍺🐸
目前市面上已经有相似的轮子出现:
TS.ED:一套针对 Express 开发的 TypeScript 装饰器组件,加入了常见功能的中间件与面向对象设计.
Nest.js:一套使用 TypeScript 编写的全新的面向对象设计的 Node.js 框架,功能和风格与 TS.ED 很是类似.
若是您对现代化开发或面向对象的方式很感兴趣,不妨尝试一下这两个项目.