preface

Koa is a NodeJs-based web development framework that has the advantages of being small and sophisticated compared to its sibling Express.

By using async functions, Koa helps you discard callbacks and greatly enhances error handling. Koa doesn’t bundle any middleware, but rather provides an elegant way to write server-side applications quickly and happily.

Since blowing so hard, what is the mechanism behind it? This article will follow the example to achieve a simple version of KOA, build a basic wheel to deepen understanding, if you are still not familiar with KOA, then it is very necessary to spend a little time on the official website 🔖

Source structure

As an excellent 31K Star project, the source code is so simple that it consists of just four files, which add up to less than 2,000 lines of code.

Where, application is the entry file, context.js is related to the context object, request.js is related to the request object, and response.js is related to the response object.

The first step is to create the above four files in an empty folder and start the parody

Basis function

Let’s review the use of KOA:

const Koa = require('koa');
const app = new Koa();

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

app.listen(3000); 

Copy the code

These lines of code do three things:

  1. We created our HTTP server using the LISTEN method, port 3000
  2. A KOA middleware is registered through the use method
  3. Encapsulate CTX as the middleware parameter on which res and REQ can be obtained

We know that KOA is based on NodeJS, so what we need to do in Application.js is to implement the same functionality with NodeJS

Start the service

// application.js
class Koa {
  / / initialization
  constructor() {}
  // Register the middleware
  use() {}
  / / to monitor
  listen(. args) {
    http
      .createServer(function (req, res) {
        // console.log(res, req);
        res.end('hello world')
      })
      .listen(...args);
  }
}
Copy the code

Essentially, we created a server using node’s HTTP module. Since server’s LISTEN method takes multiple arguments, we simply deconstructed all the arguments. Create test.js as the test file. If the page for accessing local port 3000 is displayed properly, the service is successfully started.

// test.js
let Koa = require(".. /lib/application");
let app = new Koa();
app.use((ctx, next) = > {
  console.log('This is middleware.');
});
app.listen(3000.() = > {
  console.log("app listen in port 3000");
});
Copy the code

Registration middleware

We did not implement the use method above. The use method takes a function as a parameter and can register multiple middleware, so there should be an array to manage it. Function execution requires us to receive CTX as a parameter. This CTX is the context object constructed by KOA for us. We can get req and RES objects through CTX

class Koa { constructor() { this.middleWares = []; } use(cb) { if (typeof cb ! == "function") throw new TypeError("argument must be a function"); this.middleWares.push(cb); } handleRequest(req, res) { this.middleWares.forEach((cb) => { cb({ req, res }); }); } listen(... args) { http.createServer(this.handleRequest.bind(this)).listen(... args); }}Copy the code

In the above code, we encapsulate req and res into context CTX, which is used as the execution parameter of the middleware. However, CTX in KOA binds many requests and related data and methods, such as cctx.request.req, cctx.path, cctx.query, cctX.body, etc. It greatly facilitates development, so the next step is to further expand the properties of CTX.

Delegate pattern

Koa Context encapsulates node’s request and response objects into a single object, providing a number of useful ways to write Web applications and apis. This is useful for adding CTX to properties or methods used throughout the application, and relying more on CTX can be considered an anti-pattern

For convenience many context accessors and methods delegate directly to their ctx.request or ctx.response object, such as ctx.type and ctx.length delegate to the response object, Ctx. path and ctx.method are delegated to the request.

Let’s review some common usages, using the example of getting a request URL:

  1. Ctx.req. Url Native req
  2. Ctx.request.req. Url Native req
  3. Ctx.request. url is encapsulated in the URL of the request
  4. Ctx. url Indicates the url of the proxy on CTX, which is equivalent to ctX.request. url

Ctx. request is a custom request with attributes such as query and path for easy access:

// request.js
let url = require("url");
let request = {
  get url() {
    // this => ctx.request
    return this.req.url;
  },
  get path() {
    return url.parse(this.req.url).pathname;
  },
  get query() {
    return url.parse(this.req.url).query; }};// Equivalent to the following
// Object.defineProperty(request, "url", {
// get() {
// return this.req.url;
/ /},
// });
module.exports = request;
Copy the code

Again, in order to proxy these properties on CTX, we modify context.js

let proto = {};

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

function defineSetter(target, key) {
  proto.__defineSetter__(key, function (val) {
    this[target][key] = val; 
  });
}

defineGetter("request"."url");
defineSetter("response"."body"); // The agent, in fact, assigns ctx.response.body
module.exports = proto;

Copy the code

__defineGetter__ is not recommended by MDN. For some reason the source proxy uses this implementation. Object. DefineProperty or get is preferred.

  let context = require("./context");
  let request = require("./request");
  let response = require("./response");
  
class Koa {
  constructor() {
    this.middleWares = [];

    this.context = context;
    this.request = request;
    this.response = response;
  }
  // ...
  handleRequest(req, res) {
    let ctx = this.createContext(req, res);
    this.middleWares.forEach((cb) = > {
      cb(ctx);
    });
  }
  // Construct the context CTX
  createContext(req, res) {
    // Use prototype chain inheritance to avoid affecting internal properties and methods
    let ctx = Object.create(this.context);
    // 
    ctx.request = Object.create(this.request);
    ctx.response = Object.create(this.response);
    ctx.app = ctx.request.app = ctx.response.app = this // Mount the KOA instance
    ctx.req = ctx.request.req = req;
    ctx.res = ctx.response.res = res;
    ctx.request.ctx = ctx.response.ctx = ctx;
    ctx.req = req;
    ctx.res = res;

    returnctx; }}Copy the code

The test file is as follows, access the URL property, and after running the output is consistent, as we expected. When the path attribute is accessed, the first two will print undefined because the REq object does not have that attribute.

// test.js
let Koa = require("./lib/application.js");

let app = new Koa();

app.use((ctx, next) = > {
  // ctx.req.url is equivalent to ctx.request.req.url(node native req)
  // ctx.url is equivalent to ctx.request.url(KOA encapsulated request)
  console.log(ctx.req.url);
  console.log(ctx.request.req.url);
  console.log(ctx.request.url);
  console.log(ctx.url);
  ctx.body = "hello world";
});

app.listen(3000);
Copy the code

At this time, the page does not return Hello World, we still need to transform Response.js. After the middleware execution is completed, if ctx.body has been assigned, we should change the res status code to 200 and call res.end() to output this value on the page, otherwise we should return an error with the status code of 404.

// response.js
let response = {
  set body(val) {
    if (typeofval ! = ="string") return;
    this.res.statusCode = 200;
    this._body = val;
  },
  get body() {
    return this._body; }};module.exports = response;
Copy the code
 handleRequest(req, res) {
    res.statusCode = 404;
    let ctx = this.createContext(req, res);
    this.middleWares.forEach((cb) = > {
      cb(ctx);
    });
    let body = ctx.body;
    if (typeof body == "undefined") {
      res.end("404 Not Found");
    } else{ res.end(ctx.body); }}Copy the code

Middleware mechanism (Onion Model)

When one middleware calls next(), the function pauses and passes control to the next middleware defined. When no more middleware executes downstream, the stack unwinds and each middleware resumes performing its upstream behavior.

Calling next at execution time gives the function execution authority to the next middleware, which then executes itself in return, creating a paperclip cascade of code. This is the classic onion model. In the middleware itself, we can also use the async function to make it easier to transition from asynchronous to synchronous. Let’s use a code example:

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

The above code will eventually be printed in the order 1, 3, 5, 6, 4, 2, which is equivalent to the following:

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

The onion model avoids this writing method of deep nesting and is more intuitive and elegant. Comparing the two pieces of code before and after, the parameter next is actually equivalent to the next middleware, executing next () is equivalent to entering the next function execution stack, layer by layer to the last one, when the innermost function execution is finished, the execution stack will pop up again.

We need a function to implement sequential execution and support asynchronism, and the compose function is the core point of KOA

compose(ctx, middwares) {
    function dispatch(index) {
      if (index == middwares.length) return Promise.resolve();
      return Promise.resolve(middwares[index](ctx, () = > dispatch(index + 1)));
    }

    return dispatch(0);
}
Copy the code

Analyze the code:

  1. The compose function takes an array of middleware and a CTX object as parameters, and uses a recursive function to string the middleware together, so the next parameter of our callback is actually the arrow function ()=> Dispatch (index + 1), and dispatch(0) starts to enter the first middleware, Next executes by calling Dispatch (index + 1) to go to the next middleware.

  2. When the callback is async, it returns a Promise object. When the promise. resolve argument is a Promise, the state is determined by the result of the Promise’s execution. Wrap each callback as a Promise to achieve asynchrony. With these features we can handle asynchronous callback nesting with synchronous logic.

For example, for example, compose: for example, compose: for example, compose:

handleRequest(req,res){
    res.statusCode = 404
    let ctx = this.createContext(req, res)
    let promise = this.compose(this.middlewares, ctx)
    promise.then(() = > {
        if (typeof ctx.body == 'object') {
            res.setHeader('Content-Type'.'application/json; charset=utf8')
            res.end(JSON.stringify(ctx.body))
        } else if (ctx.body instanceof Stream) {
            ctx.body.pipe(res)
        } else if (typeof ctx.body === 'string') {
            res.setHeader('Content-Type'.'text/html; charset=utf8')
            res.end(ctx.body)
        } else {
            res.end('Not found')}}}Copy the code

Error handling

Koa has a set of error handling mechanisms that listen for instance error events. The main thing is to throw err to app for processing when error occurs. In order to pass error information, we make KOA inherit EventEmitter of events module, so that we can use Emit on to distribute error events inside the program

let EventEmitter = require('events').EventEmitter
class Koa extends EventEmitter {... }Copy the code

Improve the error handling mechanism, modify the CTX attribute and handle it accordingly after the error occurs

handleRequest(req,res){
    res.statusCode = 404
    let ctx = this.createContext(req, res)
    this.on('error'.this.onerror);  // Error event handling
    let promise = this.compose(this.middlewares, ctx)
    promise.then(() = >{... }).catch(err= > {
        this.emit('error', err)  // Throw on error
        res.statusCode = 500
        res.end('server error')})}// Uniform processing of errors by the program
// Print the error stack
// Set header to err. Headers, MSG to err. Message, etc
onerror(err){...console.error(err);
}
Copy the code

conclusion

So far, our simple wheel has realized several of the advantages of KOA, and you can see that the key points of KOA focus on the use of CTX objects and middleware. By proxy mode, the native res and req method properties are proxied to CTX, and then koA’s built-in request and reponse provide some unique properties and convenient operation methods. The middleware uses compose to concatenate the functions. And make the logic cleaner with async await syntax sugar.

If there is any question or the author does not understand the right place welcome everyone to criticize and correct, common progress 🔥🔥

Reference link: juejin.cn/post/684490…