Author: MarkLin

Learning Objectives:

  1. Native Node encapsulation
  2. The middleware
  3. routing

Koa principle

The code for a nodeJS entry level HTTP service is as follows,

// index.js
const http = require('http')
const server = http.createServer((req, res) = > {
  res.writeHead(200)
  res.end('hello nodejs')
})

server.listen(3000, () = > {console.log('server started at port 3000')})Copy the code

Koa’s goal is to implement callbacks in a more simplified, streamlined, and modular way, and we want to follow koA’s lead in implementing code as follows:

// index.js
const Moa = require('./moa')
const app = new Moa()

app.use((req, res) = > {
  res.writeHeader(200)
  res.end('hello, Moa')
})

app.listen(3000, () = > {console.log('server started at port 3000')})Copy the code

So we need to create a moa.js file. The main content of this file is to create a class moa, mainly contains use() and listen() methods

/ / create earth-sized. Js
const http = require('http')

class Moa {

  use(callback) {
    this.callback = callback } listen(... args) {const server = http.createServer((req, res) = > {
      this.callback(req, res) }) server.listen(... args) } }module.exports = Moa
Copy the code

Context

To simplify the API, KOA introduces the concept of context, encapsulates and mounts the original request object REq and response object RES onto the context, and sets getters and setters to simplify operations

// index.js
// ...

// app.use((req, res) => {
// res.writeHeader(200)
// res.end('hello, Moa')
// })

app.use(ctx= > {
  ctx.body = 'cool moa'
})

// ...
Copy the code

To achieve the effect of the above code, we need to separate three classes, namely context, request, response, and create the above three JS files respectively.

// request.js
module.exports = {
  get url() {
    return this.req.url
  }
  get method() {
    return this.req.method.toLowerCase()
  }
}

// response.js
module.exports = {
  get body() {
    return this._body
  }

  set body(val) = {
    this._body = val
  }
}

// context.js
module.exports = {
  get url() {
    return this.request.url
  }
  get body() = {
    return this.response.body
  }
  set body(val) {
    this.response.body = val
  }
  get method() {
    return this.request.method
  }
}
Copy the code

Next we need to add a createContext(req, res) method to the Moa class and mount it in the appropriate place in listen() :

// moa.js
const http = require('http')

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

class Moa {
  // ...listen(... args) {const server = http.createServer((req, res) = > {
      // Create context
      const ctx = this.createContext(req, res)

      this.callback(ctx)

      / / responseres.end(ctx.body) }) server.listen(... args) } createContext(req, res) {const ctx = Object.create(context)
    ctx.request = Object.create(request)
    ctx.response = Object.create(response)

    ctx.req = ctx.request.req = req
    ctx.res = ctx.response.res = res
  }
}

Copy the code

The middleware

Koa Intermediate key mechanism: The Koa middleware mechanism is the concept of function composition, which combines a set of functions that need to be executed sequentially into a single function. The arguments of the outer function are actually the return values of the inner function. The onion ring model is a visual representation of this mechanism, which is the essence and difficulty of the Koa source code.

Combination of synchronization functions

Suppose there are three synchronization functions:

// compose_test.js
function fn1() {
  console.log('fn1')
  console.log('fn1 end')}function fn2() {
  console.log('fn2')
  console.log('fn2 end')}function fn3() {
  console.log('fn3')
  console.log('fn3 end')}Copy the code

If we want to combine three functions into a single function and execute them in order, we usually do something like this:

// compose_test.js
// ...
fn3(fn2(fn1()))
Copy the code

Execute node compose_test.js and the following output is displayed:

fn1
fn1 end
fn2
fn2 end
fn3
fn3 end
Copy the code

This should not be called a compose of functions. Instead, we should expect a compose() method to compose the functions for us, and write the code as follows:

// compose_test.js
// ...
const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code

Let’s implement the compose() function,

// compose_test.js
// ...
const compose = (middlewares) = > () => {
  [first, ...others] = middlewares
  let ret = first()
  others.forEach(fn= > {
    ret = fn(ret)
  })
  return ret
}

const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code

As you can see, we end up with the desired output:

fn1
fn1 end
fn2
fn2 end
fn3
fn3 end
Copy the code

Asynchronous function combination

Now that we know about synchronous function combinations, our actual scenarios in middleware are actually asynchronous, so let’s look at how asynchronous function combinations work. First, let’s modify the synchronous functions to make them asynchronous.

// compose_test.js
async function fn1(next) {
  console.log('fn1')
  next && await next()
  console.log('fn1 end')}async function fn2(next) {
  console.log('fn2')
  next && await next()
  console.log('fn2 end')}async function fn3(next) {
  console.log('fn3')
  next && await next()
  console.log('fn3 end')}/ /...
Copy the code

Now we expect the output to look like this:

fn1
fn2
fn3
fn3 end
fn2 end
fn1 end
Copy the code

And we don’t want to change the way we write code,

// compose_test.js
// ...
const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code

So we just need to modify the compose() function to support asynchronous functions:

// compose_test.js
// ...

function compose(middlewares) {
  return function () {
    return dispatch(0)
    function dispatch(i) {
      let fn = middlewares[i]
      if(! fn) {return Promise.resolve()
      }
      return Promise.resolve(
        fn(function next() {
          return dispatch(i + 1)}))}}const middlewares = [fn1, fn2, fn3]
const finalFn = compose(middlewares)
finalFn()
Copy the code

Running results:

fn1
fn2
fn3
fn3 end
fn2 end
fn1 end
Copy the code

Perfect!!

Perfect earth-sized

We’ll port the asynchronous compositing code directly to moa.js, but since koA also requires the CTX field, we’ll need to modify the compose() method a bit:

// moa.js
// ...
class Moa {
  // ...
  compose(middlewares) {
    return function (ctx) {
      return dispatch(0)
      function dispatch(i) {
        let fn = middlewares[i]
        if(! fn) {return Promise.resolve()
        }
        return Promise.resolve(
          fn(ctx, function () {
            return dispatch(i + 1)})}}}}Copy the code

After implementing the compose() method we continue to refine our code by first adding a middlewares to the class construction, which records all the functions that need to be composed, and then recording every callback we call in the use() method. Save it in Middlewares and then call it in the right place:

// moa.js
// ...
class Moa {
  constructor() {
    this.middlewares = []
  }

  use(middleware) {
    this.middlewares.push(middleware) } listen(... args) {const server = http.createServer(async (req, res) => {
      // Create context
      const ctx = this.createContext(req, res)
      const fn = this.compose(this.middlewares)
      await fn(ctx)
      / / responseres.end(ctx.body) }) server.listen(... args) }// ...
}
Copy the code

Let’s add a little code to test it:

// index.js
/ /...
const delay = (a)= > new Promise(resolve= > setTimeout((a)= > resolve()
  , 2000))
app.use(async (ctx, next) => {
  ctx.body = "1"
  await next()
  ctx.body += "5"
})
app.use(async (ctx, next) => {
  ctx.body += "2"
  await delay()
  await next()
  ctx.body += "4"
})
app.use(async (ctx, next) => {
  ctx.body += "3"
})

Copy the code

After running the node index.js command to start the server, we go to the page localhost:3000 to check and find the page 12345!

At this point, our simplified version of Koa has been implemented. Let’s celebrate first!!

Router

Koa also has a very important routing function, feeling that the lack of routing would lack its integrity, so let’s briefly introduce how to implement the routing function.

In fact, the principle of routing is to call the corresponding function according to the address and method, its core is to use a table to record the registered routes and methods, the schematic diagram is as follows:

The usage is as follows:

// index.js
// ...
const Router = require('./router')
const router = new Router()

router.get('/'.async ctx => { ctx.body = 'index page' })
router.get('/home'.async ctx => { ctx.body = 'home page' })
router.post('/'.async ctx => { ctx.body = 'post index' })
app.use(router.routes())

// ...
Copy the code

Create router. Js file in the root directory, and then implement the following code according to the routing principle:

// router.js
class Router {
  constructor() {
    this.stacks = []
  }

  register(path, method, middleware) {
    this.stacks.push({
      path, method, middleware
    })
  }

  get(path, middleware) {
    this.register(path, 'get', middleware)
  }

  post(path, middleware) {
    this.register(path, 'post', middleware)
  }

  routes() {
    return async (ctx, next) => {
      let url = ctx.url === '/index' ? '/' : ctx.url
      let method = ctx.method
      let route
      for (let i = 0; i < this.stacks.length; i++) {
        let item = this.stacks[i]
        if (item.path === url && item.method === method) {
          route = item.middleware
          break}}if (typeof route === 'function') {
        await route(ctx, next)
        return
      }

      await next()
    }
  }
}

module.exports = Router
Copy the code

Loacalhost :3000 test loacalHost :3000 test loacalHost :3000

This article source address: github.com/marklin2012…


Welcome to the bump Lab blog: AOtu.io

Or pay attention to the bump Laboratory public account (AOTULabs), push the article from time to time: