以前用koa寫過很多的demo,加一個實際的線上應用,但一直沒有怎麼看過它的源碼。html
此次抽空看了一下源碼。前端
它其實只有4個文件:node
經過package.json
文件,咱們能夠清楚地看到:git
application.js
是入口文件,那麼進去看看吧。github
const Koa = require('koa');
const app = new Koa();
app.listen(3000);
app.use((ctx, next) => {
ctx.body = "hello"
})
複製代碼
就是起了一個服務。ajax
這裏有一個debug模塊,能夠用它來作一些調試。(使用前提是你的環境變量設置了DEBUG,否則看不到輸出)npm
callback函數代碼:json
use方法,源碼給的註釋是:數組
Use the given middleware
fn
.bash
Koa裏面就是經過一個個的中間件來構建整個服務。
use方法的實現超級簡單:
上面callback函數中,有一個代碼:
const fn = componse(this.middleware)
複製代碼
它就是用來組合全部的中間件
好比咱們有這樣一段代碼:
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);
}
複製代碼
但願能獲得:一、三、五、六、四、2的結果。
這個代碼比較容易:
let fns = [fn1,fn2,fn3];
function dispatch(index){
let middle = fns[index];
// 判斷一下臨界點
if(fns.length === index) return function(){}
middle({},()=>dispatch(index+1));
}
dispatch(0);
複製代碼
理解了同步的寫法,當中間件的寫法爲asyn await時,就好寫了。
function dispatch(index){
let middle = fns[index];
if(fns.length === index) return Promise.resolve()
return Promise.resolve(middle({},()=>dispatch(index+1)))
}
複製代碼
一塊兒來看一下compose的代碼吧:
核心邏輯和上面的代碼差很少,無非是在邏輯判斷上更加地嚴謹一些。
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();
})
複製代碼
上面的結果是not found,緣由是第一個中間件那裏沒有await next。
咱們再去看createContext
的源碼實現:
就是對以前req對象從新包裝了一層。
這裏用了高級的語法: get/set,相似Object.definePrototype,主要能夠在set的時候作一些邏輯處理。
和request.js的處理方式相似。這裏我摘抄一段body的寫法:
{
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.js文件所作的事情就比較有意思了。
它作了一層代理,將request下的一些屬性方法以及response下的一些屬性方法直接掛載在ctx對象上。
譬如以前要經過ctx.request.url
來獲得請求路徑,如今只要寫成ctx.url
便可。
delegate
這個庫,咱們來簡單看一眼,主要看兩個方法便可:
咱們能夠再簡化一下:
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');
複製代碼
相信看了以後,對這個實現邏輯有了一個比較清晰的認知。
看完koa的源碼,咱們能夠知道koa自己很是小,實現地比較優雅,能夠經過寫中間件來實現本身想要的。
經常使用的中間件大概有:static、body-parser、router、session等。
koa-static是一個簡單的靜態中間件,它的源碼在這裏,核心邏輯實現是由koa-send完成,不過我翻了一下,裏面沒有etag的處理。
咱們本身也能夠寫一個最最簡單的static中間件:
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();
}
}
}
複製代碼
基礎代碼以下:
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;
複製代碼
無非Buffer.concat(buffers)
會有幾種狀況須要處理一下,如form、json、file等。
在koa-bodyparser中,它用co-body
包裝了一層。
form和json的處理相對比較容易:
querystring.parse(buff.toString()); // form的處理
JSON.parse(buff.toString()); // json的處理
複製代碼
這裏須要說一下的是,file是如何處理的:
這裏須要封裝一個Buffer.split
方法,來獲得中間的幾塊內容,再進行切割處理。
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;
複製代碼
固然像koa-better-body
裏面用的file處理,並無使用split。它用了formidable
截取操做都是在multipart_parser.js文件中處理的。
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());
複製代碼
掘金上有一篇文章:解讀並實現一個簡單的koa-router
我這邊也按本身看源碼的思路分析一下吧。
router.routes
是返回一箇中間件:
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;
};
複製代碼
它作的事情就是請求進來會通過 router.match,而後將匹配到的 route 的執行函數 push 進數組,並經過 compose(koa-compose) 函數合併返回且執行。
像咱們寫router.get/post
,所作的事就是註冊一個路由,而後往this.stack裏面塞入layer的實例:
另外像匹配特殊符號,如:/:id/:name
,它是利用path-to-regexp來作處理的。
看懂上面這些,再結合掘金的那篇源碼分析基本能搞的七七八八。
Koa的東西看上去好像比較簡單似的,可是仍是有不少東西沒有分析到,好比源碼中的proxy等。
不過根據二八法則,咱們基本上只要掌握80%的源碼實現就好了。
爲個人博客打個廣告,歡迎訪問:小翼的前端天地