This is the first article in Yamatsuki’s hands-on series on Node. In addition, I have set up a new warehouse on Github with one question per day and one interview question per day. Welcome to exchange.

  • Front end test notes
  • Front end big factory surface through big complete set
  • Computer basic interview questions subtotal

When we are learning more about a framework or library, it is sometimes necessary to explore source code in order to understand its thinking and design, as well as to better use and avoid unintended bugs. For a framework/library like KOA, which is extremely simple and widely used, you should know its source code.

And to see if we know enough about it, we can implement a mini-library with only core functionality. As the saying goes, although the sparrow is small, it has all five organs.

This is just 40 lines of code to implement a small koA with its core functionality.

Source code implementation:Github.com/shfshanyue/…

Github address: github.com/shfshanyue/…

Here is a simplified example with almost all of koA’s core features:

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)

// output
// 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: Encapsulates the basic data structure of the server framework 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 simple server

Let’s start with an HTTP service based on Node’s basic HTTP API and implement the simplest version of KOA:

const http = require('http')

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

server.listen(3000)
Copy the code

Here’s an example of implementing the simplest version of KOA, which I’ll call 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

Build the Application

Start with the Appliacation framework:

  • app.listen: handles requests and responses, and listens on ports
  • app.use: Middleware function that processes the request and completes 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 that calls 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’re going to revamp app. Middlewares

  • 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

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
      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

Complete the compose function wrapper

Koa’s onion model states that each middleware layer is like each layer of an onion, with each layer going in and out twice as it passes through the center of the onion, and the layer that goes in first comes out last.

  • 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 = (a)= > 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

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.

My source code implementation of the warehouse for: KOA-mini

Pay attention to my

Scan code to add my wechat, remarks into the group, join the advanced front-end advanced group

In addition, welcome to pay attention to the public number [Internet factory recruitment] to receive recruitment information, direct to the factory responsible person. If you are the technical person in charge, you are welcome to send in the promotion information.