preface

Koa 2.x is currently the most popular NodeJS framework. The source code for Koa 2.0 is very compact and does not pack as much functionality as Express. So most of the functionality is provided by the Koa development team (which is also a producer of Express) and community contributors to the middleware that Koa uses to implement the NodeJS wrapper feature. It is very simple to import the middleware and call Koa’s use method to use it in the appropriate place. This allows you to implement some functionality by manipulating CTX internally, and we’ll discuss how common middleware is implemented and how we can develop a Koa middleware for use by ourselves and others.

Koa’s onion model introduction

We will not analyze the implementation principle of the Onion model too much, but mainly analyze how the middleware works based on the usage of the API and the Onion model.

Characteristics of onion model

// introduce Koa const Koa = require("koa"); // Create service 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); }); app.use(async (ctx, next) => { console.log(5); await next(); console.log(6); }); // Listen app.listen(3000); // 1/3/5/6/4/2Copy the code

We know that Koa’s use method supports asynchrony, so in order to ensure normal code execution according to the order of onion model, we need to make the code wait when calling next, and then continue to execute after asynchrony, so we recommend using async/await in Koa. The introduced middleware is called in the use method, so we can analyze that each Koa middleware returns an async function.

Koa – BodyParser middleware simulation

The koA-BodyParser middleware converts our POST requests and form submission query strings into objects and attaches them to ctx.request.body. It is convenient for us to use the value on other middleware or interfaces. It should be installed in advance before use.

npm install koa koa-bodyparser

Koa-bodyparser is used as follows:

The use of the koa – bodyparser

const Koa = require("koa");
const bodyParser = require("koa-bodyparser"); const app = new Koa(); Use (bodyParser()); app.use(async (ctx, next) => {if (ctx.path === "/" && ctx.method === "POST"// With middleware, ctx.request.body attributes are automatically added to the post request data console.log(ctx.request.body); }}); app.listen(3000);Copy the code

According to the usage, we can see that the koA-BodyParser middleware actually introduces a function, which is executed in use. According to the characteristics of KOA, we infer that the koA-BodyParser function should return an async function. Here is the code for our simulated implementation.

File: my – koa – bodyparser. Js

const querystring = require("querystring");

module.exports = function bodyParser() {
    returnAsync (CTX, next) => {await new Promise((resolve, reject) => {// Store an array of dataletdataArr = []; // Receive data ctx.req.on("data", data => dataArr.push(data)); Ctx.req.on ("end", () => {// Get the requested data type JSON or formlet contentType = ctx.get("Content-Type"); // Get the data Buffer formatlet data = Buffer.concat(dataArr).toString();

                if (contentType === "application/x-www-form-urlencoded"Ctx.request.body ctx.request.body = queryString.parse (data); ctx.request.body = queryString.parse (data); }else if (contentType === "applaction/json"Ctx.request.body ctx.request.body = json.parse (data); ctx.request.body = json.parse (data); } // Execute the successful callback resolve(); }); }); // continue to await next(); }; };Copy the code

A few points to note in the code above are that the next call and why receiving data via stream, processing data, and hanging the data in ctx.request.body are in the Promise.

The first isnextWe know thatKoanextExecution, in fact, is the execution of the next middleware function, the nextuseIn theasyncFunction, to ensure that the asynchronous code will finish before the current code is executed, so we need to useawaitWait, followed by data from receiving to hangingctx.request.bodyBoth are performed in the Promise because the data is received asynchronously, and the entire process of processing the data needs to wait for the asynchronous completion before hanging the data in thectx.request.bodyGo on, you can guarantee us the nextuseasyncThe function can be inctx.request.bodySo we useawaitWait for a Promise to succeed before executing itnext.

Koa-better-body Middleware simulation

Koa-bodyparser is still a bit weak on form submission because it doesn’t support file uploads. Koa-better-body makes up for this, but koA-better-body is a middleware version of KOA 1.x. The middleware for Koa 1.x is implemented using Generator functions and we need to convert koA-better-body to Koa 2.x middleware using koA-convert.

npm install koa koa-better-body koa-convert path uuid

Koa-better-body is used as follows:

Koa – better – the use of the body

const Koa = require("koa");
const betterBody = require("koa-better-body");
const convert = require("koa-convert"); // Convert koA 1.0 middleware to KOA 2.0 middleware const path = require("path");
const fs = require("fs");
const uuid = require("uuid/v1"); // Generate a random string const app = new Koa(); Use (convert(betterBody({uploadDir: path.resolve(__dirname,"upload")}))); app.use(async (ctx, next) => {if (ctx.path === "/" && ctx.method === "POST") {// With middleware, ctx.request.fields automatically adds the file data console.log(ctx.request.fields) for post requests; // Rename the filelet imgPath = ctx.request.fields.avatar[0].path;
        letnewPath = path.resolve(__dirname, uuid()); fs.rename(imgPath, newPath); }}); app.listen(3000);Copy the code

The main function of koA-better-body in the above code is to save the file uploaded by the form to a locally specified folder and hang the file stream object on the ctx.request.fields property. Next, we will simulate the functionality of KOA-Better-Body to implement a version of middleware based on KOA 2.x to handle file uploads.

File: my – koa – better – body. Js

const fs = require("fs");
const uuid = require("uuid/v1");
const path = require("path"); // Use buffer.prototype.split = to extend the split method to Bufferfunction (sep) {
    letlen = Buffer.from(sep).length; // The number of bytes in the delimiterletresult = []; // The array returnedletstart = 0; // Find the starting position of Bufferletoffset = 0; // Offset // loop to find delimiterswhile((offset = this.indexOf(sep, start)) ! == -1) {result.push(this.slice(start, offset)); start = offset + len; } // process the rest of the result.push(this.slice(start)); // Return the resultreturn result;
}

module.exports = function (options) {
    return async (ctx, next) => {
        await new Promise((resolve, reject) => {
            letdataArr = []; Ctx.req.on (ctx.req.on)"data", data => dataArr.push(data));

            ctx.req.on("end", () => {// Get the separator string for each segment of the request bodylet bondery = `--${ctx.get("content-Type").split("=")[1]}`; // Get newlines for different systemslet lineBreak = process.platform === "win32" ? "\r\n" : "\n"; // Final return result for non-file type dataletfields = {}; // Separate buffers to remove useless headers and tails' 'And at the end of theThe '-'dataArr = dataArr.split(bondery).slice(1, -1); ForEach (lines => {// For normal values, the information consists of a line containing the key name + two newlines + data values + newlines // For files, The information consists of a line containing filename + two newlines + file content + newlinelet [head, tail] = lines.split(`${lineBreak}${lineBreak}`); // Determine if it is a file. If it is a file, create a file and write it. If it is a normal value, store it in the Fields objectif (head.includes("filename") {// To prevent the contents of the file from being split with a newline, re-intercept the contents and remove the last newlinelettail = lines.slice(head.length + 2 * lineBreak.length, -lineBreak.length); // Create a writable stream and specify a path to write to: Fs.createwritestream (path.join(__dirname, options.uploaddir, uuid())).end(tail); fs.createWritestream (path.join(__dirname, options.uploaddir, uuid())).end(tail); }else{// is a normal value to retrieve the key namelet key = head.match(/name="(\w+)"/ [1]); // Fields [key] = tail.toString("utf8").slice(0, -lineBreak.length); }}); // Attach the fields object to ctx.request.fields and complete the Promise ctx.request.fields = fields; resolve(); }); }); // go down to await next(); }}Copy the code

The above content logic can be understood through code comments, which is to simulate the functional logic of KOA-better-body. Our main concern lies in the way the middleware is implemented. The asynchronous operation of the above functionality is still to read data, which is still executed in the Promise to wait for the completion of data processing. And waits with await, the Promise performs a successful call to Next.

Koa-views Middleware simulation

Node templates are the tools we often use to help us render pages on the server side. There are many kinds of templates, so koA-View middleware appeared to help us to accommodate these templates, and install the dependent modules first.

npm install koa koa-views ejs

Here is an EJS template file:

File: index. Ejs

<! DOCTYPE html> <html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ejs</title>
</head>
<body>
    <%=name%>
    <%=age%>

    <%if (name=="panda") {%>
        panda
    <%} else {%>
        shen
    <%}%>

    <%arr.forEach(item => {%>
        <li><%=item%></li>
    <%})%>
</body>
</html>Copy the code

Koa-views:

The use of the koa – views

const Koa = require("koa");
const views = require("koa-views");
const path = require("path"); const app = new Koa(); Use (views(path.resolve(__dirname,"views"), {
    extension: "ejs"
}));

app.use(async (ctx, next) => {
    await ctx.render("index", { name: "panda", age: 20, arr: [1, 2, 3] });
});

app.listen(3000);Copy the code

It can be seen that after we use KOA-Views middleware, the render method on CTX helps us to achieve the rendering and response page of the template. It is the same as using EJS’s own render method directly, and it can be seen from the usage that the render method is executed asynchronously. So we need to wait with await. Next we will simulate implementing a simple version of KOA-Views middleware.

File: my – koa – views. Js

const fs = require("fs");
const path = require("path");
const { promisify } = require("util"); // Convert the file-reading method to a Promise constreadFile = promisify(fs.radFile); Exports = // middleware module.exports =function (dir, options) {
    returnAsync (CTX, next) => {// Async (CTX, next) => {const view = require(options.extension); Ctx. render = async (filename, data) => {// Asynchronously read file contentslet tmpl = await readFile(path.join(dir, `${filename}.${options.extension}`), "utf8"); // Render the template and return the page stringletpageStr = view.render(tmpl, data); // Set the response type and the response page ctx.set("Content-Type"."text/html; charset=utf8"); ctx.body = pageStr; } // go ahead and await next(); }}Copy the code

The render method attached to CTX is executed asynchronously because the internal reading of the template file is executed asynchronously and requires waiting. Therefore, the render method is async function, which dynamically introduces the templates we use inside the middleware, such as EJS, Use the corresponding render method inside ctx.render to retrieve the page string after the replacement data and respond with the HTML type.

Koa – Static middleware simulation

The following is the usage of the KOA-static middleware. The code used depends on the following, which must be installed before use.

npm install koa koa-static mime

Koa-static = koa-static

Koa – the use of the static

const Koa = require("koa");
const static = require("koa-static");
const path = require("path");

const app = new Koa();

app.use(static(path.resolve(__dirname, "public")));

app.use(async (ctx, next) => {
    ctx.body = "hello world";
});

app.listen(3000);Copy the code

Through use and analysis, we know that the function of KOA-static middleware is to help us process static files when the server receives a request. If we directly access the file name, we will look for this file and respond directly. If there is no such file path, we will treat it as a folder and look for the index.html under the folder. If it does, it responds directly; if it does not, it is handed over to other middleware.

File: my – koa -static. Js

const fs = require("fs");
const path = require("path");
const mime = require("mime");
const { promisify } = require("util"); / / will bestatAnd access to the Promise conststat = promisify(fs.stat);
const access = promisify(fs.access)

module.exports = function (dir) {
    returnAsync (CTX, next) => {// Processes the accessed route as an absolute path, using join because it may be /letrealPath = path.join(dir, ctx.path); Try {// getstatobjectlet statObj = await stat(realPath); // If it is a file, set the file type and respond directly to the content, otherwise look for index.html as a folderif (statObj.isFile()) {
                ctx.set("Content-Type", `${mime.getType()}; charset=utf8`); ctx.body = fs.createReadStream(realPath); }else {
                let filename = path.join(realPath, "index.html"); // If the file does not exist, execute next in catch and give another middleware to process await access(filename); // there is a set file type and the response content ctx.set("Content-Type"."text/html; charset=utf8"); ctx.body = fs.createReadStream(filename); } } catch (e) { await next(); }}}Copy the code

In the above logic, we need to check whether the path exists. Since the functions we export are async functions, we convert stat and access into promises and use try… Catch is used to catch, and when the path is illegal, next is called and handed over to other middleware.

Koa-router middleware simulation

In the Express framework, routing is built into the framework, whereas in Koa it is not built into the framework. It is implemented using the KOA-Router middleware and needs to be installed before use.

npm install koa koa-router

The KOA-Router is very powerful, so we will use it briefly and simulate it based on the functionality used.

Simple usage of koa-Router

const Koa = require("Koa");
const Router = require("koa-router");

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

router.get("/panda", (ctx, next) => {
    ctx.body = "panda";
});

router.get("/panda", (ctx, next) => {
    ctx.body = "pandashen";
});

router.get("/shen", (ctx, next) => {
    ctx.body = "shen"; }) // Call routing middleware app.use(router.routes()); app.listen(3000);Copy the code

It can be seen from the above that koa-Router exports a class. When using it, it needs to create an instance and call the routes method of the instance to connect the async function returned by the method. However, when matching the route, it will match the path in the route GET method and execute the internal callback function in serial. When all the callback functions are completed, the entire Koa serial next is executed. The principle is the same as other middleware, and I will briefly implement the function used above.

File: my – koa – router. Js

Class Layer {constructor(path, cb) {this.path = path; this.cb = cb; } match(path) {// The route of the address is equal to the currently configured routetrueOtherwise returnfalse
        return path === this.path;
    }
}

// 路由的类
class Router {
    constructor{path: / XXX, fn: cb} this.layers = []; } get(path, cb) {this.layers.push(new Layer(path, cb)); } compose(CTX, next, handlers) {// compose the matching routing functions in tandemfunctionDispatch (index) {// If the current index number is greater than the length of the stored route object, Koa's next method is executedif(index >= handlers.length) returnnext(); // Otherwise call the callback execution of the fetched route object and pass in a function, Handlers [index]. Cb (CTX, () => Dispatch (index + 1)); handlers[index]. } // Execute the route object's callback function dispatch(0) for the first time; }routes() {
        returnAsync (CTX, next) {// Currently next is Koa's own next, that is, Koa's other middleware // filter routes with the same pathlethandlers = this.layers.filter(layer => layer.match(ctx.path)); this.compose(ctx, next, handlers); }}}Copy the code

We created a Router class and defined the get method, post, etc. We just implemented get. The logic in GET is to build the parameter function that calls the get method and the route string into an object and store it in the array layers. Therefore, we create Layer class specially for constructing route objects, which is convenient for extension. When routing matches, we can get the route string according to ctx.path, filter the route object in the array that does not match the route, call the compose method, and pass in the filtered array as parameter Handlers. Executes the callback function on the route object serially.

composeThe implementation idea of this method is very important inKoaSource code used in tandem middleware, inReactSource code used in seriesreduxpromise,thunkloggerSuch modules, our implementation is a simple version, and there is no compatible asynchronous, the main idea is recursiondispatchThe callback function is executed each time the callback function of the next route object in the array is fetched until the callback function of all matched routes is executedKoaThe next middlewarenextNotice herenextArguments that are different from the callback function in an arraynext, the callback function of the routing object in the arraynextRepresents the callback for the next matched route.

conclusion

We have analyzed and simulated some middleware above. In fact, we can understand the advantages of Koa compared with Express is that it is not so heavy, easy to develop and use, and all the required functions can be realized by the corresponding middleware. Using middleware can bring us some benefits. For example, we can mount the processed data and new methods on CTX, which is convenient for use in the callback function passed by use later. It can also help us to process some common logic, so that we will not process it in every callback of USE, which greatly reduces the redundant code. From this point of view, the process of using middleware for Koa is a typical “decorator” pattern. After the above analysis, I believe that you also understand Koa’s “Onion model” and asynchronous characteristics, and know how to develop their own middleware.

The original source: https://www.pandashen.com