directory

  • The use of koa
  • Read the KOA source code briefly
  • What does CTX mount
  • Next built the Onion model
    • How to ensure correct execution of middleware with asynchronous code
    • Fixed confusion caused by multiple calls to Next
  • Handle exceptions based on event drivers

The use of koa

Koa is very simple to use, written after introducing dependencies

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) = > {
  console.log(ctx)
})
app.listen(4000)
Copy the code

Then open http://127.0.0.1:4000 in the browser to access

If body is Not returned, KOA defaults to Not Found

ctx

Expand the code further to see what’s on top of CTX

// ...
  console.log(ctx)
  console.log('native req ----') // Node native req
  console.log(ctx.req.url)
  console.log(ctx.request.req.url)
  console.log('koa request ----') // KOA encapsulates request
  console.log(ctx.url)
  console.log(ctx.request.url)
  // native req ----
  // /
  // /
  // koa request ----
  // /
  // /
// ...
Copy the code

The above code stored in the warehouse, self – help.

The KOA website states that CTX has a set of request and Response property aliases.

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
// ctx.url proxies ctx.request.url
Copy the code

next

The following code is stored in the warehouse, for yourself.

Use next to see what works

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) = > {
  console.log(1)
  next()
  console.log(2)
})
app.use((ctx, next) = > {
  console.log(3)
  next()
  console.log(4)
})
app.use((ctx, next) = > {
  console.log(5)
  next()
  console.log(6)
})
app.listen(4000)
/ / 1
/ / 3
/ / 5
/ / 6
/ / 4
/ / 2
Copy the code

As you can see from the code print above, next serves as a placeholder. You can view it as the following

app.use((ctx, next) = > {
  console.log(1)
  app.use((ctx, next) = > {
    console.log(3)
    app.use((ctx, next) = > {
      console.log(5)
      next()
      console.log(6)})console.log(4)})console.log(2)})Copy the code

This is the Onion model.

What if some middleware has asynchronous code?

const Koa = require('koa')
let app = new Koa()
// Asynchronous functions
const logger = () = > {
  return new Promise((resolve, reject) = > {
    setTimeout(_= > {
      console.log('logger')
      resolve()
    }, 1000)
  })
}
app.use((ctx, next) = > {
  console.log(1)
  next()
  console.log(2)
})
app.use(async (ctx, next) => {
  console.log(3)
  await logger()
  next()
  console.log(4)
})
app.use((ctx, next) = > {
  console.log(5)
  next()
  console.log(6)
})
app.listen(4000)
/ / 1
/ / 3
/ / 2
/ / wait for 1 s
// logger
/ / 5
/ / 6
/ / 4
Copy the code

At this time the print result is not the expected result, we expect 1 -> 3 -> 1S logger -> 5-> 6-> 4 ->2

At this point we need to add an await before next

// ...
app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(2)})// ...
Copy the code

Read the KOA source code briefly

Koa aims to be a smaller, more expressive, and more robust Web development framework.

The source code is also very lightweight and easy to read.

Four core documents

  • application.js: Simple encapsulationhttp.createServer()And integratecontext.js
  • context.js: Broker and consolidaterequest.jsandresponse.js
  • request.js: Based on nativereqEncapsulated is better
  • response.js: Based on nativeresEncapsulated is better

Get started

The following code is stored in the warehouse, as needed.

Koa is implemented in ES6 with two core methods app.listen() and app.use((CTX, next) =< {… })

Implement app.listen() in application.js

const http = require('http')
class Koa {
  constructor () {
    // ...
  }  
  // Process user requests
  handleRequest (req, res) {
    // ...} listen (... args) {let server = http.createServer(this.handleRequest.bind(this)) server.listen(... args) } }module.exports = Koa
Copy the code

What does CTX mount

This can be seen from the simple use of CTX above

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy
Copy the code

We need all of the above objects, which are ultimately proxied to CTX objects.

Create context. Js/request. Js/response. Js three files

Request. The contents of js

const url = require('url')
let request = {}
module.exports = request
Copy the code

The response. Js content

let response = {}
module.exports = response
Copy the code

The context. Js content

let context = {}

module.exports = context
Copy the code

Import the above three files in application.js and place them on the instance

const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa extends Emitter{
  constructor () {
    super(a)// object.create breaks the prototype chain
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
}
Copy the code

Since it cannot be assigned directly with an equal sign, the original variable will be tampered with when modifying its attributes because the object references the same memory space.

So break the dependency with the object.create method, which is equivalent to

function create (parentPrototype) {
  function F () {}
  F.prototype = parentPrototype
  return new F()
}
Copy the code

User requests are then processed and request/Response is brokered on CTX

  // Create context
  createContext (req, res) {
    let ctx = this.context
    / / request
    ctx.request = this.request
    ctx.req = ctx.request.req = req
    / / response
    ctx.response = this.response
    ctx.res = ctx.response.res = res
    return ctx
  }
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    return ctx
  }
Copy the code

In context.js, the proxy is implemented using __defineGetter__ / __defineSetter__, which is a variant of the Object.defineProperty() method and can be set separately without overwriting the Settings.

let context = {}
// Define the getter
function defineGetter (key, property) {
  context.__defineGetter__ (property, function () {
    return this[key][property]
  })
}
// Define the setters
function defineSetter (key, property) {
  context.__defineSetter__ (property, function (val) {
    this[key][property] = val
  })
}
/ / agent request
defineGetter('request'.'path')
defineGetter('request'.'url')
defineGetter('request'.'query')
/ / agent for the response
defineGetter('response'.'body')
defineSetter('response'.'body')
module.exports = context
Copy the code

In Request.js, encapsulation is implemented using the property accessor provided by ES5

const url = require('url')
let request = {
  get url () {
    return this.req.url // This is the object called ctx.request
  },
  get path () {
    let { pathname } = url.parse(this.req.url)
    return pathname
  },
  get query () {
    let { query } = url.parse(this.req.url, true)
    return query
  }
  / /... More to be perfected
}
module.exports = request
Copy the code

In Response.js, encapsulation is achieved using the property accessor provided by ES5

let response = {
  set body (val) {
    this._body = val
  },
  get body () {
    return this._body // This is the called object ctx.response
  }
  / /... More to be perfected
}
module.exports = response
Copy the code

The above implementation encapsulates request/ Response and proxies it to CTX

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy
Copy the code

Next built the Onion model

Use ((CTX, next) =< {… })

Use is used to store middleware, such as cookies, session, static… Wait for a bunch of processing functions, and execute them in onion form.

  constructor () {
    // ...
    // Store the middleware array
    this.middlewares = []
  }
  // Use middleware
  use (fn) {
    this.middlewares.push(fn)
  }
Copy the code

A bunch of registered middleware is expected to execute when processing user requests

  // Combine middleware
  compose (middlewares, ctx) {
    function dispatch (index) {
      // The iteration terminating condition completes the middleware
      // Then return the promise of success
      if (index === middlewares.length) return Promise.resolve()
      let middleware = middlewares[index]
      // let the first function finish, if there is asynchrony, need to see if there is any await
      // Return a promise
      return Promise.resolve(middleware(ctx, () = > dispatch(index + 1)))}return dispatch(0)}// Process user requests
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    this.compose(this.middlewares, ctx)
    return ctx
  }
Copy the code

The above dispatch iteration functions are used in many places, such as recursively deleting directories, and are at the heart of KOA.

How to ensure correct execution of middleware with asynchronous code

Promises are returned primarily to handle cases where the middleware contains asynchronous code

After all middleware execution is complete, the page needs to be rendered

  // Process user requests
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404 // The default 404 will be changed when body is set
    let ret = this.compose(this.middlewares, ctx)
    ret.then(_= > {
      if(! ctx.body) {// Body is not set
        res.end(`Not Found`)}else if (ctx.body instanceof Stream) { / / flow
        res.setHeader('Content-Type'.'text/html; charset=utf-8')
        ctx.body.pipe(res)
      } else if (typeof ctx.body === 'object') { / / object
        res.setHeader('Content-Type'.'text/josn; charset=utf-8')
        res.end(JSON.stringify(ctx.body))
      } else { / / string
        res.setHeader('Content-Type'.'text/html; charset=utf-8')
        res.end(ctx.body)
      }
    })
    return ctx
  }
Copy the code

Need to consider multiple situations to do compatibility.

Fixed confusion caused by multiple calls to Next

Run the following tests with the above code

The execution result will be

// 1 => 3 =>1s,logger => 4
// => 3 =>1s,logger => 4 => 2
Copy the code

It doesn’t meet our expectations

Because the execution is as follows

In step 2, the next function is not allowed to be called more than once within a middleware function. For example, the next function is not allowed to be called more than once within a middleware function because the compose index is already 2.

The solution is to use flag as the subscript of the functional middleware that runs in the Onion model. If next is run twice in one middleware, the index will be lower than flag.

  /** * Combined middleware *@param {Array<Function>} middlewares 
   * @param {context} ctx 
   */ 
  compose (middlewares, ctx) {
    let flag = -1
    function dispatch (index) {
      // 3) flag Records the running middleware subscripts
      // 3.1) If a middleware calls next twice then index will be less than flag
      // if (index <= flag) return Promise.reject(new Error('next() called multiple times'))
      flag = index
      // 2) Iteration termination condition: middleware is finished
      // 2.1) then return the promise of success
      if (index === middlewares.length) return Promise.resolve()
      // 1) let the first function finish, if there is asynchron, need to see if there is any await
      // 1.1) must return a promise
      let middleware = middlewares[index]
      return Promise.resolve(middleware(ctx, () = > dispatch(index + 1)))}return dispatch(0)}Copy the code

Handle exceptions based on event drivers

How do you handle exceptions that occur in middleware?

Node is event-driven, so we simply inherit the Events module

const Emitter = require('events')
class Koa extends Emitter{
  // ...
  // Process user requests
  handleRequest (req, res) {
    // ...
    let ret = this.compose(this.middlewares, ctx)
    ret.then(_= > {
      // ...
    }).catch(err= > { // Handler exception
      this.emit('error', err)
    })
    return ctx
  }  
}
Copy the code

Then do catch exception above, use as follows

const Koa = require('./src/index')

let app = new Koa()

app.on('error'.err= > {
  console.log(err)
})
Copy the code

The test case code is stored in the repository, as needed.

conclusion

From the above we have implemented a simple KOA, request/ Response.js file needs to be extended to support more attributes.

The complete code and test cases are stored at @careteen/ KOA. If you are interested, go to debug.