本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
学习目标
- 了解koa的洋葱模型
- 了解 koa-compose 作用
- koa的实现原理
- nodejs调试技巧
源码地址
调试方案
首先,将项目clone到本地
git clone git@github.com:baosisi07/koa-analysis.git
1. VS Code本地调试
VS Code内置了debug调试配置,可在本地新增launch.json文件,选择不同的调试配置。具体步骤如下:
如果项目目录下没有launch.json时:
按图中步骤操作后,选择node.js
然后,目录下会多出文件 .vscode/launch.json。内容大致如下,调试的文件为koa/examples/middleware/app.js
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Program",
"program": "${workspaceFolder}/examples/middleware/app.js",
"request": "launch",
"skipFiles": [
"<node_internals>/**"
],
"type": "node"
}
]
}
此时,快捷键Command+Shift+P调出操作命令列表,搜索选择如下所示:
然后选择json中配置好的debug名称:
如果你本地的node管理用的是nvm,那么会报如下错误:
这是因为没有指定node的版本,debug无法运行,添加如下配置即可,按实际安装使用的版本配置runtimeVersion,我本地用的是16.14.0
{
"name": "Launch Program",
...
"runtimeVersion": "16"
}
配置完成就可以愉快地调试了✌️!
添加断点——> 运行debug并调试
2. chrome调试
- 首先打开chrome浏览器,地址栏输入chrome://inspect,然后添加监听配置,如下图所示:
- 项目下输入命令
node --inspect examples/middleware/app.js - 回到浏览器,打开
inspect,并打上断点调试
koa洋葱模型
这是洋葱模型最经典的图
可以简单地将 next() 之前的任意代码视为“捕获”阶段,中间件以一个类似于栈的结构组成并依次执行。下图为实际执行中间件的顺序,可以体会到上图的含义
可以从下图清晰地了解到koa中间件的执行流程:
我们从中间件一的
beforeNext 任务开始执行,然后按照紫色箭头的执行步骤完成中间件的任务调度。要完成任务调度,我们就需要不断地取出中间件来执行,这就是koa-compose的核心功能。
koa主体流程
new Koa()时,生成Application类的一个实例,而Application继承自nodejs内置模块events,实例的初始化包含middleware、context、request、response等属性app.use(fn)实际是往middleware属性push中间件函数,可以理解为注册中间件,为了实现链式调用,还要返回当前实例自身thisapp.listen(arg)实际是http.createServer(app.callback()).listen(arg)的语法糖app.callback()实际是转换了函数入参(req,res)为(ctx, fn)主要代码如下:
const Emitter = require('events');
module.exports = class Application extends Emitter {
constructor(options) {
super();
options = options || {};
this.proxy = options.proxy || false;
this.subdomainOffset = options.subdomainOffset || 2;
this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
this.maxIpsCount = options.maxIpsCount || 0;
this.env = options.env || process.env.NODE_ENV || 'development';
if (options.keys) this.keys = options.keys;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
if (util.inspect.custom) {
this[util.inspect.custom] = this.inspect;
}
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
use(fn) {
this.middleware.push(fn);
return this;
}
callback() {
const fn = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
handleRequest(ctx, fnMiddleware) {
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
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.state = {};
return context;
}
};
listen函数可以简单梳理为:
listen(...args) {
const fnMiddleware = compose(this.middleware);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
};
const server = http.createServer(handleRequest);
return server.listen(...args);
}
koa-compose
这里的 compose(this.middleware) 是我们今天讨论的重点,它相当于一个任务调度系统,控制着中间件的执行。
先看下代码实现:
function compose (middleware) {
middleware = flatten(middleware)
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)
}
}
}
}
- 首先,
compose函数接受中间件数组middleware作为参数 - 执行
compose函数返回一个类似中间件的函数,参数context,next
在执行中间件之前,注册完中间件之后,callback()函数执行后返回的handleRequest(ctx, fn)被执行,而此函数会调用中间件函数fn(如下图1中的fnMiddleware),也就是调用compose返回的函数(如下图2),此时会执行dispatch(0)
图1:
图2:
dispatch(0)
由上图可知,当在第一个中间件内部调用
next 函数,其实就是继续调用 dispatch 函数,此时参数 i 的值为 1。
dispatch(1)
由上图可知,当在第二个中间件内部调用
next 函数,仍然是调用 dispatch 函数,此时参数 i 的值为 2。
dispatch(2)
由上图可知,当在第三个中间件内部没有调用
next 函数,这时候不会调用 dispatch 函数,直接返回异步任务结果,回到第二个中间件的next()之后继续执行。
同样地,中间件二执行结束也返回异步任务结果,回到第一个中间件的next()之后继续执行,直到所有的中间件都执行完毕。
理解了就可以把代码重写了一遍了,简化代码可参考:koa-compose简化版
总结
koa-compose理解起来确实有些难,需要平时的积累,对异步任务有深刻的理解,对async/await和Promise都要有深入的研究,才能比较轻松的理解koa-compose的实现及原理。加油学习吧!