The Koa source code is minimal, with less than 2k lines of code, and consists of four module files, making it a good place to learn.
Reference code: learn-koa2
Let’s look at some native Node implementation Server code:
const http = require('http');
const server = http.createServer((req, res) = > {
res.writeHead(200);
res.end('hello world');
});
server.listen(3000, () = > {console.log('server start at 3000');
});
Copy the code
Very simple a few lines of code, to achieve a Server Server. The createServer method receives a callback that performs various operations on each requested REq RES object and returns a result. However, it is also obvious that the callback function can easily become bloated with the complexity of the business logic. Even if the callback function is divided into small functions, it will gradually lose control of the whole process in the complex asynchronous callback.
In addition, Node provides some natively provided apis that sometimes confuse developers:
res.statusCode = 200;
res.writeHead(200);
Copy the code
Changing an RES property or calling an RES method can change the HTTP status code, which can easily lead to different code styles in a multi-person project.
Let’s look at the Koa implementation Server:
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('1-start');
await next();
console.log('1-end');
});
app.use(async (ctx, next) => {
console.log('2-start');
ctx.status = 200;
ctx.body = 'Hello World';
console.log('2-end');
});
app.listen(3000);
// Final output:
// 1-start
// 2-start
// 2-end
// 1-end
Copy the code
Koa uses middleware concepts to handle an HTTP request, and Koa uses async and await syntax to make asynchronous processes more manageable. The CTX execution context proxies the native RES and REQ, allowing developers to avoid the underlying layer and access and set properties through the proxy.
After reading the comparison, we should have a few questions:
ctx.status
Why can you directly set the status code, is not not seeres
The object?- Middleware
next
What is it? Why executenext
Is it the next piece of middleware? - After all middleware execution is complete, why can I go back to the original middleware (onion model)?
Now let’s take a look at the source code with some confusion and implement a simple version of Koa ourselves!
Encapsulate the HTTP Server
Reference code: step-1
// How to use Koa
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
We started by mimicking the use of KOA and building a simple skeleton:
Create new kao/application.js.
const http = require('http');
class Application {
constructor() {
this.callbackFn = null;
}
use(fn) {
this.callbackFn = fn;
}
callback() {
return (req, res) = > this.callbackFn(req, res) } listen(... args) {const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
Copy the code
Create a new test file kao/index.js
const Kao = require('./application');
const app = new Kao();
app.use(async (req, res) => {
res.writeHead(200);
res.end('hello world');
});
app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code
We’ve encapsulated the HTTP Server preliminaries: instantiate an object with new, register the callback function with use, and start the server with Listen and pass in the callback.
Note that when new is called, the server server is not actually turned on. It is actually turned on when listen is called.
This code, however, has significant drawbacks:
- Use passes the callback function and receives the arguments that are still native
req
andres
- Multiple calls to use override the previous middleware, rather than executing multiple middleware in sequence
Let’s tackle the first problem first
Encapsulate the REq and RES objects and construct the context
Reference code: step-2
Let’s start with the GET and set references in ES6
Get and set based on ordinary objects
const demo = {
_name: ' ',
get name() {
return this._name;
},
set name(val) {
this._name = val; }}; demo.name ='deepred';
console.log(demo.name);
Copy the code
Class-based get and set
class Demo {
constructor() {
this._name = ' ';
}
get name() {
return this._name;
}
set name(val) {
this._name = val; }}const demo = new Demo();
demo.name = 'deepred';
console.log(demo.name);
Copy the code
Get and set based on Object.defineProperty
const demo = {
_name: ' '
};
Object.defineProperty(demo, 'name', {
get: function() {
return this._name
},
set: function(val) {
this._name = val; }});Copy the code
Proxy-based GET and set
const demo = {
_name: ' '
};
const proxy = new Proxy(demo, {
get: function(target, name) {
return name === 'name' ? target['_name'] : undefined;
},
set: function(target, name, val) {
name === 'name' && (target['_name'] = val)
}
});
Copy the code
There were also implementations of __defineSetter__ and __defineGetter__, which are now deprecated.
const demo = {
_name: ' '
};
demo.__defineGetter__('name'.function() {
return this._name;
});
demo.__defineSetter__('name'.function(val) {
this._name = val;
});
Copy the code
The main difference is that object.defineProperty __defineSetter__ Proxy can set attributes dynamically, whereas otherwise they can only be set at definition time.
Request. js and Response. js in Koa use a lot of get and set proxies
New kao/request. Js
module.exports = {
get header() {
return this.req.headers;
},
set header(val) {
this.req.headers = val;
},
get url() {
return this.req.url;
},
set url(val) {
this.req.url = val; }},Copy the code
When you visit request.url, you are actually visiting the native req.url. Note that the this.req native object is not injected at this time!
Create kao/response.js in the same way
module.exports = {
get status() {
return this.res.statusCode;
},
set status(code) {
this.res.statusCode = code;
},
get body() {
return this._body;
},
set body(val) {
// The source code contains various judgments about the type val, which are omitted here
/* Possible types 1. string 2. Buffer 3. Stream 4. Object */
this._body = val; }}Copy the code
We don’t use the native this.res.end for the body, because the body will be read and modified multiple times when we write koA code, so the actual action that returns the browser information is wrapped and manipulated in application.js
Also note that the this.res native object is not injected at this time!
New kao/context. Js
const delegate = require('delegates');
const proto = module.exports = {
// Context's own method
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>'.res: '<original node res>'.socket: '<original node socket>'}; }},// The delegates principle is __defineGetter__ and __defineSetter__
// Method is a delegate method, getter delegates getter,access delegates getter and setter.
// proto.status => proto.response.status
delegate(proto, 'response')
.access('status')
.access('body')
// proto.url = proto.request.url
delegate(proto, 'request')
.access('url')
.getter('header')
Copy the code
Context.js proxies request and response. Ctx. body points to ctx.response.body. But ctx.response ctx.request has not been injected yet!
You might wonder why Response. js and request.js use get set proxies, while context.js uses delegate proxies. The main reason is that the set and GET methods can also add some logic of their own. The delegate, on the other hand, is pure and only proxies properties.
{
get length() {
// Own logic
const len = this.get('Content-Length');
if (len == ' ') return;
return~~len; }},// Only proxy attributes
delegate(proto, 'response')
.access('length')
Copy the code
So context.js is a good place to use delegates, which are simply properties and methods that delegate request and response.
The actual injection of native objects is done in the createContext method of application.js.
const http = require('http');
const context = require('./context');
const request = require('./request');
const response = require('./response');
class Application {
constructor() {
this.callbackFn = null;
// Context Request respones for each Kao instance
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.callbackFn = fn;
}
callback() {
const handleRequest = (req, res) = > {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx)
};
return handleRequest;
}
handleRequest(ctx) {
const handleResponse = (a)= > respond(ctx);
// callbackFn is an async function that returns a Promise object
return this.callbackFn(ctx).then(handleResponse);
}
createContext(req, res) {
// For each request, a CTX object is created
// CTX request response for each request
// CTX proxy this is where the native REq res is proxy
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
returnctx; } listen(... args) {const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
// Return the last data according to the type of ctx.body
String 2.buffer 3.stream 4.object */
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content)); }}Copy the code
The code uses the object.create method to create a brand new Object that inherits the original properties through the prototype chain. This can effectively prevent contamination of the original object.
CreateContext is called on each HTTP request, and each call generates a new CTX object and proxies the native object for the HTTP request.
Respond is the last method to return the HTTP response. Res.end terminates the HTTP request based on the type of ctx.body after all middleware is executed.
Now let’s test it again: kao/index.js
const Kao = require('./application');
const app = new Kao();
// Use CTX to modify the status code and response content
app.use(async (ctx) => {
ctx.status = 200;
ctx.body = {
code: 1.message: 'ok'.url: ctx.url
};
});
app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code
Middleware mechanisms
Reference code: step-3
const greeting = (firstName, lastName) = > firstName + ' ' + lastName
const toUpper = str= > str.toUpperCase()
const fn = compose([toUpper, greeting]);
const result = fn('jack'.'smith');
console.log(result);
Copy the code
Functional programming has the concept of compose. For example, combine greeting and toUpper into a composite function. Calling this composite function calls the greeting first, then passes the return value to toUpper to continue execution.
Implementation method:
// Imperative programming (procedural oriented)
function compose(fns) {
let length = fns.length;
let count = length - 1;
let result = null;
return function fn1(. args) {
result = fns[count].apply(null, args);
if (count <= 0) {
return result
}
count--;
returnfn1(result); }}// Declarative programming (functional)
function compose(funcs) {
return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code
Koa’s middleware mechanism is similar to compose, again packaging multiple functions into one, but Koa’s middleware is similar to the Onion model, where execution from A middleware to B middleware is completed, and the latter middleware can return to A again.
Koa uses KoA-compose to implement the middleware mechanism, the source code is very concise, but a little difficult to understand. It is recommended to understand recursion first
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/** * @param {Object} context * @return {Promise} * @api public */
return function (context, next) {
// last called middleware #
let index = - 1
return dispatch(0)
function dispatch (i) {
// Next is called multiple times in one middleware
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// fn is the current middleware
let fn = middleware[i]
if (i === middleware.length) fn = next // If the last middleware is also next (usually the last middleware is directly ctx.body, there is no need for next)
if(! fn)return Promise.resolve() // No middleware, return success
try {
Resolve (fn(context, function next () {return dispatch(I + 1)})) */
// dispatch.bind(null, I + 1) is the next argument in the middleware, which is called to access the next middleware
// if fn returns a Promise object, promise. resolve returns the Promise object directly
// if fn returns a normal object, promise. resovle promises it
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// Middleware is an async function, so an error doesn't go there and is caught directly in the catch of fnMiddleware
// Catch middleware is a normal function that promises to get to the catch method of fnMiddleware
return Promise.reject(err)
}
}
}
}
Copy the code
const context = {};
const sleep = (time) = > new Promise(resolve= > setTimeout(resolve, time));
const test1 = async (context, next) => {
console.log('1-start');
context.age = 11;
await next();
console.log('1-end');
};
const test2 = async (context, next) => {
console.log('2-start');
context.name = 'deepred';
await sleep(2000);
console.log('2-end');
};
const fn = compose([test1, test2]);
fn(context).then((a)= > {
console.log('end');
console.log(context);
});
Copy the code
Recursive call stack execution:
Knowing the middleware mechanism, we should be able to answer the previous question:
What is next? How is the Onion model implemented?
Next is a function wrapped around Dispatch
Executing next on the NTH middleware executes dispatch(n+1), which goes to the NTH +1 middleware
Since dispatch returns all promises, the NTH middleware await next(); Go to the NTH +1 middleware. When the NTH +1 middleware execution is complete, the NTH middleware can be returned
If next is not called again in one middleware, then all middleware after it is not called again
Modify the kao/application. Js
class Application {
constructor() {
this.middleware = []; // Storage middleware
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
use(fn) {
this.middleware.push(fn); // Storage middleware
}
compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')}/** * @param {Object} context * @return {Promise} * @api public */
return function (context, next) {
// last called middleware #
let index = - 1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if(! fn)return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
callback() {
// Synthesize all middleware
const fn = this.compose(this.middleware);
const handleRequest = (req, res) = > {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn)
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const handleResponse = (a)= > respond(ctx);
// Execute the middleware and give the final result to Respond
return fnMiddleware(ctx).then(handleResponse);
}
createContext(req, res) {
// For each request, a CTX object is created
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
ctx.app = ctx.request.app = ctx.response.app = this;
returnctx; } listen(... args) {const server = http.createServer(this.callback());
return server.listen(...args);
}
}
module.exports = Application;
function respond(ctx) {
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content)); }}Copy the code
Test the
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx, next) => {
console.log('1-start');
await next();
console.log('1-end');
})
app.use(async (ctx) => {
console.log('2-start');
ctx.body = 'hello tc';
console.log('2-end');
});
app.listen(3001, () = > {console.log('server start at 3001');
});
// 1-start 2-start 2-end 1-end
Copy the code
Error handling mechanism
Reference code: step-4
Because the compose function still returns a Promise object, we can catch exceptions in a catch
kao/application.js
handleRequest(ctx, fnMiddleware) {
const handleResponse = (a)= > respond(ctx);
const onerror = err= > ctx.onerror(err);
// catch the onerror method of CTX
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
Copy the code
kao/context.js
const proto = module.exports = {
// Context's own method
onerror(err) {
// The middleware reported an error
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
res.end(err.message || 'Internal error'); }}Copy the code
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// Errors can be caught
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () = > {console.log('server start at 3001');
});
Copy the code
Now we have implemented error exception catching in the middleware, but we still lack a mechanism to catch errors in the framework layer. We can make the Application inherit from the native Emitter to implement error listeners
kao/application.js
const Emitter = require('events');
/ / Emitter inheritance
class Application extends Emitter {
constructor() {
/ / call the super
super(a);this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response); }}Copy the code
kao/context.js
const proto = module.exports = {
onerror(err) {
const { res } = this;
if ('ENOENT' == err.code) {
err.status = 404;
} else {
err.status = 500;
}
this.status = err.status;
// Raises an error event
this.app.emit('error', err, this);
res.end(err.message || 'Internal error'); }}Copy the code
const Kao = require('./application');
const app = new Kao();
app.use(async (ctx) => {
// Errors can be caught
a.b.c = 1;
ctx.body = 'hello tc';
});
app.listen(3001, () = > {console.log('server start at 3001');
});
// Listen for error events
app.on('error', (err) => {
console.log(err.stack);
});
Copy the code
So far we can see two ways of catching Koa exceptions:
- Middleware Catch (Promise Catch)
- Frame capture (Emitter Error)
// Middleware that catches global exceptions
app.use(async (ctx, next) => {
try {
await next()
} catch (error) {
return ctx.body = 'error'}})Copy the code
// Event listener
app.on('error', err => {
console.log('error happends: ', err.stack);
});
Copy the code
conclusion
The Koa process can be divided into three steps:
Initialization phase:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
Copy the code
New initializes an instance, use collects middleware into the middleware array, listen synthesizes middleware fnMiddleware, returns a callback function to HTTP. createServer, starts the server, and waits for HTTP requests.
Request stage:
On each request, createContext generates a new CTX, which is passed to fnMiddleware, triggering the entire flow of the middleware
Response stage:
When the entire middleware is complete, the respond method is called, the last processing is done to the request, and the response is returned to the client.
Refer to the flow chart below: