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.js
Native API Use : Use Node.js Native API Write a web The server - The second article is about reading
Express
The 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 :
Koa
Will write an article on the core architecture of , This is the article .- For one
web The server
Come on , Routing is essential , therefore@koa/router
Can write an article . - In addition, some common middleware may be written , Static files support or
bodyparser
wait , 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 :
ctx
Replaced thereq
andres
- have access to JS The new API 了 , such as
async
andawait
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
Koa
This class , Because he usednew
instantiate , So we think of him as a class . - app.use:app yes
Koa
An example of ,app.use
It seems to be an instance method of adding middleware . - app.listen: Instance method to start the server
- ctx: This is
Koa
The context of , It seems to replace the previousreq
andres
- 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://segmentfault.com/a/1190000023201844.
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.req
Just 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
request
andreq
What's the difference ? These two variables look so much like , What the hell is it ? This is to sayKoa
For nativereq
An extension of , We knowhttp.createServer
In the callback ofreq
As a description of the request object , You can get the requestedheader
ah ,method
Ah, these variables . howeverKoa
Think thisreq
Provided API It fails to work well , So he expanded a little on this basis API, In fact, it's just some grammar sugar , Extendedreq
It 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 isrequest
yesKoa
Packedreq
,req
Is a native request object .response
andres
It's the same thing . - since
request
andresponse
They're just packaged grammar candy , Well, actuallyKoa
You 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 :
Koa
yesExpress
A new framework written by the original team .Koa
Used JS The new API, such asasync
andawait
.Koa
Architecture andExpress
Make a big difference .Express
The idea is big and comprehensive , A lot of functions are built in , Like routing , Static resources, etc , andExpress
The middleware is also implemented using the same mechanism of routing , The whole code is more complicated .Express
Source code can see my previous article : Handwriting Express.js Source codeKoa
It looks clearer ,Koa
A 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
Koa
Support other functions , Middleware must be added manually . As aweb The server
, Routing is a basic function , So we'll come and see... The next articleKoa
Official 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
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 ~