文章爲在下之前開發時的一些記錄與當時的思考, 學習之初的內容總會有所考慮不周, 若是出錯還請多多指教.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 很是類似.
若是您對現代化開發或面向對象的方式很感興趣,不妨嘗試一下這兩個項目.