I have written a lot of demos with koa before, and added an actual online application, but I have never seen its source code.
This time I took the time to look at the source code.
It's actually only 4 files:
- application.js (main file)
- context.js (creates a context object for network requests)
- request.js (request object that wraps koa)
- response.js (wrapping koa's response object)
Through the package.json
file, we can clearly see:
application.js
It's an entry file, so let's go in and have a look.
core method
- listen
- use
Basic usage
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
app.use((ctx, next) => {
ctx.body = "hello"
})
listen
It's just a service.
There is a debug module that can be used to do some debugging. (The premise is that your environment variable is set to DEBUG, otherwise you will not see the output)
callback function code:
use
The use method, the annotation given by the source code is:
Use the given middleware
fn
.
In Koa, the entire service is built through one by one middleware.
The implementation of the use method is super simple:
In the above callback function, there is a code:
const fn = componse(this.middleware)
It is used to combine all middleware
middleware
For example, we have this piece of code:
let fn1 = (ctx,next)=>{
console.log(1);
next();
console.log(2);
}
let fn2 = (ctx,next)=>{
console.log(3);
next();
console.log(4);
}
let fn3 = (ctx,next)=>{
console.log(5);
next();
console.log(6);
}
Hope to get: 1, 3, 5, 6, 4, 2 results.
This code is easier:
let fns = [fn1,fn2,fn3];
function dispatch(index){
let middle = fns[index];
// 判断一下临界点
if(fns.length === index) return function(){}
middle({},()=>dispatch(index+1));
}
dispatch(0);
After understanding the way of writing synchronization, when the middleware is written as asyn await, it is easy to write.
function dispatch(index){
let middle = fns[index];
if(fns.length === index) return Promise.resolve()
return Promise.resolve(middle({},()=>dispatch(index+1)))
}
Let's take a look at the code of compose:
The core logic is similar to the above code, but it is more rigorous in logical judgment.
easy mistakes
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
function ajax(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve("123");
},3000)
})
}
app.use(async (ctx,next)=>{
console.log(1);
next();
console.log(2);
});
app.use(async (ctx, next) => {
ctx.body = await ajax();
})
The above result is not found, because there is no await next in the first middleware.
ctx
Let's look createContext
at the source code implementation:
request
It is to repackage the previous req object.
Advanced syntax is used here: get/set, similar to Object.definePrototype, which can mainly do some logic processing during set.
response
Similar to how request.js handles it. Here I excerpted a paragraph of body writing:
{
get body() {
return this._body;
},
set body(val) {
const original = this._body;
this._body = val;
// no content
if (null == val) {
if (!statuses.empty[this.status]) this.status = 204;
this.remove('Content-Type');
this.remove('Content-Length');
this.remove('Transfer-Encoding');
return;
}
// set the status
if (!this._explicitStatus) this.status = 200;
// set the content-type only if not yet set
const setType = !this.header['content-type'];
// string
if ('string' == typeof val) {
if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
this.length = Buffer.byteLength(val);
return;
}
// buffer
if (Buffer.isBuffer(val)) {
if (setType) this.type = 'bin';
this.length = val.length;
return;
}
// stream
if ('function' == typeof val.pipe) {
onFinish(this.res, destroy.bind(null, val));
ensureErrorHandler(val, err => this.ctx.onerror(err));
// overwriting
if (null != original && original != val) this.remove('Content-Length');
if (setType) this.type = 'bin';
return;
}
// json
this.remove('Content-Length');
this.type = 'json';
}
}
context
What the context.js file does is more interesting.
It makes a layer of proxy and directly mounts some attribute methods under the request and some attribute methods under the response on the ctx object.
For example, you have to pass ctx.request.url
to get the request path before, but now you only need to write it ctx.url
.
delegate
This library, let's take a brief look, mainly look at two methods:
We can simplify it a bit more:
let proto = {
}
function delateGetter(property,name){
proto.__defineGetter__(name,function(){
return this[property][name];
})
}
function delateSetter(property,name){
proto.__defineSetter__(name,function(val){
this[property][name] = val;
})
}
delateGetter('request','query');
delateGetter('request','method')
delateGetter('response','body');
delateSetter('response','body');
I believe that after reading it, I have a clearer understanding of the implementation logic.
Implementation of some middleware
After reading the source code of koa, we can know that koa itself is very small, the implementation is more elegant, and we can achieve what we want by writing middleware.
Commonly used middleware are probably: static, body-parser, router, session, etc.
koa-static
koa-static is a simple static middleware, its source code is here , the core logic implementation is completed by koa-send , but I flipped it, there is no etag processing.
We can also write the simplest static middleware ourselves:
const path = require('path');
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
function static(p){
return async (ctx,next)=>{
try{
p = path.join(p,'.'+ctx.path);
let stateObj = await stat(p);
console.log(p);
if(stateObj.isDirectory()){
}else{
ctx.body = fs.createReadStream(p);
}
}catch(e){
console.log(e)
await next();
}
}
}
body-parser
The basic code is as follows:
function bodyParser(){
return async (ctx,next)=>{
await new Promise((resolve,reject)=>{
let buffers = [];
ctx.req.on('data',function(data){
buffers.push(data);
});
ctx.req.on('end',function(){
ctx.request.body = Buffer.concat(buffers)
resolve();
});
});
await next();
}
}
module.exports = bodyParser;
There Buffer.concat(buffers)
are several situations that need to be dealt with, such as form, json, file, etc.
In koa-bodyparser , it co-body
wraps a layer with .
Handling of form and json is relatively easy:
querystring.parse(buff.toString()); // form的处理
JSON.parse(buff.toString()); // json的处理
What needs to be said here is how the file is processed:
Here we need to encapsulate a Buffer.split
method to get a few pieces of content in the middle, and then perform the cutting process.
Buffer.prototype.split = function(sep){
let pos = 0;
let len = Buffer.from(sep).length;
let index = -1;
let arr = [];
while(-1!=(index = this.indexOf(sep,pos))){
arr.push(this.slice(pos,index));
pos = index+len;
}
arr.push(this.slice(pos));
return arr;
}
// 核心实现
let type = ctx.get('content-type');
let buff = Buffer.concat(buffers);
let fields = {}
if(type.includes('multipart/form-data')){
let sep = '--'+type.split('=')[1];
let lines = buff.split(sep).slice(1,-1);
lines.forEach(line=>{
let [head,content] = line.split('\r\n\r\n');
head = head.slice(2).toString();
content = content.slice(0,-2);
let [,name] = head.match(/name="([^;]*)"/);
if(head.includes('filename')){
// 取除了head的部分
let c = line.slice(head.length+6);
let p = path.join(uploadDir,Math.random().toString());
require('fs').writeFileSync(p,c)
fields[name] = [{path:p}];
} else {
fields[name] = content.toString();
}
})
}
ctx.request.fields = fields;
Of course, like koa-better-body
the file processing used in it, split is not used. it uses formidable
The interception operations are all handled in the multipart_parser.js file.
also-router
Basic usage
var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router.get('/', (ctx, next) => {
// ctx.router available
});
app
.use(router.routes())
.use(router.allowedMethods());
principle
There is an article on Nuggets: Interpret and implement a simple koa-router
I will also analyze it according to the idea of looking at the source code.
router.routes
is to return a middleware:
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function dispatch(ctx, next) {
debug('%s %s', ctx.method, ctx.path);
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
var matched = router.match(path, ctx.method);
var layerChain, layer, i;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
ctx.router = router;
if (!matched.route) return next();
var matchedLayers = matched.pathAndMethod
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
ctx.captures = layer.captures(path, ctx.captures);
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
return memo.concat(layer.stack);
}, []);
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
What it does is that the incoming request will go through router.match, and then push the execution function of the matched route into the array, and merge it back and execute it through the compose(koa-compose) function.
As we wrote router.get/post
, all we do is register a route, and then stuff an instance of layer into this.stack:
In addition, like matching special symbols, such as: /:id/:name
, it is processed by path-to-regexp .
Understand the above, combined with the source code analysis of the Nuggets, you can basically do it.
Summarize
Koa's things seem to be relatively simple, but there are still many things that have not been analyzed, such as the proxy in the source code.
However, according to the 28 rule, we basically only need to master 80% of the source code implementation.
last of the last
Advertise my blog, welcome to visit: Front-end world of small wings
Author: henryzp
Link: https://juejin.im/post/5ae980adf265da0b7f445d85
Source: Nuggets The
copyright belongs to the author. For commercial reprints, please contact the author for authorization, and for non-commercial reprints, please indicate the source.