关于deno:Deno从零到架构级系列二注解路由

12次阅读

共计 6770 个字符,预计需要花费 17 分钟才能阅读完成。

上回介绍了 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('/'),否则就跟类办法上的门路合并
      let 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()](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) {let path = prefix + (root === '/' ? conf.path : `${root}${conf.path}`)
      let controllers = Array.isArray(controller) ? controller : [controller]
      controllers.forEach((controller) => (router as any)[conf.method.toLowerCase()](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 ヾ(~▽~)

正文完
 0