preface

Koa is the most commonly used Web framework for building NodeJS services. Its source code is not complex, but it does achieve two core points:

  • Flow control in Middleware, also known as the Onion model
  • Mount Request (IncomingMessage Object) and Response (ServerResponse Object) from the http.createserver method into the context (CTX)

Whether you are preparing for an interview or want to improve your coding skills, understanding Koa source code is a good choice. So, our goal is to:

Focus only on core function points, minimize code, and implement a Koa yourself.

The preparatory work

First, make sure your working directory is as follows:

├ ─ ─ lib │ ├ ─ ─ application. Js │ ├ ─ ─ the context, js │ ├ ─ ─ request. Js │ └ ─ ─ the response. The js └ ─ ─ app. JsCopy the code

Next, write an entry-level Hello World service:

// app.js
const Koa = require("./lib/application.js");
const app = new Koa();

app.use(async ctx => {
  ctx.body = "Hello World";
});

app.listen(3000);
Copy the code

Of course, the service is temporarily unavailable.

Application

As an entry file, application.js exports a Class (essentially a constructor) that is used to create Koa instances.

const http = require("http");

class Koa {
  constructor() {
    // Store middleware functions
    this.middleware = [];
  }
  use(fn) {
    this.middleware.push(fn);
    // chain call
    return this; } listen(... args) {const server = http.createServer(this.callback());
    returnserver.listen(... args); } callback() {// Handle specific requests and responses...}}module.exports = Koa;
Copy the code

Since NodeJS natively provides fewer methods for request and response objects, Koa has extended these two objects accordingly.

And to simplify the API, encapsulate and mount these two objects into Koa’s session Context.

Request

// request.js
module.exports = {
  get url() {
    return this.req.url;
  },
  get method() {
    return this.req.method; }};Copy the code

Response

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

Context

The Context object sets the getters and setters for the properties being accessed and delegates them to the Request and Response objects.

// 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

When setting up too many accessors, you can write it another way:

// context.js
const reqGetters = ["url"."method"],
  resAccess = ["body"],
  proto = {};

for (let name of reqGetters) {
  proto.__defineGetter__(name, function() {
    return this["request"][name];
  });
}
for (let name of resAccess) {
  proto.__defineGetter__(name, function() {
    return this["response"][name];
  });
  proto.__defineSetter__(name, function(val) {
    return (this["response"][name] = val);
  });
}
module.exports = proto;
Copy the code

At the same time, change application.js

const http = require("http");
const request = require("./request");
const response = require("./response");
const context = require("./context");

class Koa {
  constructor() {
    this.request = request;
    this.response = response;
    this.context = context;
    // Store middleware functions
    this.middleware = [];
  }
  callback() {
    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);
      this.middleware[0](ctx);
      res.end(ctx.body);
    };
    return handleRequest;
  }
  createContext(req, res) {
    // Mount the expanded request and response to the context
    const context = Object.create(this.context);
    const request = (context.request = Object.create(this.request));
    const response = (context.response = Object.create(this.response));
    // Mount the native request and response
    context.req = request.req = req;
    context.res = response.res = res;

    returncontext; }}module.exports = Koa;
Copy the code

When I run app.js, the browser says “Hello World”

The middleware

The Koa middleware mechanism stems from the compose function: it is a higher-order function that combines sequentially executed functions into a single function, with the return value of the inner function as an argument to the outer function.

For example 🌰 :

function lower(str) {
  return str.toLowerCase();
}

function join(arr) {
  return arr.join(",");
}

function padStart(str) {
  return str.padStart(str.length + 6."apple,");
}

// I want to call join() lower() padStart() sequentially

padStart(lower(join(["BANANA"."ORANGE")));// apple,banana,orange
Copy the code

Of course, you’ll need a compose function to do this automatically, not manually.

function compose(. funcs) {
  return args= > funcs.reduceRight((composed, f) = > f(composed), args);
}

const fn = compose(padStart, lower, join);
fn(["BANANA"."ORANGE"]);
// apple,banana,orange
Copy the code

Change your app.js code:

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

Koa expects the terminal to print: 1, 2, 3, 4, 5, 6, so you need to implement Koa’s compose function for multiple middleware calls sequentially and accept next() to call the next middleware function. Request due to the existence of next(), it will recursively enter the next middleware function to be processed until there is no next middleware function, the end of recursion, and the execution stack will return to the previous middleware function to continue processing until the execution stack is empty and Response will be returned.

Moving on to the source code, you’ll see that Koa uses the Koa-compose library and that the code is minimal.

Add the following to your application.js:

class Boa {
  callback() {
    const fn = this.compose(this.middleware);
    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    const handleResponse = (a)= > this.respond(ctx);
    return fnMiddleware(ctx).then(handleResponse);
  }
  respond(ctx) {
    const { res, body } = ctx;
    res.end(body === undefined ? "Not Found" : body);
  }
  compose(middleware) {
    return function(context, next) {
      return dispatch(0);
      function dispatch(i) {
        let fn = middleware[i];
        // All middleware functions are completed, fn = undefined, end recursion
        if (i === middleware.length) fn = next;
        if(! fn)return Promise.resolve();
        // Call the next middleware function recursively
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); }}; }}Copy the code

To better understand the onion model, you can use VSCode’s Run panel for debugging.

return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); // Set a breakpoint here
Copy the code

And configure.vscode/launch.json in the root directory

{
  // Use IntelliSense to learn about possible attributes.
  // Hover to view descriptions of existing attributes.
  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Launch Program",
      "skipFiles": ["<node_internals>/**"],
      "program": "${workspaceFolder}\\app.js"
    }
  ]
}
Copy the code

Run F5, access http://localhost:3000, hit the breakpoint, use Step Over and Step Into to run the program inside the third next(), and you should see the call stack as shown below:

Koa-compose is implemented differently from Compose, but the core idea is the same: Once dispatch(0) is called, the middleware function automatically executes recursively, calling Dispatch (1) dispatch(2) Dispatch (3) in turn, while in compose() :

const fn = compose(padStart, lower, join);
fn(["BANANA"."ORANGE"]);
// Once fn() is called, it executes recursively, calling join(), lower(), padStart() in sequence
Copy the code

Compose’s data flow is unidirectional, whereas koA-compose’s next() approach makes Koa’s data flow bidirectional (from the outside in, and from the inside out), as if you were using a needle 💉 through an onion 🧅, going through the skin, into the core, and out of the skin. This is where Koa middleware is unique (the “onion model”) : from Compose, better than compose.

Deal with asynchronous

NodeJS is full of I/O and network requests, which are asynchronous requests.

Koa uses async functions and await operators, discarding callback functions and handling asynchronous operations in a more elegant way.

Change your app.js code:

const fs = require("fs");
const promisify = require("util").promisify;

// Asynchronously read the contents of demo. TXT in the root directory
const readTxt = async() = > {const promisifyReadFile = promisify(fs.readFile);
  const data = await promisifyReadFile("./demo.txt", { encoding: "utf8" });
  return data ? data : "no content";
};

app
  .use((ctx, next) = > {
    console.log("start");
    next();
    console.log("end");
  })
  .use(async (ctx, next) => {
    const data = await next();
    ctx.body = data;
  })
  .use(readTxt);
Copy the code

Create demo.txt in the root directory

you are the best.
Copy the code

Your application now includes a new middleware function, readTxt(), whose internal fs.readfile () falls under the asynchronous I/O category.

According to the guiding idea of Koa: use async and await syntactic sugar to write synchronous code to solve asynchronous operations.

Run app.js, the terminal prints start end, browser access shows you are the best.

At this point, you have implemented all the core functions of Koa 🎉

The source address