Deno從零到架構級系列(二)——註解路由

image

上回介紹了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

image

TypeScript裝飾器配置

由於deno原本就支持TS,但用TS實現裝飾器,須要先配置。在根目錄新建配置文件tsconfig.json,配置文件以下:express

{
 "compilerOptions": {
   "allowJs": true,
   "module": "esnext",
   "emitDecoratorMetadata": true,
   "experimentalDecorators": true
 }
}

TS裝飾器

這裏提一下,註解和裝飾器是兩個東西,對於不一樣的語言來說,功能不一樣。json

  • 註解(Annotation):僅提供附加元數據支持,並不能實現任何操做。須要另外的 Scanner 根據元數據執行相應操做。
  • 裝飾器(Decorator):僅提供定義劫持,可以對類及其方法的定義並無提供任何附加元數據的功能。

我一直稱註解稱習慣了。你們理解就好。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

裝飾器修飾類class

裝飾器能夠修飾類,也能夠修飾方法。咱們先來看修飾類的例子,以下:框架

// 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。 這裏要注意三個點:

  • 運行命令:deno run -c tsconfig.json ./test.ts,這裏的-c是執行ts配置文件,注意是json文件
  • 外層工廠函數的執行順序:從上到下依次執行。
  • 裝飾器函數的執行順序:從下到上依次執行。

TS註解路由

好啦,下面咱們接着上一回的內容,正式改造註解路由了。oak和之前koa、express改造思路都同樣。改造以前,按照路由分發請求流程,以下圖:

image

改造以後,咱們的流程以下圖。

image

新建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()

好啦,整個類的裝飾器改造就結束了。整個項目目錄結構以下:

image

先不着急運行,雖然運行也會成功,但啥都作不了,爲啥呢? 由於類方法的路由尚未作,不賣關子了,接下來作類方法的裝飾器。

TS類方法的裝飾器

仍是先從代碼上來,先改造控制層,以下:

// 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)

到這裏,註解路由就改造完了。可是,這個時候請你們跳到彩蛋把導入文件的方法補上。而後一鼓作氣的運行入口文件,就大功告成了。

image

彩蛋

這裏的彩蛋部分,實際上是一個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導入迭代的文件。

解決Deno的bug

另外你們若是在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。飛狐在此用點特殊的手法解決了。嘿嘿~

image

下回預告

學會了TS裝飾器能夠作的不少,好比:請求參數註解、日誌、權限判斷等等。回顧一下,這篇的內容比較多,也比較深刻。你們能夠好好消化一下,歸納一下:

  • 裝飾者模式
  • TS類的裝飾器,TS類方法的裝飾器
  • 文件夾的導入,文件的引入

下回咱們講全局錯誤處理,借鑑alosaur作異常處理。有任何問題你們能夠在評論區留言~

Ta-ta for now ヾ( ̄▽ ̄)

相關文章
相關標籤/搜索