use Node.js Write a web The server , I've written two articles before :
- The first one is that you can build a without any framework
web The server, Mainly familiarNode.jsNative API Use : Use Node.js Native API Write a web The server - The second article is about reading
ExpressThe basic usage of , The more important thing is to look at his source code : Handwriting Express.js Source code
Express Source code or more complex , With routing processing and static resource support and other functions , The functions are quite comprehensive . Compared with , This article will talk about Koa It's much simpler ,Koa Although it is Express It was written by the original people of , But the design idea is different .Express It's more biased towards All in one Thought , All kinds of functions are integrated together , and Koa The library itself has only one middleware kernel , Other functions like routing processing and static resources are not available , All need to introduce a third-party middleware library to achieve . The following picture can be seen intuitively Express and koa The difference in function , This is from the official document :
be based on Koa This kind of architecture , I plan to write in several articles , All source code parsing :
KoaWill write an article on the core architecture of , This is the article .- For one
web The serverCome on , Routing is essential , therefore@koa/routerCan write an article . - In addition, some common middleware may be written , Static files support or
bodyparserwait , I haven't decided yet , There may be one or more articles .
This article can run mini version Koa The code has been uploaded GitHub, lift down , Play code while reading the article better :https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore
A simple example
I write source code analysis , Generally follow a simple routine : First introduce it into the warehouse , Write a simple example , Then I write the source code to replace the library , And let our example run smoothly . This article also follows this routine , because Koa Only middleware is the core library of , So the examples we write are also simpler , It's just middleware .
Hello World
The first example is Hello World, Any request for a path will return Hello World.
const Koa = require("koa");
const app = new Koa();
app.use((ctx) => {
ctx.body = "Hello World";
});
const port = 3001;
app.listen(port, () => {
console.log(`Server is running on http://127.0.0.1:${port}/`);
});
logger
Then one more logger Well , It's recording how long it took to process the current request :
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
Note that this middleware should be placed in Hello World In front of .
From the code of the two examples above ,Koa Follow Express There are several obvious differences :
ctxReplaced thereqandres- have access to JS The new API 了 , such as
asyncandawait
Handwritten source code
Let's see what we used before writing the source code API, These are the goals of our handwriting :
- new Koa(): First of all, it must be
KoaThis class , Because he usednewinstantiate , So we think of him as a class . - app.use:app yes
KoaAn example of ,app.useIt seems to be an instance method of adding middleware . - app.listen: Instance method to start the server
- ctx: This is
KoaThe context of , It seems to replace the previousreqandres - async and await: Support new syntax , And it can use
await next(), explainnext()It's likely to be apromise.
The handwritten source code of this article is written with reference to the official source code , Try to keep the file name and function name as consistent as possible , When writing specific methods, I will also paste the official source code address .Koa This library code is not much , It's mainly in this folder :https://github.com/koajs/koa/tree/master/lib, Let's start .
Koa class
from Koa Project package.json Inside main This line of code shows that , The entry point of the whole application is lib/application.js This file :
"main": "lib/application.js",
lib/application.js This document is what we often use Koa class , Although we often call him Koa class , But in the source code, this class is called Application. Let's write down the shell of this class first :
// application.js
const Emitter = require("events");
// module.exports Direct export Application class
module.exports = class Application extends Emitter {
// Constructor first runs the constructor of the parent class
// And do some initialization work
constructor() {
super();
// middleware Instance property is initialized to an empty array , It is used to store the following possible middleware
this.middleware = [];
}
};
We can see from this code that ,Koa Use it directly class Keyword to declare the class , Before you saw me Express Friends of source code resolution may also have an impression ,Express The source code is still used in the old prototype To implement object-oriented . therefore Koa In the project introduction Expressive middleware for node.js using ES2017 async functions It's not an empty word , It not only supports ES2017 new API, And in their own source code is also used in the new API. I think so Koa The operating environment must be node v7.6.0 or higher Why? . So here we can actually see that Koa and Express One of the big differences between , That's it :Express Use the old API, More compatible , Can be in the old Node.js Run... On version ;Koa Because of the new API, Only in v7.6.0 Or a later version of .
There's another thing to note about this code , That's it Application Inherited from Node.js Native EventEmitter class , This class is actually a publish subscribe model , You can subscribe and publish messages , I detailed his source code in another article . So he has some methods if he's in application.js I can't find , That might be inherited from EventEmitter, For example, the following line of code :

Here you are this.on This method , It looks like he should be Application An example method of , But this document doesn't contain , In fact, he inherited from EventEmitter, It's for error This event adds the callback function of . This line of code if Inside this.listenerCount It's also EventEmitter An example method of .
Application Class is exactly JS The use of object-oriented , If you are right about JS Object oriented is not very familiar with , You can read this article first :https://juejin.im/post/6844904069887164423.
app.use
From the example we used earlier, you can see app.use Is to add a middleware , We also initialize a variable in the constructor middleware, Used to store middleware , therefore app.use The code is very simple , Just plug the received middleware into this array :
use(fn) {
// Middleware must be a function , Otherwise, report an error
if (typeof fn !== "function")
throw new TypeError("middleware must be a function!");
// The processing logic is simple , The middleware will be plugged into middleware An array of line
this.middleware.push(fn);
return this;
}
Be careful app.use Method finally returns this, This is kind of interesting , Why return to this Well ? This is actually what I mentioned in other articles : Class returns this Chain call can be implemented . Like here app.use Then you can continue to point , like this :
app.use(middlewaer1).use(middlewaer2).use(middlewaer3)
Why do you have this effect ? Because of the this In fact, this is the current example , That is to say app, therefore app.use() The return value of app,app There's an example method on use, So you can go on app.use().use().
app.use The official source code here : https://github.com/koajs/koa/blob/master/lib/application.js#L122
app.listen
In the previous example ,app.listen Is used to start the server , I've seen it in the front, using the original API Realization web The server Our friends all know , To start the server, you need to call native http.createServer, So this method is used to call http.createServer Of .
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
This method itself has not much to say , Just call http The module starts the service , The main logic is this.callback() Inside the .
app.listen The official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L79
app.callback
this.callback() It's for http.createServer Callback function for , It's also an instance function , This function must conform to http.createServer The parametric form of , That is to say
http.createServer(function(req, res){})
therefore this.callback() The return value of must be a function , And it's in this form function(req, res){}.
Except that the form must conform to ,this.callback() What are you going to do ? He is http Module callback function , So he has to deal with all the web requests , All processing logic must be in this method . however Koa The processing logic of is in the form of middleware , For a request , He had to go through all the middleware one by one , The logic of the specific passage , Of course you can traverse middleware This array , Take out the methods one by one , Of course, it can also be used in a more common way in the industry :compose.
compose Generally speaking, it is to merge a series of methods into one method to facilitate the call , The form of concrete implementation is not fixed , Yes The common use in an interview is reduce Realized compose, Also like Koa In this way, according to their own needs to achieve a separate compose.Koa Of compose It also encapsulates a library separately koa-compose, The source code of this library is also what we must see , Let's go step by step , The first this.callback Write it out .
callback() {
// compose come from koa-compose library , It is to combine middleware into a function
// We need to achieve
const fn = compose(this.middleware);
// callback The return value must match http.createServer Parameter form
// namely (req, res) => {}
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
This method starts with koa-compose The middleware is composed into a function fn, And then in http.createServer The callback uses req and res Created a Koa Common context ctx, Then call this.handleRequest To actually handle network requests . Notice the this.handleRequest It's an example method , And the local variables in the current method handleRequest It's not a thing . Let's look at these methods one by one .
this.callback The corresponding official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L143
koa-compose
koa-compose Although it was used as a separate library , But his role is crucial , So let's also look at his source code .koa-compose Is to merge an array of middleware into a method for external call . Let's go back to the next one Koa Architecture of middleware :
function middleware(ctx, next) {}
This array has many such middleware :
[
function middleware1(ctx, next) {},
function middleware2(ctx, next) {}
]
Koa The idea of merging is not complicated , Is to make compose Return a function , The returned function will start traversing the array :
function compose(middleware) {
// Parameter check ,middleware It must be an array
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
// Each item in the array must be a method
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}
// Return a method , This method is compose Result
// External can call this method to start traversing middleware array
// The form of parameters is the same as that of ordinary middleware , All are context and next
return function (context, next) {
return dispatch(0); // Start middleware execution , Start with the first one in the array
// The method of executing middleware
function dispatch(i) {
let fn = middleware[i]; // Take out the middleware that needs to be executed
// If i Equal to the array length , Indicates that the array has been executed
if (i === middleware.length) {
fn = next; // Here let's fn It's equal to what's coming in from the outside next, In fact, it's the finishing line , Such as return 404
}
// If there's no ending from the outside next, It's just resolve
if (!fn) {
return Promise.resolve();
}
// Execution middleware , Note that the parameters passed to the middleware should be context and next
// To middleware next yes dispatch.bind(null, i + 1)
// So the middleware calls next In fact, it calls dispatch(i + 1), That is to execute the next middleware
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}
The main logic of the above code is this line :
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
there fn It's middleware that we write ourselves , For example, at the beginning of the article logger, Let's change it a little bit and see more clearly :
const logger = async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
};
app.use(logger);
Then we compose What's implemented inside is actually :
logger(context, dispatch.bind(null, i + 1));
in other words logger The received next It's actually dispatch.bind(null, i + 1), You call next() When , It's actually calling theta dispatch(i + 1), In this way, the effect of executing the next middleware of array is achieved .
In addition, the middleware is wrapped in a layer before returning Promise.resolve, So we all write middleware ourselves , Whether you use it or not Promise,next After the call, all returned are a Promise, So you can use await next().
koa-compose See the source code here :https://github.com/koajs/compose/blob/master/index.js
app.createContext
Used above this.createContext It's also an example method . This method is based on http.createServer Incoming req and res To build ctx This context , The official source code looks like this :

In this code context,ctx,response,res,request,req,app These variables assign values to each other , I feel dizzy . There's no need to get into this pile of noodles , We just need to get his ideas and skeleton clear , How to carry it ?
- First, find out the purpose of his assignment , His purpose is very simple , Just for the convenience of use . You can easily get other variables through one variable , For example, I only have
request, But what I want isreq, What shall I do? ? After this assignment , Direct userequest.reqJust go . Other similar , It's hard for me to say whether it's good or not , But it's really convenient to use , The disadvantage is that it is easy to get stuck in the source code . - that
requestandreqWhat's the difference ? These two variables look so much like , What the hell is it ? This is to sayKoaFor nativereqAn extension of , We knowhttp.createServerIn the callback ofreqAs a description of the request object , You can get the requestedheaderah ,methodAh, these variables . howeverKoaThink thisreqProvided API It fails to work well , So he expanded a little on this basis API, In fact, it's just some grammar sugar , ExtendedreqIt becomesrequest. The original... That was preserved after the extensionreq, Should also want to provide users with more choices . So the difference between these two variables isrequestyesKoaPackedreq,reqIs a native request object .responseandresIt's the same thing . - since
requestandresponseThey're just packaged grammar candy , Well, actuallyKoaYou can run without these two variables . So when we carry the skeleton, we can kick these two variables out , Now the skeleton is clear .
Then we kick out response and request Then write down createContext This method :
// Create context ctx Object function
createContext(req, res) {
const context = Object.create(this.context);
context.app = this;
context.req = req;
context.res = res;
return context;
}
Now the whole world feels fresh ,context What's on it is clear at a glance . But our context It was originally from this.context Of , This variable also has to look at .
app.createContext The corresponding official source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L177
context.js
above this.context It's actually from context.js, So let's start with Application Add this variable to the constructor :
// application.js
const context = require("./context");
// In the constructor
constructor() {
// Omit other code
this.context = context;
}
Then we'll see context.js There's something in it ,context.js It's like this :
const delegate = require("delegates");
module.exports = {
inspect() {},
toJSON() {},
throw() {},
onerror() {},
};
const proto = module.exports;
delegate(proto, "response")
.method("set")
.method("append")
.access("message")
.access("body");
delegate(proto, "request")
.method("acceptsLanguages")
.method("accepts")
.access("querystring")
.access("socket");
In this code context The export is an object proto, The object itself has some methods ,inspect,toJSON And so on. . And then there's a bunch of delegate().method(),delegate().access() And so on. . Um. , What's this for ? You know what this does , We need to see delegates This library :https://github.com/tj/node-delegates, This library is also tj Written by the great God . The general use is like this :
delegate(proto, target).method("set");
The purpose of this line of code is , When you call proto.set() When the method is used , It was actually forwarded to proto[target], What's actually called is proto[target].set(). So is proto Acting right target The interview of .
That's for us context.js What does it mean in it ? Like this line of code :
delegate(proto, "response")
.method("set");
The purpose of this line of code is , When you call proto.set() when , Actually call proto.response.set(), take proto Switch to ctx Namely : When you call ctx.set() when , What's actually called is ctx.response.set(). The purpose of this is actually to make it easy to use , You can write one less response. and ctx It's not just acting response, And acting for request, So you can also go through ctx.accepts() This calls to ctx.request.accepts(). One ctx It includes response and request, So here context It's also a grammar sugar . Because we've played in front of us response and request These two grammatical sugars ,context As the grammar sugar that packaged these two grammatical sugars , Let's kick it out, too . stay Application In the constructor of the this.context Assign an empty object :
// application.js
constructor() {
// Omit other code
this.context = {};
}
Now grammar sugar is kicked out , Whole Koa The structure is clearer ,ctx There are only a few required variables :
ctx = {
app,
req,
res
}
context.js The corresponding source code here :https://github.com/koajs/koa/blob/master/lib/context.js
app.handleRequest
Now we ctx and fn It's all constructed , Then we process the request by invoking fn,ctx It was passed to him as a parameter , therefore app.handleRequest The code can be written :
// Processing specific requests
handleRequest(ctx, fnMiddleware) {
const handleResponse = () => respond(ctx);
// Call middleware to handle
// After all processing is finished, call handleResponse Return request
return fnMiddleware(ctx)
.then(handleResponse)
.catch((err) => {
console.log("Somethis is wrong: ", err);
});
}
We see compose Library returns fn Although the second parameter is supported to close out , however Koa It didn't use him , If not , After all middleware is executed, it returns an empty promise, So it can be used then Then he dealt with it later . There is only one processing to be done later , It is to return the processing results to the requester , This is the same. respond What needs to be done .
app.handleRequest The corresponding source code here :https://github.com/koajs/koa/blob/master/lib/application.js#L162
respond
respond It's an auxiliary method , It's not in Application In class , All he has to do is return the web request :
function respond(ctx) {
const res = ctx.res; // Take out res object
const body = ctx.body; // Take out body
return res.end(body); // use res return body
}
Be accomplished
Now we can write our own Koa Replace the official Koa Let's run the example we started with , however logger There will be some problems when the middleware is running , Because he uses syntax sugar in the following line of code :
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
there ctx.method and ctx.url In what we build ctx There is no such thing as , But that's okay , Isn't he just one req The grammar of sugar , We from ctx.req Just take it up , So the line above is changed to :
console.log(`${ctx.req.method} ${ctx.req.url} - ${ms}ms`);
summary
Through layer by layer of silk cocoon , We managed to pull out Koa The code skeleton of , I wrote a mini version of Koa.
This mini code has been uploaded GitHub, You can take it down and play with it :https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/Node.js/KoaCore
Finally, let's summarize the main points of this paper :
KoayesExpressA new framework written by the original team .KoaUsed JS The new API, such asasyncandawait.KoaArchitecture andExpressMake a big difference .ExpressThe idea is big and comprehensive , A lot of functions are built in , Like routing , Static resources, etc , andExpressThe middleware is also implemented using the same mechanism of routing , The whole code is more complicated .ExpressSource code can see my previous article : Handwriting Express.js Source codeKoaIt looks clearer ,KoaA kernel library is just a library of its own , Only middleware functions , Requests come through each Middleware in turn , And then come out and return it to the requester , This is what you often hear about “ Onion model ”.- to want to
KoaSupport other functions , Middleware must be added manually . As aweb The server, Routing is a basic function , So we'll come and see... The next articleKoaOfficial routing Library@koa/router, Stay tuned .
Reference material
Koa Official documents :https://github.com/koajs/koa
Koa Source code address :https://github.com/koajs/koa/tree/master/lib
At the end of the article , Thank you for your precious time reading this article , If this article gives you a little help or inspiration , Please don't be stingy with your praise and GitHub Little star , Your support is the motivation for the author to continue to create .
Author's blog GitHub Project address : https://github.com/dennis-jiang/Front-End-Knowledges
Author digs the gold article summary :https://juejin.im/post/5e3ffc85518825494e2772fd
I also made a official account [ The big front of the attack ], No advertising , Don't write about hydrology , Only high quality original , Welcome to your attention ~





