Xu Shuaiwu is the front engineer of Wedoctor Cloud service team. A front-end programming ape who loves to toss and cook

preface

Each time we define a route, after writing the Controller method, we have to define it again in the Router file, which is very tedious and troublesome.


This is tempting, but the cost of switching frames for a script is too high. Can we use this in Koa or Egg? Or is it possible to keep writing it as it is and progressively enhance the use of decorators to define routes? The answer is yes, the implementation of the decorator routing notation is actually very simple, you read and read.

Decoration method

A decorator is a syntactic sugar defined as an ordinary function that is called with the @ + function name. It can precede the definition of classes and class methods. Classes and class methods take different parameters.

Class decorator

Class decorators can be used to decorate the entire class:

@testable
class MyTestableClass {
  // ...
}

function testable(target) {  target.isTestable = true; }  MyTestableClass.isTestable // true  // This code is from ruan Yifeng ES6 tutorial Copy the code

The target argument passed in is the class itself, and if the decorator function has a return value, it replaces the class with the return value. The basic behavior is as follows:

@decorator
class A {}

/ / is equivalent to

class A {} A = decorator(A) || A; Copy the code

Class method decorator

The class method decorator is very much like the Object.defineProperty method, with three arguments:

  • targetThe: class object is the prototype object of the “class”. It has aconstructorThe property points to the class itself
  • name: The name of the decorator property
  • descriptor: Property descriptor, the sumObject.definePropertyThe approach is consistent

The specific properties and descriptions of property descriptors can be seen here, descriptor.

function foo(target, name, descriptor){
}
Copy the code

When a decorator needs to pass a parameter, we can create a higher-order function that returns a decorator function.

function foo (url) {
  return function (target, name, descriptor) {
    console.log(url)
  }
}
class Bar {  @foo('/test')  baz () {  } } Copy the code

Reflect

Reflect is a new API for manipulating objects in ES6, where we use the metadata-related API to bind routing data to objects, which is used to generate the final routing file. Reflect-metadata is a library that supports the API. For details, see here reflect-metadata. We mainly use the following two apis:

// Set metadata
Reflect.defineMetadata(metadataKey, metadataValue, target);
// Get the set value
let result = Reflect.getMetadata(metadataKey, target);
Copy the code

Implement decorator routing

Implementation approach

The end goal is to get the router configuration needed to generate a Koa or Egg. In this case, Koa requires a configuration similar to the following. So our goal is to get the following file. Reflect-metadata can write metadata, such as path and request type, to each method. Therefore, you only need to provide a unified registration method to register the path and function set by using decorator on the Router object, thus completing the automatic route registration process.

const app = new Koa();
const router = new Router();

router.get('/user/info', UserInfoController);
router.get('/user/list', UserListController);
router.post('/user/create', UserCreateController);  app.use(router.routes()) Copy the code

Controller

We define a Controller method to store the common path.

/ * ** Controller decorator* To decorate the Controller class *
* @param {string} [baseUrl= ""] class public prefix * @returns  * @memberof Decorator * / Controller (baseUrl = ' ') {  return (target) = > {  Reflect.defineMetadata(BASE_URL, baseUrl, target)  } } Copy the code

Basic HTTP methods

Because the koa-Router registers methods like Get and Post with the same parameters, the decorator can register a generic method to generate decorators for each method as follows:

/ * ** Utility functions for generating various method decorators *
 * @param {*} method
 * @memberof Decorator * / createMethodDecorator (method) {  return (url) = > {  return (target, name, decorator) = > {  // target is the class that decorates the method  // Because the class method decorator executes before the class decorator, the common prefix of the Controller class is not available at this stage  // Create a router based on the saved information  this.controllerList.add(target)  // decorator.value is the decorator function itself  Reflect.defineMetadata(METHOD, method, decorator.value)  // The function name is used as the URL without specifying the requested URL  Reflect.defineMetadata(METHOD_URL, url || name, decorator.value)  }  } } Copy the code

Generating a Router file

After we have routing information, we need to register all routing information to the Router object to complete the route registration. We traverse all controller classes stored and then obtain the corresponding routing information from the method for registration:

/ * ** Register routes *
* @param {*} router Koa Router object * @memberof Decorator * / registerRouter (router) {  for (const controller of this.controllerList) {  // Get the class constructor, which is the target argument in the class decorator  const controllerCtor = controller.constructor  const baseUrl = Reflect.getMetadata(BASE_URL, controllerCtor) || ' '   // Get all the attributes on the class object  const allProps = Object.getOwnPropertyNames(controller)  for (const props of allProps) {  const handle = controller[props]  // Traverses all functions whose attributes are functions with routing information  if (typeofhandle ! = ='function') { continue }  const method = Reflect.getMetadata(METHOD, handle)  const url = Reflect.getMetadata(METHOD_URL, handle)  if (method && url && router[method]) {  // Because it is demo, the format of each URL is not verified and converted  // In practice, you need to concatenate the three paths into a valid URL format  const completeUrl = this.prefix + baseUrl + url  // Register the interface path and functions with the Router object  router[method](completeUrl, handle)  }  }  } } Copy the code

Load all controllers

Finally, we need to load all the Controller files in. To avoid handwriting, we create a load function to automatically load all the Controller files.

Here we use requireContext, which is available by default with webpack as require.context(). If you are not using webpack, you need to manually import the require-Context NPM package to use it.

RequireContext This method takes three parameters: the directory to search for, whether to search for subfolders, and the regular expression to match files. Use this method to get all eligible modules.

import requireContext from 'require-context'

export const load = function (path) {
  const ctx = requireContext(path, true, /\.js$/)
  ctx.keys().forEach(key= > ctx(key))
}  // Use: pass in the controller function folder load(path.resolve(__dirname, './controller')) Copy the code

extension

Decorators are very powerful and can do more than just auto-register routes, such as route authentication, middleware, dependency injection, parameter verification, logging, and so on.

conclusion

To sum up, we have implemented a Koa Router decorator routing, Express, Egg and other frameworks, and the registerRouter function can be slightly modified according to the differences of route registration methods in different frameworks. Corresponding to the old project that has existed for a long time, we can also use this method to write the decorator for the new route, and customize the registerRouter to achieve the effect of gradual enhancement.

The code implemented in this article is here【 Koa – Decorator – Demo 】