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.
// 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:
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.
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.
next
We know thatKoa
的 next
Execution, in fact, is the execution of the next middleware function, the nextuse
In theasync
Function, to ensure that the asynchronous code will finish before the current code is executed, so we need to useawait
Wait, followed by data from receiving to hangingctx.request.body
Both 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.body
Go on, you can guarantee us the nextuse
的 async
The function can be inctx.request.body
So we useawait
Wait 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:
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.
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:
<! 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:
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.
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
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.
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.
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.
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.
compose
The implementation idea of this method is very important inKoa
Source code used in tandem middleware, inReact
Source code used in seriesredux
的 promise
,thunk
和 logger
Such modules, our implementation is a simple version, and there is no compatible asynchronous, the main idea is recursiondispatch
The 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 executedKoa
The next middlewarenext
Notice herenext
Arguments that are different from the callback function in an arraynext
, the callback function of the routing object in the arraynext
Represents 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.