Deno from zero to architecture level series - annotation routing

The last time I introduced the basic installation and use of Deno. Based on the oak framework, the control layer and routing layer are built, and the entry file is modified. Then this time, we will continue to transform the routing and simulate springmvc to implement annotation routing.

Decorator pattern

The decorator pattern (decorator), which is the way to dynamically add responsibilities to an object, is called the decorator pattern. Directly on the first example:

// New file fox.ts
// Create a fox class
class Fox {
 // The skill method returns the words that foxes can run, assuming that the skills that foxes can run are built
 skill() {
   return 'Fox will run.'
 }
}
// Create a flyingfox class
class Flyingfox  {
   private fox: any  
   // Constructor, passing in the object to be decorated
   constructor(fox: any) {
     this.fox = fox;
     // Here, the return value of the skill method of this class is directly printed
     console.log(this.skill())
   }
   // The skill method of this class
   skill() {
     // The decorated person is obtained here
     let val = this.fox.skill();
     // Here is a simple string, assuming that a new skill is added to the decorated person
     return val + 'Add a pair of wings and you're ready to fly!'
   }
}
// new a fox object
let fox = new Fox();

// The print result is: the fox will run. Add a pair of wings and you're ready to fly!
new Flyingfox(fox);

Running deno run fox.ts directly will print the result. This is a very simple example of the decorator pattern, we will continue to use TS annotations to implement this example.

TypeScript decorator configuration

Because deno already supports TS, but to implement decorator with TS, you need to configure it first. Create a new configuration file tsconfig.json in the root directory, the configuration file is as follows:

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

TS decorator

It is mentioned here that annotations and decorators are two things, and they have different functions for different languages.

  • Annotation: It only provides additional metadata support and does not implement any operations. An additional Scanner is required to perform the corresponding action based on the metadata.
  • Decorator: It only provides definition hijacking, and can define classes and their methods without providing any additional metadata.

I've always called the annotations used to it. It's good for everyone to understand.

A TypeScript decorator is a function, written as: @ + function name. Defined before acting on classes and class methods. Or take the above example to rewrite, as follows

@Flyingfox
class Fox {}

// Equivalent to
class Fox {}
Fox = Flyingfox(Fox) || Fox;

Many friends often see this way of writing, as follows:

function Flyingfox(...list) {
  return function (target: any) {
    Object.assign(target.prototype, ...list)
  }
}

In this way, another layer of functions is encapsulated outside the decorator, and the advantage is that it is easy to pass parameters. After mastering the basic grammar, let's take a look at the actual combat, and only know the deeper stuff in the actual combat.

Decorator decorates class

Decorators can decorate classes as well as methods. Let's first look at an example of a modifier class, as follows:

// test.ts
// Define a Time method
function Time(ms: string){
  console.log('1-first step')
  // The target here is the class you want to modify
  return function(target: Function){
    console.log(`4-the fourth step, ${value}`)
  }
}
// Define a Controller method, which is also a factory function
function Controller(path: string) {
  console.log('2-second step')
  return function(target: Function){
    console.log(`3-third step, ${value}`)
  }
}

@Time('calculating time')
@Controller('This is controller')
class Controller {
}
// Run: deno run -c tsconfig.json ./test.ts
// 1- The first step
// 2- Second step
// 3-The third step, this is the controller
// 4- The fourth step, calculating the time

If you have any doubts, you can console out to see this target. Three points to note here:

  • Run the command: deno run -c tsconfig.json ./test.ts, where -c is to execute the ts configuration file, pay attention to the json file
  • Execution order of outer factory functions: Execute sequentially from top to bottom.
  • Execution order of decorator functions: Execute sequentially from bottom to top.

TS Annotated Routing

Okay, let's continue the content of the last time and officially transform the annotation routing. oak has the same idea of ​​​​renovation as koa and express before. Before the transformation, follow the routing distribution request process, as shown below:

After the transformation, our process is as shown below.

Create a new decorators folder, containing three files, as follows:

// decorators/router.ts
// Here, the oak framework is uniformly introduced
import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts'
// Unified export of oak app and router. In fact, you can put a separate file here, because there is also an entry file server.ts that will be used
export const app: Application = new Application();
export const router: Router  = new Router();
// The routing prefix should actually be placed in the configuration file here
const prefix: string = '/api'
// Build a map to store routes
const routeMap: Map<{target: any, method: string, path: string}, Function | Function[]> = new Map()

// Here is the decorator we apply to the class
export function Controller (root: string): Function {
  return (target: any) => {
    // traverse all routes
    for (let [conf, controller] of routeMap) {
      // Here is to judge if the path of the class is @Controller('/'), otherwise it is merged with the path on the class method
      let path = prefix + (root === '/' ? conf.path : `${root}${conf.path}`)
      // Force controller to be an array
      let controllers = Array.isArray(controller) ? controller : [controller]
      // Here is the most critical point, which is the distribution route
      controllers.forEach((controller) => (router as any)[conf.method.toLowerCase()](path, controller))
    }
  }
}

Here is the route on the class, and I have commented each line. Give your friends a suggestion, if you don't understand, just console it. The Map used here is used to store routes. In fact, reflection is better, but the native reflect support is relatively small, and additional reflect files need to be introduced. If you are interested, you can see the implementation of the alosaur framework.

// decorators/index.ts
export * from "./router.ts";
export * from "./controller.ts";

This is actually nothing to talk about, it is the entry file, and the files under the folder are exported. The controller.ts here will leave a suspense first and put it on the easter egg. Then transform the control layer, the code is as follows:

// controller/bookController.ts
import { Controller } from "../decorators/index.ts";
// Here we pretend to be the data from the business layer
const bookService = new Map<string, any>();
bookService.set("1", {
  id: "1",
  title: "Listen to flying fox chat deno",
  author: "flying fox",
});

// Here is the class decorator
@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);
    }
  }
}

Then transform the project entry file server.ts

// server.ts
// I don't care about loadControllers here, the easter eggs will talk
import { app, router, loadControllers } from './decorators/index.ts'

class Server {
  constructor () {
    this.init()
  }

  async init () {
    // Here is to import all controllers, where controller is the name of the control layer folder
    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()

Well, the decorator transformation of the entire class is over. The entire project directory structure is as follows:

Don't rush to run first, although the operation will be successful, but nothing can be done, why? Because the routing of the class method has not been done, it will not be sold, and then the decorator of the class method will be done.

Decorator for TS class methods

Let’s start with the code first and transform the control layer first, as follows:

// controller/bookController.ts
const bookService = new Map<string, any>();
bookService.set("1", {
  id: "1",
  title: "Listen to flying fox chat deno",
  author: "flying fox",
});

@Controller('/book')
export default class BookController {
  // Here is the class method decorator
  @Get('/getbook')
  getbook (context: any) {
    context.response.body = Array.from(bookService.values());
  }
  // Here is the class method decorator
  @Get('/getbookById')
  getbookById (context: any) {
    if (context.params && context.params.id && bookService.has(context.params.id)) {
      context.response.body = bookService.get(context.params.id);
    }
  }
}

The class method decorator is implemented, and only the changes are explained here, as follows:

// decorators/router.ts
import { Application, Router } from 'https://deno.land/x/oak@v6.0.1/mod.ts'
// Here is the enumeration of 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))
    }
  }
}
// Here is the http request factory function, the incoming type is http get, post, etc.
function httpMethodFactory (type: MethodType) {
  // path is the path of the class method, such as: @Get('getbook'), this path refers to getbook.
  // The class method decorator passes in three parameters, the target is the method itself, and the key is the property name
  return (path: string) => (target: any, key: string, descriptor: any) => {
    // We don't need the third parameter descriptor here, but let's explain it. The value of the object is as follows:
    // {
    //   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)

At this point, the annotation routing has been transformed. However, at this time, please jump to the easter egg to fill in the method of importing the file. Then run the entry file in one go, and you're done.

Easter eggs

The egg part here is actually a deno import file method. The code is as follows:

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

The readDirSync here is to read the incoming folder path, and then use import to import the iterated files.

Fix Deno's bug

In addition, if you encounter errors in versions before 1.2, the following errors are reported:

Error: Another accept task is ongoing

Don't worry, this is deno's mistake. The workaround is as follows:

async listen () {
  // await app.listen({ port: 8000 });
  setTimeout(async () => {
    await app.listen({ port: 8000 })
  }, 1);
}

Find the entry file, add a setTimeout to the listening port method, and you can do it. In the previous deno official issue, many people were mentioning this bug. Flying Fox solved it with a special technique. hehe~

next notice

Learned that the TS decorator can do a lot, such as: request parameter annotation, log, permission judgment, etc. Looking back, this article has more content and is more in-depth. You can digest it well and summarize:

  • decorator pattern
  • TS class decorator, TS class method decorator
  • Folder import, file import

Next time, we will talk about global error handling and learn from alosaur for exception handling. If you have any questions, you can leave a message in the comment area~

Ta-ta for now ヾ( ̄▽ ̄)

Tags: node.js TypeScript deno

Posted by Brink Kale on Mon, 23 May 2022 23:33:12 +0300