Introduction: implement a simple KOA core code, easy to understand the PRINCIPLE of KOA. Learn koA code by the way.

A brief analysis of KOA

Create an HTTP service and bind only one middleware. Create index. Js

/** index.js */
const Koa = require('koa')

const app = new Koa()

app.use(ctx= > {
  ctx.body = 'Hello World'
})

app.listen(8080.function() {
  console.log('server start 8080')})Copy the code

You can see that in this code

  • Koa is a constructor
  • Koa has at least two methods: UES and LISTEN
  • Listen has the same parameters as HTTP
  • The UES accepts a method that is called when the user accesses it, passing in the context CTX.

Let’s look at the directory structure of the KOA source code

|-- koa
    |-- .npminstall.done
    |-- History.md
    |-- LICENSE
    |-- Readme.md
    |-- package.json
    |-- lib
        |-- application.js
        |-- context.js
        |-- request.js
        |-- response.js
Copy the code

Application. Js is the entry file, which is a class when opened. Context. js, request.js, response.js are all an object that forms the context CTX

Starting the HTTP Service

Write the application.js part of the code first. Create the myKoa folder where our KOA code will be placed. Create myKoa/application. Js

Application. Js exports a class that has at least two methods: Listen and use. Listen creates the service and listens for the port number HTTP service. Use is used to collect middleware. The implementation code is as follows

/** myKoa/application.js */
const http = require('http')

module.exports = class Koa {
  constructor() {
    // Storage middleware
    this.middlewares = []
  }
  // Collect middleware
  use(fn) {
    this.middlewares.push(fn)
  }
  // Process the current request method
  handleRequest(req, res) { // Node passes req and res
    res.end('Handwritten KOA core code') // In order to access the page display, temporarily added
  }
  // Create a service and listen for port numberslisten(... arges) {const app = http.createServer(this.handleRequest.bind(this)) app.listen(... arges) } }Copy the code

The code is simple. Use stores middleware in middlewares. Listen starts the service and calls handleRequest each time a request comes in

Create CTX (1)

Context. js, request.js, response.js are all an object that forms the context CTX. The following code

/** myKoa/context.js */
const proto = {}

module.exports = proto
Copy the code
/** myKoa/request.js */
module.exports = {}
Copy the code
/** myKoa/response.js */
module.exports = {}
Copy the code

The relationship is as follows: The export of request.js and response.js files will be bound to the exported object of context.js and used as ctx.request and ctx.response respectively.

Koa uses CTX independently of each other for each new koa (), processing objects exported by context.js, request.js, and Response.js. The source code uses the object.create () method to create a new Object, using an existing Object to provide the __proto__ of the newly created Object. This will be demonstrated in code later

Before you create CTX, take a look at a few more properties on CTX and their direct relationships. It’s important to distinguish between what comes with Node and what comes with KOA

app.use(async ctx => {
  ctx; / / this is the Context
  ctx.req; // This is node Request
  ctx.res; // This is node Response
  ctx.request; // This is koA Request
  ctx.response; // This is koa Response
  ctx.request.req; // This is node Request
  ctx.response.res;  // This is node Response
});
Copy the code

Why is this design addressed later in the article

Start creating CTX. Part of the code is as follows

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // Storage middleware
    this.middlewares = []
    // Bind context, request, response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // Create context CTX
  createContext(req, res) {
    const ctx = this.context
    // KoA Request, Response
    ctx.request = this.request
    ctx.response = this.response
    // Node Request and Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // Collect middleware
  use(fn) {/ *... * /}
  // Process the current request method
  handleRequest(req, res) {
    // Create the context, ready to pass to the middleware
    const ctx = this.createContext(req, res)
    
    res.end('Handwritten KOA core code') // In order to access the page display, temporarily added
  }
  // Create a service and listen for port numberslisten(... arges) {/ *... * /}}Copy the code

At this point, a basic context CTX is created.

Create CTX (2)

Gets an attribute on the context

Implement a ctx.request.url. Consider a few questions before you begin

  • Request.js exports an object and does not accept arguments
  • ctx.req.url,ctx.request.url,ctx.request.req.urlThe three directly should always be equal

Koa does this

  • Step 1 ctx.request.req = ctx.req = req
  • Access to ctx.request.url becomes access to ctx.request.req.url

Get grammar candy

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
Copy the code

This refers to the Object generated by Object.create(request), not the Object exported by Request.js

Set the properties on the context

Next we implement ctx.response.body = ‘Hello World’. When ctx.response.body is set, the property is actually stored in ctx.response._body. When ctx.response.body is retrieved, it is simply retrieved from ctx.response._body. The following code

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
Copy the code

In this case, this refers to the Object generated by Object.create(Response), not the Object exported by Response.js

Set the CTX alias

Koa gives us a lot of aliases, such as ctx.body which is ctx.response.body

With previous experience, getting/setting properties is easy. Go straight to code

/** myKoa/context.js */
const proto = {
  get url() {
    return this.request.req.url
  },
  get body() {
    return this.response.body
  },
  set body(v) {
    this.response.body = v
  },
}
module.exports = proto
Copy the code

It’s easy to have feelings. Of course, the KOA context section does not end here. If you look at the koa/lib/context.js code, you can see this code at the bottom (only the access method is selected)

delegate(proto, 'response')
  .access('status')
  .access('body')

delegate(proto, 'request')
  .access('path')
  .access('url')
Copy the code

Koa encapsulates get/set. The Delegator third-party package is used. The core is to use the __defineGetter__ and __defineSetter__ methods. Here, for simplicity’s sake, we simply encapsulate two methods instead of Delegator to implement simple functionality.

// Get attributes. Call methods like defineGetter(' Response ', 'body')
function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}
// Set the properties. Call methods like defineSetter('response', 'body')
function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}
Copy the code

The myKoa/context.js file is finally changed to

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

/ / request
defineGetter('request'.'url')
/ / response
defineGetter('response'.'body')
defineSetter('response'.'body')

module.exports = proto
Copy the code

Make ctx.body appear on the page

This step is as simple as checking whether ctx.body has a value and firing req.end(). The relevant codes are as follows

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/ *... * /}
  // Create context CTX
  createContext(req, res) {/ *... * /}
  // Collect middleware
  use(fn) {/ *... * /}
  // Process the current request method
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    
    res.statusCode = 404 / / the default status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')}}// Create a service and listen for port numberslisten(... arges) {/ *... * /}}Copy the code

You can do the same thing with attributes like headers

Implementing synchronous middleware

The middleware takes two parameters, a context CTX and a next method. The context CTX has been written, which is how to implement the next method.

Write a dispatch method whose main functions are: pass in subscript 0, find the method middleware with subscript 0 in the array, call middleware and pass in a method next, and when next calls, find the subscript + 1 method. To achieve the following

const middlewares = [f1, f2, f3]
function dispatch(index) {
  if (index === middlewares.length) return
  const middleware = middlewares[index]
  const next = (a)= > dispatch(index+1)
  middleware(next)
}
dispatch(0)
Copy the code

At this point, the next method is implemented.

In KOA, it is not allowed for one middleware to call Next twice in one request. Such as

app.use((ctx, next) = > {
  ctx.body = 'Hello World'
  next()
  next() Next () called multiple times
})
Copy the code

Koa uses a little trick. Log the middleware subscript for each invocation, and an error is reported if the middleware subscript called is not incremented by 1(middleware subscript <= last middleware subscript). Modify the code as follows

const middlewares = [f1, f2, f3] For example, there are three methods in middleware
let i = - 1 
function dispatch(index) {
  if (index <= i) throw new Error('next() called multiple times')
  if (index === middlewares.length) return
  i = index
  const middleware = middlewares[index]
  const next = (a)= > dispatch(index+1)
  middleware(next)
}
dispatch(0)
Copy the code

Middleware code is almost complete, CTX is passed in and myKoa/application.js file is added.

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/ *... * /}
  // Create context CTX
  createContext(req, res) {/ *... * /}
  // Collect middleware
  use(fn) {/ *... * /}
  // Process middleware
  compose(ctx) {
    const middlewares = this.middlewares
    let i = - 1 
    function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = (a)= > dispatch(index+1)
      middleware(ctx, next)
    }
    dispatch(0)}// Process the current request method
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    this.compose(ctx)

    res.statusCode = 404 / / the default status
    if (ctx.body) {
      res.statusCode = 200
      res.end(ctx.body)
    } else {
      res.end('Not Found')}}// Create a service and listen for port numberslisten(... arges) {/ *... * /}}Copy the code

This is where synchronous middleware is implemented

Implementing asynchronous middleware

Using asynchronous middleware in KOA is written as follows

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

app.use(async (ctx, next) => {
  await new Promise((res, rej) = > setTimeout(res,1000))
  console.log('ctx.body:', ctx.body)
})
Copy the code

About 1s after the above code accepts the request the console prints ctx.body: Hello World. As you can see, KOA is based on async/await. Expect a Promise to return after each next().

Also, given that the middleware executes asynchronously, handleRequest should wait for the middleware to complete execution before executing the code. Compose should also return a Promise

Async can be used to quickly complete the normal function = Promise transformation.

Modify the compose and handleRequest codes

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {/ *... * /}
  // Create context CTX
  createContext(req, res) {/ *... * /}
  // Collect middleware
  use(fn) {/ *... * /}
  // Process middleware
  compose(ctx) {
    const middlewares = this.middlewares
    let i = - 1 
    async function dispatch(index) {
      if (index <= i) throw new Error('next() called multiple times')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = (a)= > dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)}// Process the current request method
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then((a)= > {
      res.statusCode = 404 / / the default status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) = > {
      console.log(err)
    })
  }
  // Create a service and listen for port numberslisten(... arges) {/ *... * /}}Copy the code

The code shown

application.js

/** myKoa/application.js */
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Koa {
  constructor() {
    // Storage middleware
    this.middlewares = []
    // Bind context, request, response
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
  // Create context CTX
  createContext(req, res) {
    const ctx = this.context
    // KoA Request, Response
    ctx.request = this.request
    ctx.response = this.response
    // Node Request and Response
    ctx.request.req = ctx.req = req
    ctx.response.res = ctx.res = res
    return ctx
  }
  // Collect middleware
  use(fn) {
    this.middlewares.push(fn)
  }
  // Process middleware
  compose(ctx) {
    const middlewares = this.middlewares
    let i = - 1 
    async function dispatch(index) {
      if (index <= i) throw new Error('multi called next()')
      if (index === middlewares.length) return
      i = index
      const middleware = middlewares[index]
      const next = (a)= > dispatch(index+1)
      return middleware(ctx, next)
    }
    return dispatch(0)}// Process the current request method
  handleRequest(req, res) {
    const ctx = this.createContext(req, res)
    const p = this.compose(ctx)
    p.then((a)= > {
      res.statusCode = 404 / / the default status
      if (ctx.body) {
        res.statusCode = 200
        res.end(ctx.body)
      } else {
        res.end('Not Found')
      }
    }).catch((err) = > {
      console.log(err)
    })
  }
  // Create a service and listen for port numberslisten(... arges) {const app = http.createServer(this.handleRequest.bind(this)) app.listen(... arges) } }Copy the code

context.js

/** myKoa/context.js */
const proto = {}

function defineGetter(property, key) {
  proto.__defineGetter__(key, function() {
    return this[property][key]
  })
}

function defineSetter(property, key) {
  proto.__defineSetter__(key, function(v) {
    this[property][key] = v
  })
}

/ / request
defineGetter('request'.'url')
/ / response
defineGetter('response'.'body')
defineSetter('response'.'body')

module.exports = proto
Copy the code

request.js

/** myKoa/request.js */
module.exports = {
  get url() {
    return this.req.url
  }
}
Copy the code

response.js

/** myKoa/response.js */
module.exports = {
  set body(v) {
    this._body = v
  },
  get body() {
    return this._body
  }
}
Copy the code