之前用koa写过不少的demo,加一个实际的线上应用,但一直没有怎么看过它的源码。
这次抽空看了一下源码。
它其实只有4个文件:
- application.js (主文件)
- context.js (创建网络请求的上下文对象)
- request.js (包装 koa 的 request 对象)
- response.js (包装 koa 的 response对象)
通过package.json
文件,我们可以清楚地看到:
application.js
是入口文件,那么进去看看吧。
核心方法
- listen
- use
基础用法
const Koa = require('koa');const app = new Koa();app.listen(3000);app.use((ctx, next) => { ctx.body = "hello"})复制代码
listen
就是起了一个服务。
这里有一个模块,可以用它来做一些调试。(使用前提是你的环境变量设置了DEBUG,不然看不到输出)
callback函数代码:
use
use方法,源码给的注释是:
Use the given middleware
fn
.
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);}复制代码
希望能得到:1、3、5、6、4、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。
ctx
我们再去看createContext
的源码实现:
request
就是对之前req对象重新包装了一层。
这里用了高级的语法: get/set,类似Object.definePrototype,主要可以在set的时候做一些逻辑处理。
response
和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* 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
文件所做的事情就比较有意思了。
它做了一层代理,将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-static是一个简单的静态中间件,它的源码在,核心逻辑实现是由完成,不过我翻了一下,里面没有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(); } }} 复制代码
body-parser
基础代码如下:
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等。
在中,它用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。它用了
截取操作都是在文件中处理的。
koa-router
基础用法
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());复制代码
原理
掘金上有一篇文章:
我这边也按自己看源码的思路分析一下吧。
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
,它是利用来做处理的。
看懂上面这些,再结合掘金的那篇源码分析基本能搞的七七八八。
总结
Koa的东西看上去好像比较简单似的,但是还是有很多东西没有分析到,比如源码中的proxy等。
不过根据二八法则,我们基本上只要掌握80%的源码实现就行了。
最后的最后
为我的博客打个广告,欢迎访问: