上回介紹了Deno的基本安裝、使用。基於oak框架搭建了控制層、路由層、對入口文件進行了改造。那這回咱們接着繼續改造路由,模擬springmvc實現註解路由。javascript
裝飾者模式(decorator),就是給對象動態添加職責的方式稱爲裝飾者模式。直接先上例子:java
// 新建文件fox.ts // 建立一個fox類 class Fox { // skill方法,返回狐狸會跑的字樣,假設就是構建了狐狸類都會跑的技能 skill() { return '狐狸會跑。' } } // 建立一個flyingfox類 class Flyingfox { private fox: any // 構造方法,傳入要裝飾的對象 constructor(fox: any) { this.fox = fox; // 這裏直接打印該類的skill方法返回值 console.log(this.skill()) } // 該類的skill方法 skill() { // 在這裏獲取到了被裝飾者 let val = this.fox.skill(); // 這裏簡單的加字符串,假設給被裝飾者加上了新的技能 return val + '再加一對翅膀,就會飛啦!' } } // new一個fox對象 let fox = new Fox(); // 打印結果爲:狐狸會跑。再加一對翅膀,就會飛啦! new Flyingfox(fox);
直接運行deno run fox.ts就會打印結果啦。這是一個很是簡單的裝飾者模式例子,咱們繼續往下,用TS的註解來實現這個例子。spring
由於deno原本就支持TS,但用TS實現裝飾器,須要先配置。在根目錄新建配置文件tsconfig.json,配置文件以下:express
{ "compilerOptions": { "allowJs": true, "module": "esnext", "emitDecoratorMetadata": true, "experimentalDecorators": true } }
這裏提一下,註解和裝飾器是兩個東西,對於不一樣的語言來說,功能不一樣。json
我一直稱註解稱習慣了。你們理解就好。api
TypeScript裝飾器是一種函數,寫法:@ + 函數名。做用於類和類方法前定義。 仍是拿上面的例子來改寫,以下數組
@Flyingfox class Fox {} // 等同於 class Fox {} Fox = Flyingfox(Fox) || Fox;
不少小夥伴常常看到這樣的寫法,以下:mvc
function Flyingfox(...list) { return function (target: any) { Object.assign(target.prototype, ...list) } }
這樣在裝飾器外面再封裝一層函數,好處是便於傳參數。基本語法掌握了,咱們就來實戰一下,實戰中才知道更深層次的東東。app
裝飾器能夠修飾類,也能夠修飾方法。咱們先來看修飾類的例子,以下:框架
// test.ts // 定義一個Time方法 function Time(ms: string){ console.log('1-第一步') // 這裏的target就是你要修飾的那個類 return function(target: Function){ console.log(`4-第四步,${value}`) } } // 定義一個Controller方法,也是個工廠函數 function Controller(path: string) { console.log('2-第二步') return function(target: Function){ console.log(`3-第三步,${value}`) } } @Time('計算時間') @Controller('這是controller') class Controller { } // 運行:deno run -c tsconfig.json ./test.ts // 1-第一步 // 2-第二步 // 3-第三步, 這是controller // 4-第四步, 計算時間
有疑問的小夥伴能夠console出來看看這個target。 這裏要注意三個點:
好啦,下面咱們接着上一回的內容,正式改造註解路由了。oak和之前koa、express改造思路都同樣。改造以前,按照路由分發請求流程,以下圖:
改造以後,咱們的流程以下圖。
新建decorators文件夾,包含三個文件,以下:
// decorators/router.ts // 這裏統一引入oak框架 import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts' // 統一導出oak的app和router,這裏的其實能夠單獨放一個文件,由於還有入口文件server.ts會用到 export const app: Application = new Application(); export const router: Router = new Router(); // 路由前綴,這裏其實應該放到配置文件 const prefix: string = '/api' // 構建一個map,用來存放路由 const routeMap: Map<{target: any, method: string, path: string}, Function | Function[]> = new Map() // 這裏就是咱們做用於類的修飾器 export function Controller (root: string): Function { return (target: any) => { // 遍歷全部路由 for (let [conf, controller] of routeMap) { // 這裏是判斷若是類的路徑是@Controller('/'),不然就跟類方法上的路徑合併 conf.path = prefix + (root === '/' ? conf.path : `${root}${conf.path}`) // 強制controller爲數組 let controllers = Array.isArray(controller) ? controller : [controller] // 這裏是最關鍵的點,也就是分發路由 controllers.forEach((controller) => (router as any)[conf.method.toLowerCase()](conf.path, controller)) } } }
這裏就是類上的路由了,每一行我都加了註釋。給小夥伴們一個建議,哪裏不明白,就在哪裏console一下。 這裏用的Map來存放路由,其實用反射更好,只是原生的reflect支持比較少,須要額外引入reflect的文件。有興趣能夠去看alosaur框架的實現方式。
// decorators/index.ts export * from "./router.ts"; export * from "./controller.ts";
這個其實沒什麼好講的了,就是入口文件,把該文件夾下的文件導出。這裏的controller.ts先留個懸念,放到彩蛋講。 接着改造控制層,代碼以下:
// controller/bookController.ts import { Controller } from "../decorators/index.ts"; // 這裏咱們僞裝是業務層過來的數據 const bookService = new Map<string, any>(); bookService.set("1", { id: "1", title: "聽飛狐聊deno", author: "飛狐", }); // 這裏是類的裝飾器 @Controller('/book') export default class BookController { getbook (context: any) { context.response.body = Array.from(bookService.values()); } getbookById (context: any) { if (context.params && context.params.id && bookService.has(context.params.id)) { context.response.body = bookService.get(context.params.id); } } }
接着改造項目入口文件server.ts
// server.ts // 這裏的loadControllers先無論,彩蛋會講 import { app, router, loadControllers } from './decorators/index.ts' class Server { constructor () { this.init() } async init () { // 這裏就是導入全部的controller,這裏的controller是控制層文件夾的名稱 await loadControllers('controller'); app.use(router.routes()); app.use(router.allowedMethods()); this.listen() } async listen () { // await app.listen({ port: 8000 }); setTimeout(async () => { await app.listen({ port: 8000 }) }, 1); } } new Server()
好啦,整個類的裝飾器改造就結束了。整個項目目錄結構以下:
先不着急運行,雖然運行也會成功,但啥都作不了,爲啥呢? 由於類方法的路由尚未作,不賣關子了,接下來作類方法的裝飾器。
仍是先從代碼上來,先改造控制層,以下:
// controller/bookController.ts const bookService = new Map<string, any>(); bookService.set("1", { id: "1", title: "聽飛狐聊deno", author: "飛狐", }); @Controller('/book') export default class BookController { // 這裏就是類方法修飾器 @Get('/getbook') getbook (context: any) { context.response.body = Array.from(bookService.values()); } // 這裏就是類方法修飾器 @Get('/getbookById') getbookById (context: any) { if (context.params && context.params.id && bookService.has(context.params.id)) { context.response.body = bookService.get(context.params.id); } } }
類方法修飾器實現,這裏就只講解有改動的地方,以下:
// decorators/router.ts import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts' // 這裏是TS的枚舉 enum MethodType { GET='GET', POST='POST', PUT='PUT', DELETE='DELETE' } export const app: Application = new Application(); export const router: Router = new Router(); const prefix: string = '/api' const routeMap: Map<{target: any, method: string, path: string}, Function | Function[]> = new Map() export function Controller (root: string): Function { return (target: any) => { for (let [conf, controller] of routeMap) { conf.path = prefix + (root === '/' ? conf.path : `${root}${conf.path}`) let controllers = Array.isArray(controller) ? controller : [controller] controllers.forEach((controller) => (router as any)[conf.method.toLowerCase()](conf.path, controller)) } } } // 這裏就是http請求工廠函數,傳入的type就是http的get、post等 function httpMethodFactory (type: MethodType) { // path是類方法的路徑,如:@Get('getbook'),這個path就是指getbook。 // 類方法修飾器傳入三個參數,target是方法自己,key是屬性名 return (path: string) => (target: any, key: string, descriptor: any) => { // 第三個參數descriptor咱們這裏不用,可是仍是講解一下,對象的值以下: // { // value: specifiedFunction, // enumerable: false, // configurable: true, // writable: true // }; (routeMap as any).set({ target: target.constructor, method: type, path: path, }, target[key]) } } export const Get = httpMethodFactory(MethodType.GET) export const Post = httpMethodFactory(MethodType.POST) export const Delete = httpMethodFactory(MethodType.DELETE) export const Put = httpMethodFactory(MethodType.PUT)
到這裏,註解路由就改造完了。可是,這個時候請你們跳到彩蛋把導入文件的方法補上。而後一鼓作氣的運行入口文件,就大功告成了。
這裏的彩蛋部分,實際上是一個deno的導入文件方法,代碼以下:
// decorators/controller.ts export async function loadControllers (controllerPath: string) { try { for await (const dirEntry of Deno.readDirSync(controllerPath)) { import(`../${controllerPath}/${dirEntry.name}`); } } catch (error) { console.error(error) console.log("no such file or dir :---- " + controllerPath) } }
這裏的readDirSync就是讀取傳入的文件夾路徑,而後用import導入迭代的文件。
另外你們若是在1.2之前的版本遇到報錯以下:
Error: Another accept task is ongoing
不要着急,這個是deno的錯誤。解決方法以下:
async listen () { // await app.listen({ port: 8000 }); setTimeout(async () => { await app.listen({ port: 8000 }) }, 1); }
找到入口文件,在監聽端口方法加個setTimeout就能夠搞定了。以前deno官方的issue,不少人在提這個bug。飛狐在此用點特殊的手法解決了。嘿嘿~
學會了TS裝飾器能夠作的不少,好比:請求參數註解、日誌、權限判斷等等。回顧一下,這篇的內容比較多,也比較深刻。你們能夠好好消化一下,歸納一下:
下回咱們講全局錯誤處理,借鑑alosaur作異常處理。有任何問題你們能夠在評論區留言~
Ta-ta for now ヾ( ̄▽ ̄)