上回介绍了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装璜器是一种函数,写法:@ + 函数名。作用于类和类办法前定义。 还是拿下面的例子来改写,如下
@Flyingfoxclass 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.tsexport * from "./router.ts";export * from "./controller.ts";
这个其实没什么好讲的了,就是入口文件,把该文件夹下的文件导出。这里的controller.ts先留个悬念,放到彩蛋讲。 接着革新管制层,代码如下:
// controller/bookController.tsimport { 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.tsconst 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.tsimport { 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.tsexport 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 ヾ( ̄▽ ̄)