Hello everyone, I am Garfield 🐱, here I wish you a happy New Year, the Year of the Tiger, tiger Tiger live!

Koa is one of the most popular server-side frameworks, as anyone who has used Nodejs knows. Compared with Express’s Al in One idea, Koa’s source code is very simple, the core library only contains a middleware kernel, and other functions need to introduce third-party middleware libraries to achieve. Koa’s source code is small and easy to understand, with only four files, making it a great source for learning.

1. Overall Koa structure

One of the main features of the Koa architecture is the middleware mechanism and the Onion model, so to speak, all of the logic in Koa is implemented through various middleware. For middleware, there is a picture on the official website:

From this figure, we can see that the Koa middleware mechanism will execute the outermost middleware first every time a request comes in, and then enter the middleware of the next layer when it encounters the next method. When all the middleware is executed, the entire request logic is completed by executing it layer by layer.

It’s a little abstract, so let’s look at a demo:

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

app.use(async (ctx, next) => {
  console.log('1');
  await next();
  console.log('2');
});

app.use(async (ctx, next) => {
  console.log('3');
  await next();
  console.log('4');

  ctx.body = 'hello, world';
});

app.listen(3000);

// Console output sequence: 1, 3, 4, 2
Copy the code

As you can see from the above code, Koa itself is a class, a basic framework, with two instance methods on it: use for registering middleware and LISTEN for starting services. For each middleware, you can receive two parameters, CTX and Next, where CTX represents Context, which is an object wrapped around HTTP requests and responses, and next represents the next middleware, which is called next.

According to this idea, we can build the following basic framework:

import http from "node:http";

class Application {
  constructor() {
    this.middlewares = [];
  }
  
  use(fn) {
    this.middlewares.push(fn);
    // Return an instance of itself to implement the chain call
    return this;
  }
  
  listen(. args) {
    const server = http.createServer(this.callback());
    returnserver.listen(... args); }callback() {
    // TODO: implement this}}Copy the code

As you can see from the above code, the use method is actually very simple, which is to push the received function into the middleware array, and finally return its own instance, which is used to implement the chain call. The Listen method is as simple as launching the service through http.createserver, and nothing to say. The key logic is in this.callback().

HTTP. CreateServer accepts a callback that takes req and res:

http.createServer((req, res) = > {
  // ...
})
Copy the code

By this logic, this.callback() must also return a function that contains all of the server-side processing logic. We know that the processing logic in Koa is organized in the form of middleware, so we need to implement the Onion model and implement the middleware scheduling. Let’s take a look at the source code:

callback () {
  The compose function is imported from the koa-compose library
  // Integrate middleware into a function
  const fn = compose(this.middleware)

  const handleRequest = (req, res) = > {
    // Create the Context object
    const ctx = this.createContext(req, res)
    // This side handles network requests
    return this.handleRequest(ctx, fn)
  }

  return handleRequest
}
Copy the code

Here we’ll focus on middleware and the Onion model, which is the compose function, Context and this.handlerequest. We know that the Koa middleware does not receive native REq/RES, but a CTX object, so let’s implement Contenxt simply:

createContext(req, res) {
  const context = Object.create(this.context);
  context.app = this;
  context.req = req;
  context.res = res;
}
Copy the code

HandleRequest is responsible for passing in the CTX object, calling the middleware, and responding when the middleware completes execution, with default exception handling:

handleRequest (ctx, fnMiddleware) {
  const res = ctx.res
  res.statusCode = 404
  const onerror = err= > ctx.onerror(err)
  const handleResponse = () = > respond(ctx)
  onFinished(res, onerror)
  // Call the middleware, and call handleResponse in response to the completion of all middleware execution
  return fnMiddleware(ctx).then(handleResponse).catch(onerror)
}
Copy the code

Respond is a helper function that simply responds to requests:

function respond(ctx) {
  const res = ctx.res;
  let body = ctx.body;
  
  // If the body is a stream, respond with the pipe method
  if (body instanceof Stream) return body.pipe(res)
  
  body = JSON.stringify(body)
  return res.end(body);
}
Copy the code

As an example, we know that the http.createServer callback accepts two parameters, req and res, which are stream. Req is Readable and RES is Writable. It can be written like this:

http.createServer((req, res) = > {
  req.pipe(res);
}).listen(3000);
Copy the code

Readable writing to Writable requires listening for data events at the same time as writing, but there is a stream backpressure problem that requires listening for the drain event to continue writing until the Writable buffer is empty. The pipe method was used to solve the stream backpressure problem

So Koa has actually added support for streams to respond directly to stream-type data:

import { Readable } from "node:stream";

app.use(async (ctx, next) => {
  // This will respond directly
  ctx.body = Readable.from("2333");
})
Copy the code

This is how the Koa process works. Let’s focus on the compose function implementation.

2. Koa-compose

Koa-compose is essentially a single library that plays a key role in implementing the Onion model, tying middleware together and merging it into a single function that is easy to call externally.

Compose is one of the most important concepts in functional programming, and is also used in Redux. For example, compose is composed by combining a series of functions into a single function. The specific implementation is not fixed. We have previously written a simple compose:

function compose<T> (. middlewares: ((arg: T) => T)[]) : (initValue: T) = >T {
	return (initValue) = > {
		return middlewares.reduce((accu, cur) = > cur(accu), initValue)
	}
}
Copy the code

In Koa, you actually implement a compose for your own needs. Before looking at the source code, we can first consider how to implement. For each middleware, there are the following apis:

middleware(ctx, next)
Copy the code

After each next function is called, the next middleware is executed. We can extract the next function, but find next in the next function. What should we do?

const next = () = > middlewares[i](ctx, next)
Copy the code

That’s right, use a recursive implementation of middleware logic to connect middleware together:

const dispatch = (i) = > {
  return middlewares[i](ctx, () = > dispatch(i + 1));
}
Copy the code

There is also a condition to end the recursion, which returns when the last middleware function calls next:

const dispatch = (i) = > {
  const middleware = middlewares[i];
  if (i === middlewares.length) return;
  return middleware(ctx, () = > dispatch(i + 1));
}
Copy the code

The final compose function looks like this:

function compose(middlewares = []) {
  return ctx= > {
    const dispatch = (i) = > {
      const middleware = middlewares[i];
      if (i === middlewares.length) return;
      return middleware(ctx, () = > dispatch(i + 1));
    }
    return dispatch(0); }}Copy the code

Our own implementation of compose is relatively simple, with additional type validation and an extra layer of Promise wrapped around the return value:

function compose (middleware) {
  // Check parameters, throw an exception if it is not an array
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
  // Walk through the array, throw an exception if it is not a function
  for (const fn of middleware) {
    if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}// compose returns a function that can also be passed into middleware
  // The middleware will execute at the end
  return function (context, next) {
    return dispatch(0) // Start with the first middleware
    function dispatch (i) {
      let fn = middleware[i]
      // End the recursion
      // If compose returns a function that is passed into the middleware
      // Take the middleware out and execute it
      if (i === middleware.length) fn = next
      if(! fn)return Promise.resolve()
      try {
        // Execute middleware
        / / this way ` dispatch. The bind () ` effect and ` () = > dispatch () ` consistent
        // return a function to be executed
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))}catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

3. Common Koa middleware

Having implemented the Compose middleware mechanism above, let’s take a look at how to implement some common middleware.

1) Exception handling

In the backend framework, exception handling is very important, without exception handling, a random error can bring down the entire service. Therefore, exception handling is provided by default in Koa, but many times we need to do some exception reporting tasks, which need to be caught before the framework layer exception catching, we can encapsulate an exception handling middleware:

app.use(async (ctx, next) => {
  try {
    await next();
  } catch(err) {
    console.log(err);
    ctx.res.statusCode = 500;
    ctx.res.write("Internel Server Error"); }})Copy the code

Note that exception catching middleware needs to be placed at the top of all middleware

2) logger

Implementing a Logger that records how long it took to process the current request is also simple:

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  // This side is based on the Context implemented above
  const { method, url } = ctx.req;
  console.log(`${method} ${url} - ${ms}ms`);
})
Copy the code

3) koa-static

Koa-static is a middleware for building a static resource server. In fact, static resource servers should be familiar to the front end. For example, Nginx is a typical static resource server, and Webpack-dev-server is a static resource server. The workflow of a static resource server is simple: get the file path based on the request address, then get the corresponding file based on the path, return the file content as the response body, and set the cache response header.

Koa-static is very simple to use:

const Koa = require('koa');
const serve = require('koa-static');

const app = new Koa();

app.use(serve('public'));

app.listen(3000.() = > {
    console.log('listening on port 3000');
});
Copy the code

Let’s take a look at the source code:

// Server can accept two arguments
// One is the path address and the other is the configuration item
function serve (root, opts) {
  // ocD indicates that this code can be changed to the default parameter value
  opts = Object.assign(Object.create(null), opts)
  // Resolve root to a valid path and add it to the configuration item
  opts.root = resolve(root)
  
  // If the requested path is a directory, the default is to fetch index.html
  if(opts.index ! = =false) opts.index = opts.index || 'index.html'

  // The return value is a Koa middleware
  return async function serve (ctx, next) {
    let done = false // Flags whether the file responded successfully

    if (ctx.method === 'HEAD' || ctx.method === 'GET') {
      try {
        // Invoke the koa-send response file
        // If the message is sent successfully, the path is returned
        done = await send(ctx, ctx.path, opts)
      } catch (err) {
        // Throw an exception if 400, 500, etc
        if(err.status ! = =404) {
          throw err
        }
      }
    }

    // If the file is successfully sent, the request is terminated
    // If not, let the middleware continue processing
    if(! done) {await next()
    }
  }
}
Copy the code

Koa-static encapsulates koA-send. It is worth noting, however, that the configuration item can pass an opt.defer parameter, which defaults to false. If true is passed, koa-static will have other middleware respond first, even if other middleware is written after koa-static. This essentially controls when the next method is called, and when defer = true is configured, koA-static calls Next first, letting the other middleware execute first, and then koa-static’s logic.

Ctx. body = fs.createreadStream (path) through the way to respond to the stream, plus a variety of path concatenation, 404 processing, cache response, etc., source code is also a file, Hundreds of lines of code

Github.com/koajs/send/…

4) router

In fact, the concept of routing was first proposed from the back end, where a request is entered and the Controller is processed according to the path of the request. Then there was the front and back end split architecture, and then there was the front end routing. Routing is therefore essential to a server framework. The @koa/ Router implementation actually references Express, so let’s see how to use it:

const Koa = require("koa");
const Router = require("@koa/router");
const bodyParser = require("koa-bodyparser");

const app = new Koa();
const router = new Router();

app.use(bodyParser());

router.get("/".(ctx) = > {
  ctx.body = "Hello World";
});

router.get("/api/users".(ctx) = > {
  const userList = [
    {
      id: 1.name: "dby".age: 12}]; ctx.body = userList; }); router.post("/api/users".async (ctx) => {
  // Using koa-bodyParser gets the body from ctx.request
  const postData = ctx.request.body;
  ctx.body = postData;
})

app.use(router.routes());

app.listen(3000);
Copy the code

The router[method] method is used to register routes, and the router.routes method is used to match routes.

The koA-Router is a class, but it is implemented in the source code as a constructor so that instance methods can be added dynamically (ES6 classes are static). There is an interesting point in the source code:

function Router() {
  // If a new call is not passed, it is automatically changed to a new call
  if(! (this instanceof Router)) return new Router();

  this.stack = [];
  
  // ...
}
Copy the code

We know that ES6 classes must be called via new, and an error will be reported if called directly. The constructor itself does not have the classCallCheck mechanism, so we need to determine for ourselves whether this is an instance of the Router:

function Router() {
  if(! (this instanceof Router)) {
    throw new Error("Router must call with new");
  }
  // ...
}
Copy the code

If you throw an exception directly, the execution of the entire program is interrupted, which is not very elegant. Router direct calls are automatically changed to new calls:

function Router() {
  if(! (this instanceof Router)) {
    return new Router();
  }
  // ...
}
Copy the code

Let’s look at how routes are registered:

for (let i = 0; i < methods.length; i++) {
  const method = methods[i];
  
  Router.prototype[method] = function(path, middleware) {
    // Support passing in multiple middleware
    // Turn middleware into an array
    // Use the remaining parameters in ES6... Middleware will do the trick
    middleware = Array.prototype.slice.call(arguments.1);

    // Call this.register to register the route
    this.register(path, [method], middleware);

    // return its own instance to implement the chain call
    return this;
  };
}
Copy the code

The methods here come from the library: github.com/jshttp/meth… HTTP request verb

Arguments objects are used mostly in ES5 syntax, with arguments remaining in ES6… Middleware will do the trick. By the way, don’t use arguments in TS, it will cause type derivation to fail

The this.register method also does a simple thing, creating a Layer object and pushing it into the stack array:

Router.prototype.register = function (path, methods, middleware) {
  const stack = this.stack;

  const route = new Layer(path, methods, middleware);

  stack.push(route);

  return route;
};
Copy the code

Layer is an object that looks like this:

class Layer {
  methods: string[];
  stack: middleware[];
  path: string;
  regexp: RegExp;
}
Copy the code

Router. routes = router.routes = router.routes The router.routes return value is actually a Koa middleware.

Router.prototype.routes = Router.prototype.middleware = function () {
  const router = this;

  // Middleware to be returned
  let dispatch = function dispatch(ctx, next) {
    const path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // Get all matched layers
    const matched = router.match(path, ctx.method);
    
    // Save all matched middleware
    let layerChain;

    // Attach the router instance to the CTX object for use by other KOA middleware
    ctx.router = router;

    // If none of the layers match, go back and execute the next Koa middleware
    if(! matched.route)return next();

    // Get the layer where all paths and methods match
    const matchedLayers = matched.pathAndMethod

    // Consolidate all stacks (i.e., middleware) on the layer into an array
    layerChain = matchedLayers.reduce(function(memo, layer) {
      returnmemo.concat(layer.stack); } []);// compose is the koa-compose library
    return compose(layerChain)(ctx, next);
  };

  return dispatch;
};
Copy the code

The router.match method above essentially iterates through all layers to find matched layers and returns an object like this:

type Matched = {
  path: Layer[]; // Only path matches layer
  pathAndMethod: Layer[]; // Layer with path and method matching
  route: boolean; // If pathAndMethod is not empty, the route is matched, and this attribute is true
}
Copy the code

And then we have the compose method. One thing to note here is that when we use routing, we usually don’t use the next method, although the function passed in can fetch it:

router.get("/".(ctx, next) = > {
  // We don't need to call next
  ctx.body = "Hello World";
});
Copy the code

But if we pass more than one callback to the same route, we must call next or the next callback will not go to:

router.get("/".(ctx, next) = > {
  console.log("2333");
  // Next must be called or the next callback will not be entered
  next();
});

router.get("/".(ctx, next) = > {
  ctx.body = "Hello World";
});
Copy the code

4. To summarize

In this article, we analyze the overall Koa architecture, the implementation mechanism of KOA-compose, the implementation of middleware and Onion model, as well as the implementation of some common middleware, and by the way, we also discuss some places worth learning from the source code. Hopefully it will help you write better and more elegant code.

reference

github.com/koajs/koa

Github.com/koajs/compo…

Github.com/koajs/stati…

Github.com/koajs/route…