preface

This article is my reading Koa source code, and the implementation of a mini VERSION of Koa process. If you’ve used Koa before but don’t know how it works, I think this article should help you. Implementing a mini VERSION of Koa shouldn’t be too difficult.

This paper will analyze the internal principles step by step, including:

  1. The basic version of KOA
  2. The context of the implementation
  3. Middleware principle and implementation

File structure

  • application.js: entry file, which includes our commonly useduseMethods,listenMethods and methodsctx.bodyDo output processing
  • context.js: mainly acts as a proxy for properties and methods, so that users can access them more easilyrequestandresponseProperties and methods of
  • request.js: To nativereqProperties to extend more available properties and methods, such as:queryAttributes,getmethods
  • response.js: To nativeresProperties to extend more available properties and methods, such as:statusAttributes,setmethods

Basic version

Usage:

const Coa = require('./coa/application')
const app = new Coa()

// Application middleware
app.use((ctx) = > {
  ctx.body = '<h1>Hello</h1>'
})

app.listen(3000.'127.0.0.1')
Copy the code

application.js:

const http = require('http')

module.exports = class Coa {
  use(fn) {
    this.fn = fn
  }
  // Listen is just the syntax sugar itself and still uses http.createserverlisten(... args) {const server = http.createServer(this.callback()) server.listen(... args) } callback() {const handleRequest = (req, res) = > {
      // Create context
      const ctx = this.createContext(req, res)
      // Call the middleware
      this.fn(ctx)
      // Output the content
      res.end(ctx.body)
    }
    return handleRequest
  }
  createContext(req, res) {
    let ctx = {}
    ctx.req = req
    ctx.res = res
    return ctx
  }
}
Copy the code

The basic implementation is simple, with a call to use to store the function, execute it when the server is started, and print the contents of ctx.body.

But it’s soulless. Then, implementing the context and middleware principles makes Koa complete.

Context

CTX extends many useful properties and methods, such as ctx.query and ctx.set(). But they are not context-wrapped. Instead, when accessing properties on CTX, it internally returns properties encapsulated in Request and Response via property hijacking. Just like when you access ctx.query, you actually access ctx.request.query.

When you think of hijacking you might think of Object.defineProperty. Inside Kao you use setters and getters for objects provided by ES6, and the effect is the same. So to implement CTX, we first implement request and Response.

To do this, you need to modify the createContext method:

// All three are objects
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Coa {
  constructor() {
    this.context = context
    this.request = request
    this.response = response
  }
  createContext(req, res) {
    const ctx = Object.create(this.context)
    // Mount the extended request and Response to CTX
    // Use object.create to create an Object that is modeled after the parameters passed in
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    
    ctx.app = request.app = response.app = this;
    // Mount native properties
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res
    
    request.ctx = response.ctx = ctx;
    request.response = response;
    response.request = request;
    
    return ctx
  }
}
Copy the code

The above is a bunch of fancy assignments, so you can get attributes in multiple ways. Query, ctx.request. Query, ctx.app.query, and so on.

If you think it’s a bit redundant, you can read these lines mainly because they are the only ones we used to implement the source code:

const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)

ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res
Copy the code

request

Request. Js:

const url = require('url')

module.exports = {
 Const request = ctx.request = object.create (this.request) * ctx.req = request.req = response.req = req * * const request = ctx.request = object.create (this.request) * ctx.req = response.req = req * * This refers to CTX, so this.req accesses the native property req *. Similarly, this.request.req can be accessed via */
  // Query parameters of the request
  get query() {
    return url.parse(this.req.url).query
  },
  // The requested path
  get path() {
    return url.parse(this.req.url).pathname
  },
  // Request method
  get method() {
    return this.req.method.toLowerCase()
  }
}
Copy the code

response

The response. Js:

module.exports = {
  // This.res is the same as above
  // Return the status code
  get status() {
    return this.res.statusCode
  },
  set status(val) {
    return this.res.statusCode = val
  },
  // The output returned
  get body() {
    return this._body
  },
  set body(val) {
    return this._body = val
  },
  // Set the header
  set(filed, val) {
    if (typeof filed === 'string') {
      this.res.setHeader(filed, val)
    }
    if (toString.call(filed) === '[object Object]') {
      for (const key in filed) {
        this.set(key, filed[key])
      }
    }
  }
}
Copy the code

The property broker

With the above implementation, we can use the ctx.request.query call to query the extended properties. In practice, however, ctx.query is more commonly used. Query is a property of request and is not accessible through ctx.query.

This can be done by simply doing a little proxy and returning ctx.request.query when accessing ctx.query.

context.js:

module.exports = {
    get query() {
        return this.request.query
    }
}
Copy the code

There are so many extended attributes in the actual code that it is impossible to write them one by one. For elegant delegate properties, Koa uses the delegates package to implement. Here I will directly and simply encapsulate the proxy function, the proxy function mainly uses __defineGetter__ and __defineSetter__ two methods.

Each object has __defineGetter__ and __defineSetter__, which bind a function to the specified property of the current object. When the property is obtained or assigned, the bound function is called. Something like this:

let obj = {}
let obj1 = {
    name: 'JoJo'
}
obj.__defineGetter__('name'.function(){
    return obj1.name
})
Copy the code

Call obj.name and get the value of obj1.name.

Now that you know what these two methods are for, let’s start modifying context.js:

const proto = module.exports = {
}

/ / the getter agent
function delegateGetter(prop, name){
  proto.__defineGetter__(name, function(){
    return this[prop][name]
  })
}
/ / setter agent
function delegateSetter(prop, name){
  proto.__defineSetter__(name, function(val){
    return this[prop][name] = val
  })
}
// Method proxy
function delegateMethod(prop, name){
  proto[name] = function() {
    return this[prop][name].apply(this[prop], arguments)
  }
}

delegateGetter('request'.'query')
delegateGetter('request'.'path')
delegateGetter('request'.'method')

delegateGetter('response'.'status')
delegateSetter('response'.'status')
delegateGetter('response'.'body')
delegateSetter('response'.'body')
delegateMethod('response'.'set')
Copy the code

Middleware Principles

Middleware ideas are at the heart of Koa and help extend functionality. That’s why it’s so small, but so powerful. Another advantage is that middleware makes the responsibility of function modules more clear. A function is one middleware, and multiple middleware can be combined to form a complete application.

Here is the famous onion model. This picture vividly expresses the function of the middleware idea, which is like an assembly line. The things processed by the upstream are transferred to the downstream, and the downstream can continue processing and finally output the processing results.

The principle of analysis

When the use registration middleware is called, each middleware is internally stored in an array, and when the middleware is executed, it is supplied with the next parameter. Calling next executes the next middleware, and so on. When the middleware in the array completes, it returns to the original path. Something like this:

app.use((ctx, next) = > {
  console.log('1 start')
  next()
  console.log('1 end')
})

app.use((ctx, next) = > {
  console.log('2 start')
  next()
  console.log('2 end')
})

app.use((ctx, next) = > {
  console.log('3 start')
  next()
  console.log('3 end')})Copy the code

The following output is displayed:

1 start
2 start
3 start
3 end
2 end
1 end
Copy the code

For those of you who have some knowledge of data structures, you’ll quickly realize that this is a “stack” structure, executed in a “first in, last out” order.

Below, I simplify the implementation principle of internal middleware to simulate middleware execution:

function next1() {
  console.log('1 start')
  next2()
  console.log('1 end')}function next2() {
  console.log('2 start')
  next3()
  console.log('2 end')}function next3() {
  console.log('3 start')
  console.log('3 end')
}
next1()
Copy the code

Execution process:

  1. callnext1, push it to the stack for execution, output1 start
  2. encounternext2Function, push it onto the stack and execute, output2 start
  3. encounternext3Function, push it onto the stack and execute, output3 start
  4. The output3 end, the function is executed,next3Pop up the stack
  5. The output2 end, the function is executed,next2Pop up the stack
  6. The output1 end, the function is executed,next1Pop up the stack
  7. Stack empty, all execution complete

I believe that through this simple example, are probably understand the implementation process of middleware.

Principle of implementation

The key point of realization of middleware principle is the transfer of CTX and next.

function compose(middleware) {
  return function(ctx) {
    return dispatch(0)
    function dispatch(i){
      // Fetch middleware
      let fn = middleware[i]
      if(! fn) {return
      }
      // dispatch.bind(null, I + 1) is the next received by the application middleware
      // Next is the next application middleware
      fn(ctx, dispatch.bind(null, i + 1))}}}Copy the code

As you can see, the implementation process is essentially a recursive call to a function. When implemented internally, there is nothing magical about Next; it is just a function called by the next middleware, passed in as a parameter for the consumer to call.

Now let’s test Compose separately. You can paste it into the console and run it:

function next1(ctx, next) {
  console.log('1 start')
  next()
  console.log('1 end')}function next2(ctx, next) {
  console.log('2 start')
  next()
  console.log('2 end')}function next3(ctx, next) {
  console.log('3 start')
  next()
  console.log('3 end')}let ctx = {}
let fn = compose([next1, next2, next3])
fn(ctx)
Copy the code

Finally, because Koa middleware can be executed asynchronously using async/await, compose returns a Promise:

function compose(middleware) {
  return function(ctx) {
    return dispatch(0)
    function dispatch(i){
      // Fetch middleware
      let fn = middleware[i]
      if(! fn) {return Promise.resolve()
      }
      // dispatch.bind(null, I + 1) is the next received by the application middleware
      // Next is the next application middleware
      try {
        return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)))}catch (error) {
        return Promise.reject(error)
      }
    }
  }
}
Copy the code

application

After the middleware logic is implemented, it is applied to the mini VERSION Koa. The original code logic needs to be modified (some code is ignored).

application.js:

module.exports = class Coa {
  constructor() {
    // Store an array of middleware
    this.middleware = []
  }

  use(fn) {
    if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
    // Add the middleware to the array
    this.middleware.push(fn)
    return this} listen(... args) {const server = http.createServer(this.callback()) server.listen(... args) } callback() {const handleRequest = (req, res) = > {
      // Create context
      const ctx = this.createContext(req, res)
      // fn is the first application middleware
      const fn = this.compose(this.middleware)
      The respond function handles the ctx.body output after all middleware has executed
      return fn(ctx).then((a)= > respond(ctx)).catch(console.error)
    }
    return handleRequest
  }
  
  compose(middleware) {
    return function(ctx) {
      return dispatch(0)
      function dispatch(i){
        let fn = middleware[i]
        if(! fn) {return Promise.resolve()
        }
        // dispatch.bind(null, I + 1) is the next received by the application middleware
        try {
          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))}catch (error) {
          return Promise.reject(error)
        }
      }
    }
  }
}

function respond(ctx) {
  let res = ctx.res
  let body = ctx.body
  if (typeof body === 'string') {
    return res.end(body)
  }
  if (typeof body === 'object') {
    return res.end(JSON.stringify(body))
  }
}
Copy the code

Complete implementation

application.js:

const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Coa {
  constructor() {
    this.middleware = []
    this.context = context
    this.request = request
    this.response = response
  }

  use(fn) {
    if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
    this.middleware.push(fn)
    return this} listen(... args) {const server = http.createServer(this.callback()) server.listen(... args) } callback() {const handleRequest = (req, res) = > {
      // Create context
      const ctx = this.createContext(req, res)
      // fn is the first application middleware
      const fn = this.compose(this.middleware)
      return fn(ctx).then((a)= > respond(ctx)).catch(console.error)
    }
    return handleRequest
  }

  // Create context
  createContext(req, res) {
    const ctx = Object.create(this.context)
    // Processed attributes
    const request = ctx.request = Object.create(this.request)
    const response = ctx.response = Object.create(this.response)
    // Native attributes
    ctx.app = request.app = response.app = this;
    ctx.req = request.req = response.req = req
    ctx.res = request.res = response.res = res

    request.ctx = response.ctx = ctx;
    request.response = response;
    response.request = request;

    return ctx
  }

  // Middleware handles logic implementation
  compose(middleware) {
    return function(ctx) {
      return dispatch(0)
      function dispatch(i){
        let fn = middleware[i]
        if(! fn) {return Promise.resolve()
        }
        // dispatch.bind(null, I + 1) is the next received by the application middleware
        // Next is the next application middleware
        try {
          return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))}catch (error) {
          return Promise.reject(error)
        }
      }
    }
  }
}

// Handle different types of body output
function respond(ctx) {
  let res = ctx.res
  let body = ctx.body
  if (typeof body === 'string') {
    return res.end(body)
  }
  if (typeof body === 'object') {
    return res.end(JSON.stringify(body))
  }
}
Copy the code

Write in the last

The simplicity of this article implements the main functions of Koa. Interested in the best or own to see the source code, to achieve their own mini version of Koa. In fact, Koa’s source code is not very much, a total of 4 files, all the code including comments is only about 1800 lines. And logic is not very difficult, very recommended reading, especially suitable for the source of entry level students to watch.

Finally, the complete implementation code is attached: Github