koa原理浅析
koa原理浅析
选取的版本为koa2原文链接
koa的源码由四个文件组成
application.js koa的骨架 context.js ctx的原型 request.js request的原型 response.js response的原型
基本用法
const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);
初始服务器
利用http模块创建服务器
const app = http.createServer((req, res) => { ... }) app.listen(3000)
事实上koa把这些包在了其listen方法中
listen(...args) { debug('listen'); const server = http.createServer(this.callback()); return server.listen(...args); }
显然this.callback()返回的是一个形如下面的函数
(req, res) => {}
上下文ctx
callback方法如下
callback() { const fn = compose(this.middleware); if (!this.listeners('error').length) this.on('error', this.onerror); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; }
ctx在koa中事实上是一个包装了request和response的对象,从createContext中可以看到起继承自context
createContext(req, res) { const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); context.app = request.app = response.app = this; context.req = request.req = response.req = req; context.res = request.res = response.res = res; request.ctx = response.ctx = context; request.response = response; response.request = request; context.originalUrl = request.originalUrl = req.url; context.cookies = new Cookies(req, res, { keys: this.keys, secure: request.secure }); request.ip = request.ips[0] || req.socket.remoteAddress || ''; context.accept = request.accept = accepts(req); context.state = {}; return context; }
可以看到ctx.request继承自request,ctx.response继承自response,查看response和request可以看到里面大都是set和get方法(获取query,设置header)等等。并且ctx代理了ctx.request和ctx.response的方法,在源码中可以看到
delegate(proto, 'response') .method('attachment') .method('redirect') .method('remove') .method('vary') .method('set') .method('append') .method('flushHeaders') .access('status') .access('message') .access('body') .access('length') .access('type') .access('lastModified') .access('etag') .getter('headerSent') .getter('writable'); /** * Request delegation. */ delegate(proto, 'request') .method('acceptsLanguages') .method('acceptsEncodings') .method('acceptsCharsets') .method('accepts') .method('get') .method('is') .access('querystring') .access('idempotent') .access('socket') .access('search') .access('method') .access('query') .access('path') .access('url') .getter('origin') .getter('href') .getter('subdomains') .getter('protocol') .getter('host') .getter('hostname') .getter('URL') .getter('header') .getter('headers') .getter('secure') .getter('stale') .getter('fresh') .getter('ips') .getter('ip');
所以我们可以直接这么写
ctx.url 等价于 ctx.request.url
中间件
我们再看一下callback函数,观察发现compose模块十分的神奇,我暂且把它称为是一个迭代器,它实现了中间件的顺序执行
const fn = compose(this.middleware); 打印fn如下 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, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } }
最初接触koa的时候我疑惑为什么我写了
ctx.body = 'hello world'
并没有ctx.response.end()之类的方法,事实上koa已经帮我们做了处理,在handleRequest方法中
const handleResponse = () => respond(ctx); // fnMiddleware即为上面compose之后的fn fnMiddleware(ctx).then(handleResponse).catch(onerror)
fnMiddleware返回的是一个promise,在中间件逻辑完成后在respond函数中最终去处理ctx.body
function respond(ctx) { // allow bypassing koa if (false === ctx.respond) return; const res = ctx.res; if (!ctx.writable) return; let body = ctx.body; const code = ctx.status; // ignore body if (statuses.empty[code]) { // strip headers ctx.body = null; return res.end(); } if ('HEAD' == ctx.method) { if (!res.headersSent && isJSON(body)) { ctx.length = Buffer.byteLength(JSON.stringify(body)); } return res.end(); } // status body if (null == body) { body = ctx.message || String(code); if (!res.headersSent) { ctx.type = 'text'; ctx.length = Buffer.byteLength(body); } return res.end(body); } // responses if (Buffer.isBuffer(body)) return res.end(body); if ('string' == typeof body) return res.end(body); if (body instanceof Stream) return body.pipe(res); // body: json body = JSON.stringify(body); if (!res.headersSent) { ctx.length = Buffer.byteLength(body); } res.end(body); }
错误处理
- (非首部)中间件层处理(我瞎起的)
对于每个中间件可能发生的错误,可以直接在该中间件捕获
app.use((ctx, next) => { try { ... } catch(err) { ... } })
- (首部)中间件层处理
事实上,我们只要在第一个中间件添加try... catch... ,整个中间件组的错误都是可以捕获的到的。
- (应用级别)顶层处理
app.on('error', (err) = {})
在上面中间件执行时看到,koa会自动帮我们捕获错误并处理,如下
try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { // 捕获错误 return Promise.reject(err) } // 在ctx.onerror中处理 const onerror = err => ctx.onerror(err); fnMiddleware(ctx).then(handleResponse).catch(onerror)
我们看ctx.onerror发现它事实上是出发app监听的error事件
onerror(err) { // delegate this.app.emit('error', err, this);
假如我们没有定义error回调怎么办呢,koa也为我们定义了默认的错误处理函数
callback方法做了判断
callback() { ... if (!this.listeners('error').length) this.on('error', this.onerror); ... }
全文完