Before I talked about Koa2 from zero to scaffolding , and from shallow to deep understanding of Koa2 source code

This article explains how to handwrite a Koa2

Step 1: Encapsulate HTTP service and create Koa constructor

I read the source code of Koa2 before and learned that Koa's service application is based on Node's native HTTP module, which is encapsulated and formed. We first use native Node to implement HTTP service

 const http = require('http')

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

server.listen(3000, () => {
  console.log('监听3000端口')
})

Let's take another look at implementing HTTP services with Koa2

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

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

app.listen(3000, () => {
  console.log('3000请求成功')
})

The first step to implement Koa is to encapsulate the native HTTP service. We create a new lib/application.js file according to the structure of the Koa source code. 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请求成功')
})

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

Then we have two directions, one is to simplify res.writeHead(200)、res.end('Hello world') ; the other is to insert multiple middleware. To do the first point, you need to write the context, response, and request files first. To do the second point, you need to rely on the context later, so let's simplify the native response, request, and integrate it into the context (ctx) object

Step 2: Build request, response, and context objects

The request, response, and context objects correspond to request.js, response.js, and context.js respectively. request.js processes the request body, response.js processes 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 do the query processing in the request, and only do the body and status processing in the response. Whether it's a request or a response, we use ES6 get and set. Simply put, get/set is the ability to get and assign values to a key

Now that we have implemented request and response, and obtained the request and response objects and their encapsulation methods, let's write the context. We once said in the source code analysis that the context inherits the parameters of the request and response objects, including methods in the request body and methods in the response body. For example, ctx.query can query the parameters on the url in the request body, It can also return data through ctx.body.

 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 methods on context.request and context.response in the context are delegated to the context, that is, context.request.query === context.query; context.response.body === context.body . And context.request, context.response are 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 the response body into one object, and both are generated on the application, modify the application.js file , add the code as follows:

 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.context, this.request, and this.response respectively in the constructor. We implemented the context ctx, now we go back to the previous problem, shorthand res.writeHead(200)、res.end('Hello world')

How do we want to simplify ---6dfd875d3c423c470c84866237e65fb9 res.writeHead(200)、res.end('Hello world') to ctx.body = 'Hello world' ?

res.writeHead(200)、res.end('Hello world') is native, ctx.body = 'Hello world' is to use Koa, we want to ctx.body = 'Hello world' do parsed and converted into res.writeHead(200)、res.end('Hello world') . Fortunately, ctx has been obtained through createContext, then create a method to encapsulate res.end, which is represented by 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) => {
      // 把原生 req,res 封装为 ctx
      const ctx = this.createContext(req, res)
      // 执行 use 中的函数, ctx.body 赋值
      this.callbackFunc(ctx)
      // 封装 res.end,用 ctx.body 表示
      return this.responseBody(ctx)
    }
}
PS: Specific code: please see Step 2 in the warehouse

Step 3: Middleware mechanism and onion model

We know that the most important function in Koa2 is middleware. Its expression is that multiple uses can be used. The function in each use method is a middleware, which is passed to the next middleware through the second parameter next. ,E.g

 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)
})
// 结果 123456

So, our middleware is an array, and secondly, through next, execute and pause execution. Once next, the execution of this middleware is suspended and the next middleware is executed.

Koa's onion model is implemented by generator + co.js in Koa1, and implemented by async/await + Promise in Koa2. This time we also use async/await + Promise to achieve

In the source code analysis, we said that Koa2's middleware composition is an independent library, namely koa-compose, and 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 on the source code analysis. We will not explore it here.

Two solutions are posted here, in fact, they are both 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()
    }
}

Another is the source code. Regarding the compose function, the author has not been able to write a good reason. Readers, please understand by yourself.

Step 4: Error trapping and monitoring mechanism

How to capture the error code in the middleware, because the middleware returns a Promise instance, so we only need to catch the error handling, 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 are only doing error capture for the middleware part now, but if you write wrong code elsewhere, how do you know and notify the developer? Node provides a native module - events, and our Application class inherits it to get the listener. function, so that when there is an error on the server, it can be caught all

Summarize

We first read the source code of Koa2, and after knowing its data structure and usage, we gradually wrote one by hand. Special thanks to the first little tadpole for the analysis and implementation of the KOA2 framework principle. This article was written by me for Koa2. basis of the article. Speaking of Koa2, its function is very simple, that is, it handles the native req and res, so that developers can write code more easily; in addition, it introduces the concept of middleware, which is like a plug-in, which can be used after introduction. It can reduce the code when it is not needed. Lightweight is probably the keyword of Koa2.

GitHub address: https://github.com/johanazhu/jo-koa2

References


山头人汉波
394 声望555 粉丝