1 Native implementation

1.1 Starting a Service

Node up a service how easy, believe that as long as the Internet, can search similar to the following code quickly start a service.

const http = require('http')
const handler = ((req, res) = > {
  res.end('Hello World! ')
})
http
  .createServer(handler)
  .listen(
    8888.() = > {
      console.log('8888' listening 127.0.0.1.)})Copy the code

Visit 127.0.0.1:8888 and you’ll see the ‘Hello World! . And then you’ll see that if you change the route, if you change the request, you’ll only get this string.

Curl 127.0.0.1:8888 curl curl -X POST http://127.0.0.1:8888 curl 127.0.0.1:8888/aboutCopy the code

At this point, you’ll go to the documentation and discover that there’s something wrong with the reQ you just called back. We can use the Method and URL attributes to return different results for different methods and routes. It’s easy to think of something like this:

const http = require('http')
const handler = ((req, res) = > {
  let resData = '404 NOT FOUND! '
  const { method, path } = req
  switch (path) {
    case '/':
      if (method === 'get') {
        resData = 'Hello World! '
      } else if (method === 'post') {
        resData = 'Post Method! '
      }
      break
    case '/about':
      resData = 'Hello About! '
  }
  res.end = resData
})
http
  .createServer(handler)
  .listen(
    8888.() = > {
      console.log('8888' listening 127.0.0.1.)})Copy the code

But a service can’t have only a few interfaces and methods. You can’t add a branch every time you add one, so it’s easy to decouple the path and method from the handler.

1.2 Policy mode decoupling

How do you decouple it? From the code in the newbie village, we can see that the policy pattern can be used to solve this problem:

const http = require('http')
class Application {
  constructor () {
    // Collect callbacks for route and method
    this.$handlers = new Map()}/ / register handler
  register (method, path, handler) {
    let pathInfo = null
    if (this.$handlers.has(path)) {
      pathInfo = this.$handlers.get(path)
    } else {
      pathInfo = new Map(a)this.$handlers.set(path, pathInfo)
    }
    // Register the callback function
    pathInfo.set(method, handler)
  }
  use () {
    return (request, response) = > {
      const { url: path, method } = request
      this.$handlers.has(path) && this.$handlers.get(path).has(method)
        ? this.$handlers.get(path).get(method)(request, response)
        : response.end('404 NOT FOUND! ')}}}const app = new Application()
app.register('GET'.'/'.(req, res) = > {
  res.end('Hello World! ')
})
app.register('GET'.'/about'.(req, res) = > {
  res.end('Hello About! ')
})
app.register('POST'.'/'.(req, res) = > {
  res.end('Post Method! ')
})
http
  .createServer(app.use())
  .listen(
    8888.() = > {
      console.log('8888' listening 127.0.0.1.)})Copy the code

1.3 Comply with the DRY principle

But at this point you’ll find:

  • If you shake your handmethodThe method is written in lower case becauseHttp.Request.methodIt’s all uppercase, it doesn’t match the right onehandlerAnd return to'404 NOT FOUND'.
  • If I want to add some action before the response data, such as adding a timestamp for each request to indicate the time of the request, I have to modify eachregisterIn thehandlerFunction that does not comply with the DRY principle

At this point, modify the code above to implement sequential execution of the handler using Promise.

const http = require('http')
class Application {
  constructor() {
    // Collect callbacks for route and method
    this.$handlers = new Map(a)// Expose get and POST methods
    this.get = this.register.bind(this.'GET')
    this.post = this.register.bind(this.'POST')}/ / register handler
  register(method, path, ... handlers) {
    let pathInfo = null
    if (this.$handlers.has(path)) {
      pathInfo = this.$handlers.get(path)
    } else {
      pathInfo = new Map(a)this.$handlers.set(path, pathInfo)
    }
    // Register the callback function
    pathInfo.set(method, handlers)
  }
  use() {
    return (request, response) = > {
      const { url: path, method } = request
      if (
        this.$handlers.has(path) &&
        this.$handlers.get(path).has(method)
      ) {
        const _handlers = this.$handlers.get(path).get(method)
        _handlers.reduce((pre, _handler) = > {
          return pre.then(() = > {
            return new Promise((resolve, reject) = > {
              _handler.call({}, request, response, () = > {
                resolve()
              })
            })
          })
        }, Promise.resolve())
      } else {
        response.end('404 NOT FOUND! ')}}}}const app = new Application()
const addTimestamp = (req, res, next) = > {
  setTimeout(() = > {
    this.timestamp = Date.now()
    next()
  }, 3000)
}
app.get('/', addTimestamp, (req, res) = > {
  res.end('Hello World! ' + this.timestamp)
})
app.get('/about', addTimestamp, (req, res) = > {
  res.end('Hello About! ' + this.timestamp)
})
app.post('/', addTimestamp, (req, res) = > {
  res.end('Post Method! ' + this.timestamp)
})
http
  .createServer(app.use())
  .listen(
    8888.() = > {
      console.log('8888' listening 127.0.0.1.)})Copy the code

1.4 Lower the user’s mind

But there are still a few minor flaws, users are always creating promises, users may want to be more brainless, so we expose the user to a next method, no matter where we execute next it will go to the next handler, isn’t that beautiful!!

class Application {
// ...
  use() {
    return (request, response) = > {
      const { url: path, method } = request
      if (
        this.$handlers.has(path) &&
        this.$handlers.get(path).has(method)
      ) {
        const _handlers = this.$handlers.get(path).get(method)
        _handlers.reduce((pre, _handler) = > {
          return pre.then(() = > {
            return new Promise(resolve= > {
             // Expose the next method and let the user decide when to go to the next handler
              _handler.call({}, request, response, () = > {
                resolve()
              })
            })
          })
        }, Promise.resolve())
      } else {
        response.end('404 NOT FOUND! ')}}}}// ...
const addTimestamp = (req, res, next) = > {
  setTimeout(() = > {
    this.timestamp = new Date()
    next()
  }, 3000)}Copy the code

2 Koa core source code analysis

Along the way, the above code has basically implemented a simple middleware framework that allows users to customize the middleware and then go to the next handler in the business logic through Next (), making it clearer to integrate the business process. But it can only promote the execution of middleware, there is no way to jump out of the middleware to give priority to other middleware. For example, in KOA, a middleware would look something like this:

const Koa = require('koa');
let app = new Koa();
const middleware1 = async (ctx, next) => { 
  console.log(1); 
  await next();  
  console.log(2);   
}
const middleware2 = async (ctx, next) => { 
  console.log(3); 
  await next();  
  console.log(4);   
}
const middleware3 = async (ctx, next) => { 
  console.log(5); 
  await next();  
  console.log(6);   
}
app.use(middleware1);
app.use(middleware2);
app.use(middleware3);
app.use(async(ctx, next) => {
  ctx.body = 'hello world'
})
app.listen(8888)
Copy the code

You can see that the console output is in the order of 1, 3, 5, 6, 4, 2, which is the classic KOA Onion model.

As we go through koA’s source code step by step, we can see that there are only four files in total, which adds up to just over 1000 lines of code if the comments are removed.

file function
applicaiton.js An entry to the KOA program that manages and invokes the middleware, handles callbacks to http.createserver, and proxies the requested request and response to the context
request.js A wrapper around the Request in the http.createserver callback, various getters, setters, and additional properties
response.js A wrapper around the response in the http.createserver callback, various getters, setters, and additional properties
context.js Proxy request and Response, and expose some functionality externally

When creating a Koa instance, Koa doesn’t really do much, setting up some of the instance’s configuration, initializing the middleware queues, and inheriting context, Request, and Response with Object.create.

2.1 the constructor

constructor(options) {
super(a);// The various configurations of the instance, do not worry too much
  options = options || {};
  this.proxy = options.proxy || false;
  this.subdomainOffset = options.subdomainOffset || 2;
  this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
  this.maxIpsCount = options.maxIpsCount || 0;
  this.env = options.env || process.env.NODE_ENV || 'development';
  if (options.keys) this.keys = options.keys;
// The most important instance attribute is used to store the middle
  this.middleware = [];
// Inherits objects from the other three files
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
}
Copy the code

Because Koa is only used for middleware integration and listening for request responses, the two instance methods of Koa that we are most interested in are use and Listen. One is used to register middleware and one is used to start services and listen on ports.

2.2 the use

The functionality is as simple as registering middleware and pushing it into the instance properties Middleware list.

use(fn) {
  if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
// Use the co library to convert the generator function, v3 will remove it and use promise and async directly... await
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || The '-');
  this.middleware.push(fn);
// Middleware app.use(XXX).use(XXX)...
  return this;
}
Copy the code

2.3 listen

It is very simple to implement, just call http.createserver directly to create the service and perform some operations of Server.listen directly. A slightly special one is that the createServer passes in arguments that are returned by calling the instance method callback.

listen(. args) {
  debug('listen');
// Create a service
  const server = http.createServer(this.callback());
// To pass through the parameters, execute the HTTP module server.listen
  returnserver.listen(... args); }Copy the code

2.4 the callback

  • callcomposeMethod to convert all middleware toPromiseExecutes, and returns an execution function.
  • Call the parent classEmitterIn thelistenerCountMethod to determine whether a user is registerederrorListener for the event, if noneerrorEvent registrationonerrorMethods.
  • Define the incomingcreateServerThis handler has two input arguments, respectivelyrequestresponse, by callingcreateContextMethods therequestresponseEncapsulated intoctxObject, and then putctxAnd the execution function of the first stepfnThe incominghandleRequestMethods.
callback() {
Koa-compose, the core of the Onion model, will be explained later on when to implement the transition middleware.
  const fn = compose(this.middleware);
// From Emitter, if there are no listeners for error events, register the default event listener method onError for error events
  if (!this.listenerCount('error')) this.on('error'.this.onerror);
// 
  const handleRequest = (req, res) = > {
// Call createContext to encapsulate req and RES as CTX objects
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };
  return handleRequest;
}
Copy the code

2.5 createContext

CreateContext encapsulates objects exposed in context, Request, and Response files and adds app, REq, and RES to facilitate CTX access.

createContext(req, res) {
  const context = Object.create(this.context);
  const request = context.request = Object.create(this.request);
  const response = context.response = Object.create(this.response);
  context.app = request.app = response.app = this;
  context.req = request.req = response.req = req;
  context.res = request.res = response.res = res;
  request.ctx = response.ctx = context;
  request.response = response;
  response.request = request;
  context.originalUrl = request.originalUrl = req.url;
  context.state = {};
  return context;
}
Copy the code

2.6 handleRequest

  • Get res, default the status to 404
  • Define failed callback functions and middleware executes successful callback functions, where the failed callback function is calledcontextIn theonerrorFunction, but eventually trigger app registrationonerrorFunctions; Successful callback function callrespondMethod, readctxInformation, write data toresAnd respond to the request.
  • useon-finishedThe module ensures that the corresponding callback function is executed when a stream is closed, completed, and reported as an error.
  • Executing middleware functionsfnMiddleware, similar to thePromise.allWhen all the middleware processes are successful, the command is executedhandleResponseOtherwise, an exception is caught.
handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err= > ctx.onerror(err);
  const handleResponse = () = > respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code

3 Koa-compose

Koa-compose source is very brief:

  • First check the validity of the input parameter, and finally return a function.
  • This function is used internallyindexLogs the current execution as an identity in the middle and returns the execution from the first middlewaredispatchResults. If a middleware executes internally multiple timesnext()Method, the value of I is equal toindex, an error will be reportedrejectIt off.
  • According to theindexTakes the middleware from the middleware list and willcontextdispatch(i + 1)Middleware entry parameterctxnextIncoming when the middleware executesnext()Method, the next middleware will be executed in order, and the current middleware will be placed on the execution stack. Finally, when I is equal to the length of the middleware array, i.e. there is no other middleware, the parameter will be enterednext(undefined in Koa source) to fn, which is undefined and returns nullresolvedThe state of thepromise.
  • When the core middleware execution is complete, it will automatically triggerawaitWork down, start executing the last middleware, and you end up with an onion model that starts from the outside in and then the inside out.
// The incoming parameter is a list of middleware and the return value is a function
function compose (middleware) {
// Check the validity of the middle
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
  for (const fn of middleware) {
    if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/ / core
  return function (context, next) {
// Set the initial index value
    let index = -1
// Execute dispatch immediately, passing in 0, and return the result
    return dispatch(0)
    function dispatch (i) {
// Prevent multiple calls to next in the same middleware
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
// Take the ith middleware from the middleware list and assign it to fn
      let fn = middleware[i]
// Next is always undefined for Koa source code.
      if (i === middleware.length) fn = next
// There is no executable middleware
      if(! fn)return Promise.resolve()
      try {
All implements Promise recursively by exposing the next callback function to ensure that the order of execution of the middleware meets the characteristics of the stack
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

4 Koa-router

Get (‘/:userName’, (res, req) => {/* XXXX */}). At this point you can introduce the KOA-Router middleware and use it as follows.

const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const router = new Router()
router.get('/'.async ctx => {
  ctx.body = 'Hello World! '
})
router.get('/:userName'.async ctx => {
  ctx.body = `Hello ${ctx.params.userName}! `
})
app
  .use(router.routes())
  .use(router.allowedMethods())
  .listen(8888)
Copy the code

The koa-router source code is placed in the lib folder, just two files:

file function
layer.js Internally, various regular expressions are used to obtain the corresponding data from the input parameters, storing the requested route, method, corresponding regular match of the route, parameters in the route, and middleware corresponding to the route
router.js The specific implementation of the Router provides exposed registration methods such as GET and POST, and middleware for processing routes
// Register routes and bind middleware
Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};
  const router = this;
  const stack = this.stack;
// Support multiple PAth-bound middleware
  if (Array.isArray(path)) {
    for (let i = 0; i < path.length; i++) {
      const curPath = path[i];
      router.register.call(router, curPath, methods, middleware, opts);
    }
    return this;
  }
// Create a route
  const route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true.name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false.strict: opts.strict || this.opts.strict || false.prefix: opts.prefix || this.opts.prefix || "".ignoreCaptures: opts.ignoreCaptures
  });
  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }
// Add middleware parameters
  for (let i = 0; i < Object.keys(this.params).length; i++) {
    const param = Object.keys(this.params)[i];
    route.param(param, this.params[param]);
  }
  stack.push(route);
  debug('defined route %s %s', route.methods, route.path);
  return route;
};
// Expose methods such as get and POST
for (let i = 0; i < methods.length; i++) {
  function setMethodVerb(method) {
    Router.prototype[method] = function(name, path, middleware) {
      if (typeof path === "string" || path instanceof RegExp) {
        middleware = Array.prototype.slice.call(arguments.2);
      } else {
        middleware = Array.prototype.slice.call(arguments.1);
        path = name;
        name = null;
      }
      this.register(path, [method], middleware, {
        name: name
      });
      return this;
    };
  }
  setMethodVerb(methods[i]);
}
Copy the code

5 Related Documents

  • koa onion model
  • En.wikipedia.org/wiki/Strate…
  • Robdodson. Me/posts/javas…

❤️ Thank you

That is all the content of this sharing. I hope it will help you

Don’t forget to share, like and bookmark your favorite things.