Warehouse: mini – koa

Hello, I’m Shanyue.

Koa’s source code is easy to understand, with just four files, and Koa is one of the most popular server-side frameworks with a surprisingly high number of downloads. Koa is also the library or framework I most recommend reading source code.

Here Yamatsuki implements a minimal Koa using forty lines of code.

Mountain month code implementation

Put the code in shfshanyue/mini-code:code/koa

Can read the source code directly, basically every line has comments.

Use NPM run Example or Node example to run the sample code

$ npm run example
Copy the code

Demonstrations and Examples

Here is a simplified and classic example of koA’s core functionality, the Onion model:

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

app.use(async (ctx, next) => {
  console.log('Middleware 1 Start')
  await next()
  console.log('Middleware 1 End')
})

app.use(async (ctx, next) => {
  console.log('Middleware 2 Start')
  await next()
  console.log('Middleware 2 End')

  ctx.body = 'hello, world'
})

app.listen(3000)

// When accessing any route, the following information is printed on the terminal:
// Middleware 1 Start
// Middleware 2 Start
// Middleware 2 End
// Middleware 1 End
Copy the code

In the simplest example, you can see that there are three clear modules, as follows:

  • Application: Basic server framework
  • Context: The wrapper around the server framework’s various basic data structures for HTTP request parsing and response.
  • Middleware: Middleware and the central mechanism of the Onion model

We started implementing these three modules step by step:

Forget the framework and write a native server

Let’s start an HTTP service based on Node’s most basic HTTP API and use it to implement the simplest version of KOA, as shown in the following example:

const http = require('http')

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

server.listen(3000)
Copy the code

Here is an example of the simplest version of KOA, which I have named KoA-Mini

const Koa = require('koa-mini')
const app = new Koa()

app.use(async (ctx, next) => {
  console.log('Middleware 1 Start')
  await next()
  console.log('Middleware 1 End')
})

app.use(async (ctx, next) => {
  console.log('Middleware 2 Start')
  await next()
  console.log('Middleware 2 End')

  ctx.body = 'hello, world'
})

app.listen(3000)
Copy the code

From the code above, you can see that there are two core apis to implement:

  1. new Koa: build Appliaction
  2. app.use/ctx: Build middleware registration functions and Context

Build the Application

Start with the Appliacation framework:

  1. app.listen: handles requests and responses, and listens on ports
  2. app.use: Middleware registration function, currently processing the request and completing the response

A simple dozen or so lines of code, as shown below:

const http = require('http')

class Application {
  constructor () {
    this.middleware = null} listen (... args) {const server = http.createServer(this.middleware) server.listen(... args) }// The native http.createserver callback is still called
  use (middleware) {
    this.middleware = middleware
  }
}
Copy the code

The code for calling Application to start the service is as follows:

const app = new Appliacation()

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

app.listen(3000)
Copy the code

Since the app.use callback is still the native HTTP. crateServer callback, in KOA the callback argument is a Context object.

The next step will be to build the Context object.

Building the Context

In KOA, app.use’s callback argument is a CTX object, not the native REq/RES. So in this step you build a Context object and use ctx.body to build the response:

  • app.use(ctx => ctx.body = 'hello, world'): by thehttp.createServerFurther encapsulation in the callback functionContextimplementation
  • Context(req, res)To:request/responseThe data structure constructs the Context object for the subject

The core code is as follows, note the comment part:

const http = require('http')

class Application {
  constructor () {} use () {} listen (... args) {const server = http.createServer((req, res) = > {
      // Construct the Context object
      const ctx = new Context(req, res)

      // The app.use function of the koA-compatible Context is handled
      this.middleware(ctx)

      // ctx.body is the response contentctx.res.end(ctx.body) }) server.listen(... args) } }// Construct a Context class
class Context {
  constructor (req, res) {
    this.req = req
    this.res = res
  }
}
Copy the code

At this point, KOA is modified as follows and app.use can work normally:

const app = new Application()

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

app.listen(7000)
Copy the code

All of the above code is simple, leaving only one of the most important and core features: the Onion model

Onion model and middleware transformation

The above work is only a simple middleware, but in reality there will be many middleware, such as error handling, permission verification, routing, logging, traffic limiting and so on.

So we’ll modify app.middlewares to make it an array:

  • app.middlewares: Collects an array of middleware callback functions and uses itcomposetogether

The compose function is abstracted for all middleware functions, which take the Context object as an argument to receive the request and process the response:

// this.middlewares represents all middleware
// Through the compose abstraction
const fn = compose(this.middlewares)
await fn(ctx)

// Of course, it can also be written in this form, as long as you take the CTX argument
await compose(this.middlewares, ctx)
Copy the code

Regardless of the compose implementation, the complete code is as follows:

const http = require('http')

class Application {
  constructor () {
    this.middlewares = [] } listen (... args) {const server = http.createServer(async (req, res) => {
      const ctx = new Context(req, res)

      // Concatenate middleware callbacks to form the onion model
      // 1. Route parsing
      // 2
      // 3. Exception handling
      // 4. Unified authentication
      // 5.
      const fn = compose(this.middlewares)
      awaitfn(ctx) ctx.res.end(ctx.body) }) server.listen(... args) } use (middleware) {// Middleware callbacks become arrays
    this.middlewares.push(middleware)
  }
}
Copy the code

Next, focus on the compose function to realize the core of the Onion model.

Core of onion model: compose function encapsulation

Koa’s onion model means that each middleware layer is like each layer of the onion. When a needle passes through the center of the onion, each layer goes in and out twice, and the layer that goes in first comes out last.

Now it’s time for the magic diagram of the onion model:

  • middleware: The first middleware will execute
  • next: Each middleware will execute the next middleware through next

How do we implement the Onion model for all middleware?

Let’s take a look at each of the Middleware apis below

middleware(ctx, next)
Copy the code

Next in each middleware means to execute the next middleware. We extract the next function, and next has next in the next function. How do we deal with this?

const next = () = > nextMiddleware(ctx, next)
middleware(ctx, next(0))
Copy the code

Yes, use a recursion to complete the transformation of the middleware and connect the middleware as follows:

// Dispatch (I) means to execute the i-th middleware

const dispatch = (i) = > {
  return middlewares[i](ctx, () = > dispatch(i+1))
}
dispatch(0)
Copy the code

Dispatch (I) represents the execution of the i-th middleware, and the next() function will execute the next middleware Dispatch (I +1), so we can easily complete the Onion model using recursion

At this point, add the recursive termination condition: when the last middleware function executes next(), it returns directly

const dispatch = (i) = > {
  const middleware = middlewares[i]
  if (i === middlewares.length) {
    return
  }
  return middleware(ctx, () = > dispatch(i+1))}return dispatch(0)
Copy the code

The final compose function code looks like this:

function compose (middlewares) {
  return ctx= > {
    const dispatch = (i) = > {
      const middleware = middlewares[i]
      if (i === middlewares.length) {
        return
      }
      return middleware(ctx, () = > dispatch(i+1))}return dispatch(0)}}Copy the code

Now that the onion model, the core functionality of KOA, is complete, write an example to try it out:

const app = new Application()

app.use(async (ctx, next) => {
  ctx.body = 'hello, one'
  await next()
})

app.use(async (ctx, next) => {
  ctx.body = 'hello, two'
  await next()
})

app.listen(7000)
Copy the code

There is one small, but not globally significant, disadvantage: exception handling, and the next step will be to complete the exception catching code

Exception handling

What if your backend service crashes because of a single error?

We only need to do one exception on the middleware execution function:

try {
  const fn = compose(this.middlewares)
  await fn(ctx)
} catch (e) {
  console.error(e)
  ctx.res.statusCode = 500
  ctx.res.write('Internel Server Error')}Copy the code

However, when it is used in daily projects, we must capture it before the exception capture of the framework layer to do some tasks of exception structuring and exception reporting. At this time, an exception processing middleware will be used:

// Error handling middleware
app.use(async (ctx, next) => {
  try {
    await next();
  }
  catch (err) {
    // 1. Exception structuring
    // 2. Exception classification
    // 3. Exception level
    // 4. Exceptions are reported}})Copy the code

code

Here is all the code for implementing the minimal KOA, annotated.

You can also view the source index.js here.

/ / / HTTP module (https://nodejs.org/api/http.html), to the Node framework core API
const http = require('http')

/ / koa team by additional library: [koa - compose] (https://github.com/koajs/compose), to complete the core of onion model, although koa - only a dozen lines compose the core code
/ / the following is the core of the onion model implementation, refer to [middleware principle of koa, handwritten koa - compose code] (https://github.com/shfshanyue/Daily-Question/issues/643)
function compose (middlewares) {
  return ctx= > {
    const dispatch = (i) = > {
      const middleware = middlewares[i]
      if (i === middlewares.length) {
        return
      }
      //
      // app.use((ctx, next) => {})
      // Take the current middleware and execute
      // When next() is called in the middleware, control is handed over to the next middleware, which is the core of the Onion model
      // If the middleware does not call next(), the next middleware will not execute
      return middleware(ctx, () = > dispatch(i+1))}// Start with the first middleware
    return dispatch(0)}}// In koA code, req/res is encapsulated using Context
// Proxies multiple properties from req/res into Context for easy access
class Context {
  constructor (req, res) {
    this.req = req
    this.res = res
  }
}

class Application {
  constructor () {
    this.middlewares = [] } listen (... args) {// Handle requests in listen and listen for port numbers
    const server = http.createServer(this.callback()) server.listen(... args) }// In KOA, app.callback() returns the Node HTTP API standard handleRequest function for easy testing
  callback () {
    return async (req, res) => {
      const ctx = new Context(req, res)

      // Compose all middleware with compose, some will be done in middleware
      // 1. Route parsing
      // 2
      // 3. Exception handling
      // 4. Unified authentication
      // 5.
      const fn = compose(this.middlewares)

      try {
        await fn(ctx)
      } catch (e) {
        // The most basic exception handling functions, in a real production environment, will be replaced by a professional exception handling middleware, and will also do
        // 1. Confirm the exception level
        // 2. An exception is reported
        // 3. Construct the status code corresponding to the exception, such as 429 and 422
        console.error(e)
        ctx.res.statusCode = 500
        ctx.res.end('Internel Server Error')
      }
      ctx.res.end(ctx.body)
    }
  }

  // Register middleware and collect it in middleware array
  use (middleware) {
    this.middlewares.push(middleware)
  }
}

module.exports = Application

Copy the code

summary

The core code of KOA is very simple, if you are a Node engineer, it is highly recommended to study the source code of KOA in your spare time, and implement a minimal version of KOA yourself.