This article is about some records and thoughts of the previous development. At the beginning of learning, there will always be some miscalculations. Please give me more advice if you make mistakes.

TL; DR

Use decorators, such as ts.ed and Nest.js, to help you build object-oriented Node applications.

The hearse drift

If you are the legendary wuling driver, you may have seen such

// Spring.
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {
    @RequestMapping("/my-jj-burst-url")
    public String index(a) {
        return "Greetings from Spring Boot!"; }}Copy the code

Such as

// ASP.Net Core.
using Microsoft.AspNetCore.Mvc;

namespace MyAwesomeApp {
    [Route("/my-jj-burst-url")]
    public class HelloController: Controller {
        [HttpGet]
        public async Task<string> Index () {
            return "Greetings from ASP.Net Core!"; }}}Copy the code

Such as

# Flask.
from flask import Flask
app = Flask(__name__)

@app.route("/my-jj-burst-url")
def helloController(a):
    return "Greetings from Flask!"
Copy the code

So when you get a Node.js code like this

// Express.

// ./hello-router.js
app.get('/my-jj-burst-url'.require('./hello-controller'))

// ./hello-controller.js
module.exports = function helloController (req, res) {
    res.send('Greetings from Express! ')}Copy the code

Your inner OS is actually

// Express + TS.ED.

import { Request, Response } from 'express'
import { Controller, Get } from '@tsed/common'

@Controller('/my-jj-burst-url')
class HelloController {
    @Get('/')
    async greeting (req: Request, res: Response) {
        return 'Greetings from Express! '}}Copy the code

There are ways to easily build your application in Node.js in this form, and if you work with TypeScript, you can instantly regain the comfort of type safety.

After using this approach, you may need to build your application in an object-oriented manner.

Take an Express application as an example

Here’s a neat little Express app:

import * as Express from 'express'

const app = Express()

app.get('/'.(req: Express.Request, res: Express.Response) = > {
  res.send('Hello! ')
})

app.listen(3000.'0.0.0.0'.(error: Error) = > {
  if (error) {
    console.error('[Error] Failed to start server:', error)
    process.exit(1)}console.log('[Info] Server is on.')})Copy the code

Now we’ll turn it into OOP, modern Express.

This is written in TypeScript.

Use decorators

If you’re looking for objects in Node,A decoratorIs your effective beauty assistant, it can enhance your appearance level, so that you will not be female guests instantly put out the light.

Decorators give you the ability to do extra things with an object, which is very helpful for object-oriented programming:

/ / code source: http://es6.ruanyifeng.com/#docs/decorator

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true
Copy the code

Please make sure to open TS’s “experimentalDecorators”; Check out other articles on decorators.

Decorating a Server

We will decorate an Express program as a Class, and each time we start a server, we will use a new program (). The general effect should be:

// Import decorators from the module that implements them.
import { AppServer, Server } from './decorator.server'

// A Class that represents an Express application.
@Server({
  host: '0.0.0.0',
  port: 3000
})
class App extends AppServer {
  private onListen () {
    console.log('[Info] Server is on.')}private onError (error: Error) {
    console.error('[Error] Failed to start server:', error)
    process.exit(1)}}const app = new App()  / / ho ho.
console.log(app.app)  // Get the express. Application.
app.start()
Copy the code

So the decoration would look something like this:

// decorator.server.ts

import * as Express from 'express'

** @param {IServerOptions} options */
function Server (options: IServerOptions) {
  return function (Constructor: IConstructor) {
    // Create an express.application.
    const serverApp = Express()

    // Get the event function from prototype.
    const { onListen, onError } = Constructor.prototype

    // Get the Settings from the decorator parameter.
    const host = options.host || '0.0.0.0'
    const port = options.port || 3000

    Create the Start method.
    Constructor.prototype.start = function () {
      serverApp.listen(port, host, (error: Error) = > {
        if (error) {
          isFunction(onError) && onError(error)
          return
        }

        isFunction(onListen) && onListen()
      })
    }

    // Attach the App to the prototype.
    Constructor.prototype.app = serverApp

    return Constructor
  }
}

/** * Server interface definition. * Server-decorated classes will contain attributes on this type. ** explicit inheritance if needed. ** @class AppServer */
class AppServer {
  app: Express.Application
  start: (a)= > void
}

/** * Server decorator function parameter interface. ** @interface IServerOptions */
interfaceIServerOptions { host? :stringport? :number
}

** @param {*} target * @returns {Boolean} */
function isFunction (target: any) {
  return typeof target === 'function'
}

/** * class Constructor type definition. * stands for a Constructor
interface IConstructor {
  new(... args:any[]) :any
  [key: string] :any
}

export {
  Server,
  AppServer
}
Copy the code

As long as it works.

Decorate a Class

For controllers, we want a Class, and the method above Class is the controller method used for routing.

Methods are decorated with the Http Method decorator, specifying the route URL and Method.

It should look like this:

import { Request, Response } from 'express'
import { Controller, Get } from './decorator.controller'

@Controller('/hello')
class HelloController {
  @Get('/')
  async index (req: Request, res: Response) {
    res.send('Greetings from Hello Controller! ')}// add a new test function.
  @Get('/wow(/:name)? ')
  async doge (req: Request, res: Response) {
    const name = req.params.name || 'Doge'
    res.send(`
      <span>Wow</span>
      <br/>
      <span>Such a controller</span>
      <br/>
      <span>Very OOP</span>
      <br/>
      <span>Many decorators</span>
      <br/>
      <span>Good for you, ${name}! </span> `)}}export {
  HelloController
}
Copy the code

In this case, the decorator needs to record the incoming URL and the corresponding function and Http Method, which is then used by the @server.

// decorator.controller.ts /** * controller decorates a Class as an App controller Controller (url: string = ") {return function (Constructor: IConstructor) {// Save the Controller URL. Object.defineProperty(Constructor, '$CONTROLLER_URL', { enumerable: true, value: Return Constructor} /** * return Constructor} /** * return Constructor} /** @param {string} string = ''): any { return function (Constructor: IConstructor, name: string, descriptor: Const controllerFunc = Constructor[name] as (const controllerFunc = Constructor[name] as (... DefineProperty (controllerFunc, '$FUNC_URL', {enumerable: {args: any[]) => any true, value: url }) Object.defineProperty(controllerFunc, '$HTTP_METHOD', { enumerable: true, value: }} export {Controller, get} /** * Constructor {new (... args: any[]): any [key: string]: any }Copy the code

Go back and modify the Server decorator

Once the Controller is written, we want to introduce the Controller directly by specifying the file path, like this:

@Server({
  host: '0.0.0.0',
  port: 3000,
  controllers: [
    './controller.hello.ts'  // Specify the controller to use.]})class App extends AppServer {
  private onListen () {
    console.log('[Info] Server is on.')}private onError (error: Error) {
    console.error('[Error] Failed to start server:', error)
    process.exit(1)}}Copy the code

@server has an controllers: string[] attribute that specifies the imported controller file. Route initialization after file import is handled automatically by the program.

So we need to add two more sentences to @server:

** @param {IServerOptions} options */
function Server (options: IServerOptions) {
  return function (Constructor: IConstructor) {
    // Create an express.application.
    const serverApp = Express()

    // New logic:
    // Read the file from the directory specified by options.controllers and get the controller object.
    // Register the controller object with serverApp.
    const controllers = getControllers(options.controllers || [])
    controllers.forEach(Controller= > registerController(Controller, serverApp))

    // ...}}Copy the code

The two sentences will do something like this:

  • Read Controller Class from file;
  • Add Controller Class to Express deluxe lunch.
** @param {string[]} controllerFilesPath * @returns {IConstructor[]} */ function getControllers (controllerFilesPath: string[]): IConstructor[] { const controllerModules: IConstructor [] = [] controllerFilesPath. ForEach (filePath = > {/ / from the controller module is read from the file. The module may export multiple controllers, which will be traversed for registration. Const module = require(filePath) object.keys (module).foreach (funcName => {const controller = module[funcName] as IConstructor controllerModules.indexOf(controller) < 0 && controllerModules.push(controller) }) }) return ControllerModules} /** * Register controllerModules to serverApp. ** @param {IConstructor} Controller * @param {express.application} serverApp */ function registerController (Controller: IConstructor, serverApp: Express.application) {const router = express.router () // Registers functions under the controller. Object.getOwnPropertyNames(Controller.prototype) .filter(funcName => funcName ! == 'constructor') .map(funcName => Controller.prototype[funcName]) .forEach(func => { const url = func['$FUNC_URL'] as string const method = func['$HTTP_METHOD'] as string if (typeof url === 'string' && typeof method === 'string') { const matcher = (router as any)[method] as any // router.get, router.post, ... If (matcher) {this.matcher. Call (router, url, (req: express.request, res: Express.Response, next: Express.NextFunction) => { func(req, res, next) }) } } }) const controllerPath = Controller['$CONTROLLER_URL'] as string serverApp.use(controllerPath, router) }Copy the code

This is almost complete, run OK, screenshots will not go up 🐸

Middlewares

There are more things we can do, such as adding middleware:

@Controller('/bye')
class ByeController {
  @Auth(a)// Login request Only.
  @UseBefore(CheckCSRF)  // CSRF check.
  @Post('/')
  async index (req: Request, res: Response) {
    res.send('Good bye! ')}@Get(The '*')
  async redirect (req: Request, res: Response) {
    res.redirect('/bye')}}Copy the code

Or define a separate decorator for a commonly used middleware; Coupled with dependency injection and other features, the entire application is very handy to use.

Detailed logic is no longer an example, I see you old drivers have already started racing 🍺🐸

Wheels on the market

There are already similar wheels on the market:

  • Ts.ed: A set of TypeScript decorator components developed for Express that add common functionality to middleware and object-oriented design.

  • Nest.js: A new object-oriented node.js framework written in TypeScript that is very similar in functionality and style to TS.ed.

If you’re interested in modern development or an object-oriented approach, try these two projects.