[源码] koa

341 阅读11分钟

koa源码.png

导航

[react] Hooks

[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式
[封装02-设计模式] 命令模式 享元模式 组合模式 代理模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] koa
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick

[源码-react01] ReactDOM.render01
[源码-react02] 手写hook调度-useState实现

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例
[深入24] Fiber
[深入25] Typescript

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson
[前端学java08-SpringBoot实战总结1-7] 阶段性总结
[前端学java09-SpringBoot实战] 多模块配置 + Mybatis-plus + 单多模块打包部署
[前端学java10-SpringBoot实战] bean赋值转换 + 参数校验 + 全局异常处理
[前端学java11-SpringSecurity] 配置 + 内存 + 数据库 = 三种方式实现RBAC
[前端学java12-SpringSecurity] JWT
[前端学java13-SpringCloud] Eureka + RestTemplate + Zuul + Ribbon

(一) 前置知识

(1) 一些单词

proper 非常 正确的 adj // properly 正确地 adv
dealing with 处理
expose 暴露 陈述
silent 沉默的 寂静
poll 轮询
stderr 标准错误

noop 空函数

(2) koa用到的nodeJs中的一些api

1
http.createServer()
koa案列:koa.listen(3000) -> http.createServer -> server.listen(3000)
nodeApi:
  - http.createServer([options][, requestListener])
    - 参数
      - options:是一个配置对象,可选,一般不用配置
      - requestListener:请求监听函数,可选
    - 返回值
      - http.Server的新实例,返回的新实例上具有 listen() 方法
官方文档:http://nodejs.cn/api/http.html#http_http_createserver_options_requestlistener
---
案例:
const http = require('http');
const server = http.createServer((req, res) => { 
  // 创建本地服务器来从其接收数据
  // req请求
  // res响应
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ // --- res.end()
    data: 'Hello World!'
  }));
});
server.listen(8000); // --------- server.listen()
2
process.nextTick
函数签名:process.nextTick(callback[, ...args])
作用:微任务 - 在同步方法执行完毕后,下一轮事件循环中的开始执行
特点:
  - 执行时机在同步任务之后,在异步任务宏任务setTimeout之前
  - 其实process.nextTick()会在node事件循环的各个周期优先执行
参数:
  - 1. callback回调函数 
  - 2. args当调用callback时要传入的额外参数


3
node.js事件轮训机制 - 一共分为6个阶段
(1) timers 定时器阶段
- ( 计时 ) 和 ( 执行到点的定时器 )
(2) pending callbacks 阶段
- 执行某些系统操作的回调函数,比如 ( tcp错误类型 )
(3) idle, prepare 阶段
- 一些准备工作
(4) poll轮询阶段,是一个轮询队列
- 1. 如果 ( 轮询队列不为空 ),依次取出执行,直到 ( 轮询队列为空 ) 或者 ( 达到系统最大限制 )
- 2. 如果 ( 轮询队列为空 )
     - 1. 如果之前设置过 ( setImmediate ) 函数,则直接进入下一个阶段 ( check阶段 )
     - 2. 如果之前没有设置过setImmediate函数,则会在当前poll阶段 ( 等待 )
          - 直到 ( 轮询队列 ) 添加进了新的回调函数,那么就会进入(4)阶段1的判断,继续执行
          - 或者 ( 定时器 ) 到点了,也会进入下一个阶段 ( check阶段 )
(5) check 阶段
- 执行 ( setImmediate ) 回调函数
(6) close callbacks 阶段
- 执行 ( close ) 事件回调函数
-------> 注意点:process.nextTick() 会在nodejs事件轮询的 ( 任意阶段,优先执行 )


---
案例
console.log(1);  // 同步任务
setTimeout(() => console.log(2)); // timer阶段执行 - nodejs事件轮询的第 1 个阶段
setTimeout(() => console.log(8), 0); // timer阶段执行 - nodejs事件轮询的第 1 个阶段
process.nextTick((n) => console.log(n), 3); // --- 在 node.js 事件轮询的 ( 任意阶段,优先执行 ),即在同步任务执行完毕后,优先执行
setImmediate(() => console.log(4)); // check阶段执行 - nodejs事件轮询的第 5 个阶段
new Promise((resolve) => {
  console.log(5); // 同步任务
  resolve();
  console.log(7); // 同步任务
}).then((res) => console.log(6)); // --- 微任务
// 执行顺序 1 5 7 3 6 2 8 4
// 同步任务 1 5 7
// 异步任务(微任务) 3 6
// 异步任务(宏任务) 2 8 4

(3) koa的一些api

1
const app = new Koa()

- 1
- 问题:app上有哪些属性?
- 回答:
app.env -----------> 环境变量 -> 默认是 NODE_ENV 或 'development'
app.keys ----------> 签名的cookie密钥数组
app.proxy ---------> 当真正的代理头字段将被信任时忽略 `.subdomains` 的 `app.subdomainOffset` 偏移量,默认为 2
app.proxyIpHeader -> 代理ip消息头
app.maxIpCount ----> 从代理ip消息头读取的最大ips,默认是0表示无限
- 2
- 问题:如何设置这些属性
- 回答:有两种方法
  - 1. 通过参数来设置: const app = new Koa({proxy: true}) 
  - 2. 动态来设置:app.proxy = true
2
context
- Koa Context 将 node 的 `request` 和 `response` 对象封装到单个对象中
- 每个请求都将创建一个 `Context`,并在中间件中作为接收器引用,或者 `ctx` 标识符
app.use(async ctx => {
  ctx; // 这是 Context
  ctx.request; // 这是 koa Request
  ctx.response; // 这是 koa Response
  ctx.req; // Node 的 `request` 对象
  ctx.res; // Node 的 `response` 对象
  ctx.state; // 推荐的命名空间,用于通过中间件传递信息和你的前端视图
  ctx.app; // 引用程序实例引用
});

(4) koa中间件的使用

  • koa应用程序
    • koa应用程序是一个包含 ( 一组中间件函数 ) 的对象,按照类似 ( 堆栈 ) 的方式组织和执行
  • 中间价
    • 中间件可以完成比如:内容协商缓存清理代理支持重定向
  • next()
    • 当一个中间件调用 next(),则该函数暂停,并将控制传递给定义的 ( 下一个中间件 )
    • 当下游没有更多中间件执行后,( 堆栈将展开 ) 并且 ( 每个中间键恢复执行 ) 其上游行为
1
koa中间件的执行顺序
---
const Koa = require("./lib/application.js");
const app = new Koa();
app.use(async (ctx, next) => { console.log(1); await next(); console.log(2); });
app.use(async (ctx, next) => { console.log(3); await next(); console.log(4); });
app.use(async (ctx, next) => { console.log(5); ctx.body = "测试中间执行顺序"; });
app.listen(1000, () => 'app run 1000')
---
// 执行顺序为 1 3 5 4 2
// 原理:
// - 1. 当中间件函数中,执行了next()方法时,其实就是执行下一个中间件,递归的执行下去
// - 2. 当执行完next(),接着执行当前中间件函数中剩下的代码,顺序就是函数调用栈的顺序
// - 3. 其实就是洋葱模型
2
context对象中包含了哪些属性 - ( ctx对象 )
---
context.app -> app属性指的是const app = new Koa() 生成的koa实例
context.req
context.res
context.request
context.response
context.originalUrl
context.state
---
app.use(async (ctx, next) => { console.log(1); await next(); console.log(2) })

image.png

(5) Object.create()

Obejct.create
---
const instanceObj = Object.create(prototypeObj)

1. 作用:以 ( 参数对象 ) 为 ( 原型 ),生成 ( 实例对象 ),实例对象完全继承参数对象的属性和方法
2. 注意点:生成的实例对象,( 实例对象本身没有任何属性和方法 ),只是继承了参数对象的所有属性和方法
3. 例子
const a = {name: []}
const b = Object.create(a)
b // {}
b.name === a.name // true
Object.getPrototypeof(b) === a // true

4. Object.create() 方法的模拟实现
- 实现原理:通过 ( 构造函数式继承 ) 来实现
function create(obj) {
    function F(){} // 声明一个构造函数
    F.prototype = obj // 将构造函数F的prototype赋值为obj,这样通过 new F() 生成的实例就能继承 obj 对象的属性和方法
    return new F() // 返回实例
}

(6) 如何调试koa源码

1. 克隆koa源码:git clone git@github.com:koajs/koa.git
2. 安装依赖:cnpm install
3. 新建 index.js,并写入示例代码
4. 在 `vscode` 中选择 `运行和调试`, 新建 `launch.json` ,选择 `node`,并做如下配置
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "启动程序",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "program": "${workspaceFolder}/index.js"
    }
  ]
}
5. 启动项目:node index.js
6. 在index.js中打断点,通过 `运行和调试` 菜单中的 `开始调试` 按钮进行断点吊饰即可

(二) KOA源码目录结构说明

1. 入口文件 
- 在package.json中通过 `main` 属性得知入口文件是 `lib/application.js`

2. 核心文件
- 核心文件都在 `lib` 文件夹中
- lib/application.js --> 主要就是Koa类相关代码,即 new Koa()
- lib/context.js ------> 是contxt对象相关
- lib/request.js ------> request相关
- bli/response.js -----> response相关

3. 依赖
- koa-compose ---------> 处理中间件
- on-finished ---------> 主要作用:当 HTTP 请求关闭、完成或出错时执行回调

(三) 源码主流程 - 中间件

1. 
首先有这样一段代码
const Koa = require("./lib/application.js"); 
const app = new Koa(); 
app.use(async (ctx, next) => { console.log(1); await next(); console.log(2); }); 
app.use(async (ctx, next) => { console.log(3); await next(); console.log(4); }); 
app.use(async (ctx, next) => { console.log(5); ctx.body = "测试中间执行顺序"; }); 
app.listen(1000, () => 'app run 1000')
--- 
// 执行顺序为 1 3 5 4 2 
// 原理: 
// - 1. 当中间件函数中,执行了next()方法时,其实就是执行下一个中间件,递归的执行下去 
// - 2. 当执行完next(),接着执行当前中间件函数中剩下的代码,顺序就是函数调用栈的顺序 
// - 3. 其实就是洋葱模型
2. 
const app = new Koa()
app.use()
app.listen()
对应源码如下
---
class Application extends Emitter {
  constructor(options) {}
  
  // ------------------------------- use
  use(fn) {
    this.middleware.push(fn);
    return this;
  }
  
  // ------------------------------- listen
  listen(...args) {
    const server = http.createServer(this.callback()); // http.createServer是node原生api
    return server.listen(...args);
  }
  
  // ------------------------------- callback
  callback() {
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res); // 创建context对象,包含request,resposne,req,res等属性
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
 }
 
  // ------------------------------- handleRequest
  handleRequest(ctx, fnMiddleware) {
    return fnMiddleware(ctx).then(handleResponse).catch(onerror); 
    // 中间件的执行过程
    // 所以核心就是这个 fnMiddleware(ctx)
  }
}
3
fnMiddleware(ctx) 
- fnMiddleware(ctx) 就是执行 const fn = compose(this.middleware) 返回的fn
- 而compose是依赖库 `koa-compose` ,具体代码如下
---

// ------------------------------------------------------------------------------------------------------------------- compose
// compose
// - 参数
//    - 中间价组成的数组,每个中间件必须是函数
// - 返回值
//    - 返回一个函数
function compose(middleware) {
  if (!Array.isArray(middleware))
    throw new TypeError("Middleware stack must be an array!"); // 参数必须是数组
  for (const fn of middleware) {
    if (typeof fn !== "function")
      throw new TypeError("Middleware must be composed of functions!"); // 每个中间件必须由函数组成
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      // 调用时:dispatch(0)

      if (i <= index) {
        return Promise.reject(new Error("next() called multiple times"));
        // i <= index 证明next()方法被多次调用
        // dispatch(0)时得出: ( 0<=-1不成立 )
      }

      index = i;
      // 1
      // i = 0 --> index = i = 0

      let fn = middleware[i];
      // 1
      // middleware[0] 第一个中间件函数

      if (i === middleware.length) fn = next;
      // 当为最后一个中间件时,继续执行dispatch() --> fn=next=undefined --> return Promise.resolve(),
      // - next不存在,return Promise.resolve()
      // - next存在,则执行next(context, dispatch.bind(null, i+1))

      // fn不存在
      if (!fn) return Promise.resolve();

      // fn存在
      // 则继续往下执行
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
        // 返回Promise成功的结果 -> 即执行中间件,中间件返回值作为promise成功状态的结果值
        // 1
        // 中间件函数的结构
        // app.use(async (ctx, next) => { ... })
        // async (ctx, next) => {...}
        // 2
        // fn(context, dispatch.bind(null, i + 1)) ---> next() 其实就是这里的 dispatch() 函数 ---> i+1就是下一个中间件
        
        // 2022.04.11 添加一个问题
        // 问题:到底是什么是中间件中的next
        // 回答:
        // - 1. 中间件 app.use(async (ctx, next) => { ... })
        // - 2. next 就是上面的 dispatch 函数
        // - 3. 而 dispatch 又会去执行下一个中间件,不断重复,直到最后一个中间件
        // - 4. 还要注意 next() 调用的时机,如果next() 后面还有代码,在next()执行完后,还会回来继续执行
        
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}
4
梳理中间件
- 调用顺序:app.listen() ---> callback() ---> handleRequest() ---> 中间件fn(ctx).then(handleResponse).catch(onerror)
- fnMiddleware = compose(this.middleware) = function (context, next) =>  dispatch(0)
---

中间件 - 具体的执行过程
1. fnMiddleware(ctx).then(handleResponse).catch(onerror)
2. (function (context, next) =>  dispatch(0)).then(handleResponse).catch(onerror)
3. dispatch(0).then(handleResponse).catch(onerror)
4. Promise.resolve(middlewareFn0(context, dispatch.bind(null,1)))
5. Promise.resolve(async(ctx, dispatch) => {... dispatch(1) ...})
6. 最终形态如下
const [fn1, fn2, fn3] = this.middleware()
const fnMiddleware = function(context, next) {
  return Promise.resolve(fn1(context, function next1() {
    return Promise.resolve(fn2(context, function next2() { // 每个中间件,如果存在next就return Promise.resolve(fn(context, dispatch.bind(null, i + 1))),不存在就 return Promise.resolve()
      return Promise.resolve(fn3(context, function next3() { // 最后一个中间件没有next函数了,因为已经是最后一个
        return Promise.resolve()
      }))
    }))
  }))
}

7. fnMiddleware() 执行的最终状态如下
- 7.1
app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  await next();
  console.log(4);
});
app.use(async (ctx, next) => {
  console.log(5);
  ctx.body = "测试中间执行顺序";
});
- 7.2
fnMiddleware() => Promise.resolve(
  // fn1()
  console.log(1)
  await Promise.resolve( // next()
    // fn2()
    console.log(3)
    await Promise.resolve( // next()
      // fn3()
      console.log(5)
      return Promise.resolve()
    )
    console.log(4)
  )
  console.log(2)
)
.then(handleResponse)
.catch(onerror)

// 13542

源码分析仓库地址

资料

koa官网 koa.bootcss.com/
koa源码分析 juejin.cn/post/699867…
川神 juejin.cn/post/684490…