上回介紹了Deno的基本安裝、使用。基於oak框架搭建了控制層、路由層、對入口檔案進行了改造。那這回我們接著繼續改造路由,模擬springmvc實現註解路由。
裝飾器模式
裝飾者模式(decorator),就是給物件動態新增職責的方式稱為裝飾者模式。直接先上例子:
// 新建檔案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的註解來實現這個例子。
TypeScript裝飾器配置
因為deno本來就支援TS,但用TS實現裝飾器,需要先配置。在根目錄新建配置檔案tsconfig.json,配置檔案如下:
{
"compilerOptions": {
"allowJs": true,
"module": "esnext",
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
TS裝飾器
這裡提一下,註解和裝飾器是兩個東西,對於不同的語言來講,功能不同。
- 註解(Annotation):僅提供附加後設資料支援,並不能實現任何操作。需要另外的 Scanner 根據後設資料執行相應操作。
- 裝飾器(Decorator):僅提供定義劫持,能夠對類及其方法的定義並沒有提供任何附加後設資料的功能。
我一直稱註解稱習慣了。大家理解就好。
TypeScript裝飾器是一種函式,寫法:@ + 函式名。作用於類和類方法前定義。 還是拿上面的例子來改寫,如下
@Flyingfox
class Fox {}
// 等同於
class Fox {}
Fox = Flyingfox(Fox) || Fox;
很多小夥伴經常看到這樣的寫法,如下:
function Flyingfox(...list) {
return function (target: any) {
Object.assign(target.prototype, ...list)
}
}
這樣在裝飾器外面再封裝一層函式,好處是便於傳引數。基本語法掌握了,我們就來實戰一下,實戰中才知道更深層次的東東。
裝飾器修飾類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改造思路都一樣。改造之前,按照路由分發請求流程,如下圖:
改造之後,我們的流程如下圖。
新建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()
好啦,整個類的裝飾器改造就結束了。整個專案目錄結構如下:
先不著急執行,雖然執行也會成功,但啥都做不了,為啥呢? 因為類方法的路由還沒有做,不賣關子了,接下來做類方法的裝飾器。
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)
到這裡,註解路由就改造完了。但是,這個時候請大家跳到彩蛋把匯入檔案的方法補上。然後一氣呵成的執行入口檔案,就大功告成了。
彩蛋
這裡的彩蛋部分,其實是一個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。飛狐在此用點特殊的手法解決了。嘿嘿~
下回預告
學會了TS裝飾器可以做的很多,比如:請求引數註解、日誌、許可權判斷等等。回顧一下,這篇的內容比較多,也比較深入。大家可以好好消化一下,概括一下:
- 裝飾者模式
- TS類的裝飾器,TS類方法的裝飾器
- 資料夾的匯入,檔案的引入
下回我們講全域性錯誤處理,借鑑alosaur做異常處理。有任何問題大家可以在評論區留言~
Ta-ta for now ヾ( ̄▽ ̄)