Step by step: handwritten Koa2

As I said before Koa2 from zero to scaffold , and From shallow to deep understanding of Koa2 source code

This article explains how to write a Koa2

Step 1: encapsulate HTTP service and create Koa constructor

After reading the source code of Koa2, we learned that Koa's service application is based on the Node's native HTTP module, which is encapsulated. First, we use the native Node to implement the HTTP service

const http = require('http')

const server = http.createServer((req, res) => {
  res.writeHead(200)
  res.end('hello world')
})

server.listen(3000, () => {
  console.log('Monitor 3000 port')
})

Let's see how to implement HTTP service with Koa2

const Koa = require('Koa')
const app = new Koa()

app.use((ctx, next) => {
  ctx.body = 'hello world'
})

app.listen(3000, () => {
  console.log('3000 Request succeeded')
})

The first step to implement Koa is to encapsulate the native HTTP service. We create a new lib / application. Exe according to the structure of Koa source code JS file, the code is as follows:

const http = require('http')

class Application {
  constructor() {
    this.callbackFunc
  }
  listen(port) {
    const server = http.createServer(this.callback())
    server.listen(port)
  }
  use(fn) {
    this.callbackFunc = fn
  }
  callback() {
    return (req, res) => this.callbackFunc(req, res)
  }
}

module.exports = Application

We introduce handwritten Koa and write a demo

const Koa = require('./lib/application')

const app = new Koa()

app.use((req, res) => {
  res.writeHead(200)
  res.end('hello world')
})

app.listen(3000, () => {
  console.log('3000 Request succeeded')
})

After starting the service, enter in the browser http://localhost:3000 , the content displays "Hello, World"“

Then we have two directions: one is to simplify res.writeHead(200) and res.end('Hello world '); The second is to plug in multiple middleware. To make the first point, you need to write the context, response and request files first. In the second point, we need to rely on context later, so we first simplify the native response and request and integrate them into the context (ctx) object

Step 2: build request, response and context objects

Request, response and context objects correspond to request js,response.js,context.js,request.js processing request body, response JS handles the response body, and context integrates request and response

// request
let url = require('url')
module.exports = {
  get query() {
    return url.parse(this.req.url, true).query
  },
}
// response
module.exporrs = {
  get body() {
    return this._body
  },
  set body(data) {
    this._body = data
  },
  get status() {
    return this.res.statusCode
  },
  set status(statusCode) {
    if (typeof statusCode !== 'number') {
      throw new Error('statusCode must be a number')
    }
    this.res.statusCode = statusCode
  },
}

Here, we only deal with query in request and body and status in response. Whether it is a request or a response, we use the get and set of ES6. In short, get/set is able to value and assign a key

Now that we have implemented request and response and obtained the request and response objects and their encapsulation methods, let's write context. We once said in the source code analysis that context inherits the parameters of request and response objects, including both the methods in the request body and the methods in the response body, such as CTX Query queries the parameters on the url in the request body through CTX Body returns data.

module.exports = {
  get query() {
    return this.request.query
  },
  get body() {
    return this.response.body
  },
  set body(data) {
    this.response.body = data
  },
  get status() {
    return this.response.status
  },
  set status(statusCode) {
    this.response.status = statusCode
  },
}

delegate is used in the source code, and the context in the context request,context. The method on response is proxied to the context, that is, context request. query === context. query; context.response. body === context. body. And context request,context. The response is mounted in the application

To sum up: request JS is responsible for simplifying the code of the request body, response JS is responsible for simplifying the code of the response body, context JS integrates the request body and response body into one object, generates them on the application, and modifies the application JS file, add the following code:

const http = require('http');
const context = require('context')
const request = require('request')
const response = require('response')
class Application {
    constructor() {
        this.callbackFunc
          this.context = context
        this.request = request
        this.response = response
    }
    ...
    createConext(req, res) {
        const ctx = Object.create(this.context)
        ctx.request = Object.create(this.request)
        ctx.response = Object.create(this.response)
        ctx.req = ctx.request.req = req
        ctx.res = ctx.response.res = res
        return ctx
    }
    ...
}

Because context, request and response are used in other methods, we assign them to this in the constructor context,this.request,this.response . We have implemented the context ctx. Now let's return to the previous problem, which is abbreviated as res.writeHead(200), res.end('Hello world ')

We want to simplify res.writeHead(200) and res.end('Hello world ') to CTX Body = 'hello world', how to change it?

res.writeHead(200) and res.end('Hello world ') are native, ctx Body = 'hello world' is how Koa is used. We need to use ctx Body = 'hello world' is parsed and converted into res.writeHead(200) and res.end('Hello world '). Fortunately, ctx has been obtained through createContext, so create another method to encapsulate res.end with ctx Body

  responseBody(ctx) {
    let context = ctx.body
    if (typeof context === 'string') {
      ctx.res.end(context)
    } else if (typeof context === 'object') {
      ctx.res.end(JSON.stringify(context))
    }
  }

Finally, we modify the callback method

//   callback() {
//     return (req, res) => this.callbackFunc(req, res)
//   }
callback() {
    return (req, res) => {
      // Encapsulate the native req and res as ctx
      const ctx = this.createContext(req, res)
      // Execute the function in use, CTX Body assignment
      this.callbackFunc(ctx)
      // Encapsulate res.end with CTX Body representation
      return this.responseBody(ctx)
    }
}

PS: specific code: see Step 2 in the warehouse

Step 3: middleware mechanism and onion model

As we know, the most important function of Koa2 is middleware, which can be expressed in multiple uses. The function in each use method is a middleware, which is expressed and passed to the next intermediate by the second parameter next, for example

app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(6)
})

app.use(async (ctx, next) => {
  console.log(2)
  await next()
  console.log(5)
})

app.use(async (ctx, next) => {
  console.log(3)
  ctx.body = 'hello world'
  console.log(4)
})
// Result 123456

Therefore, our middleware is an array. Secondly, execute and pause execution through next. As soon as next, suspend the execution of this middleware and execute the next middleware.

Koa's onion model is implemented in Koa1 with generator + co.js, while Koa2 uses async/await + Promise. This time, we also use async/await + Promise to implement it

In the source code analysis, we said that the middleware synthesis of Koa2 is an independent library, namely koa compose. Its core code is as follows:

function compose(middleware) {
  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

The specific interpretation can be viewed from the source code analysis. We don't explore it here

The two solutions posted here are actually recursive

componse() {
    return async (ctx) => {
      function createNext(middleware, oldNext) {
        return async () => {
          await middleware(ctx, oldNext)
        }
      }
      let len = this.middlewares.length
      let next = async () => {
        return Promise.resolve()
      }
      for (let i = len - 1; i >= 0; i--) {
        let currentMiddleware = this.middlewares[i]
        next = createNext(currentMiddleware, next)
      }
      await next()
    }
}

There is also the source code. The author can't write a good description of the compose function. However, readers should understand it by themselves

Step 4: error capture and monitoring mechanism

How to capture the error code in the middleware? Because the middleware returns the Promise instance, we only need to handle the catch error and add the onerror method

onerror(err, ctx) {
    if (err.code === 'ENOENT') {
      ctx.status = 404
    } else {
      ctx.status = 500
    }
    let msg = ctx.message || 'Internal error'
    ctx.res.end(msg)
    this.emit('error', err)
}
callback() {
    return (req, res) => {
      const ctx = this.createContext(req, res)
      const respond = () => this.responseBody(ctx)
      + const onerror = (err) => this.onerror(err, ctx)
      let fn = this.componse()
      + return fn(ctx).then(respond).catch(onerror)
    }
}

We have only captured the errors in the middleware, but how can we know and notify the developers if the wrong code is written in other places? Node provides a native module - events, and our Application class can inherit it to obtain the monitoring function, so that we can catch all the errors on the server

summary

We first read the source code of Koa2, knew its data structure and usage, and then gradually handwritten one. Thank you very much here First little tadpole Koa2 framework principle analysis and implementation, his article is the basis for me to write koa2 article. Back to koa2, its function is particularly simple, that is, it processes the native req and res, so that developers can write code more easily; In addition, the concept of middleware is introduced, which is like a plug-in. It can be used when it is introduced. It can reduce code when it is not needed. Lightweight is probably the keyword of koa2

GitHub address: https://github.com/johanazhu/...

reference material

Tags: Javascript node.js Front-end koa2

Posted by pakmannen on Wed, 06 Apr 2022 05:04:35 +0300