通过模拟一个极简版本的 Koa 学习实现原理。

初始化项目

# 安装 koa
npm i koa

添加启动文件:

// app.js
const Koa = require('koa')const app = new Koa()app.listen(3000, () => {console.log('server is running on http://localhost:3000')
})

nodemon ./app.js启动服务。

源码目录结构

查看 node_modules/koapackage.json,查看加载 koa 时实际加载的文件:

"main": "lib/application.js",
// node_modules\koa\lib\application.js
module.exports = class Application extends Emitter {listen(...args) {debug('listen');// 使用原生 http 模块开启 HTTP 服务,成功后调用回调const server = http.createServer(this.callback());return server.listen(...args);}callback() {// 获取全部中间件并依次调用const fn = compose(this.middleware);if (!this.listenerCount('error')) this.on('error', this.onerror);const handleRequest = (req, res) => {const ctx = this.createContext(req, res);return this.handleRequest(ctx, fn);};return handleRequest;}// 挂载中间件的方法use(fn) {...}...
}

Koa 中大量使用了 ES6 的语法(例如 class)进行编写,加载 koa,实际上加载的是 Application 类,通过实例化这个类创建 app

lib 下其他文件:

├─ application.js # 负责整个应用的创建组织
├─ context.js # 处理 context 上下文对象
├─ request.js # 内部封装的 context.request 对象
└─ response.js # 内部封装的 context.response 对象

下面开始模仿一个极简的 Koa。

基础结构

在项目目录下新建一个 my-koa 文件夹存放模拟代码。

mkdir my-koa
cd ./my-koa
# 初始化 npm 以在该目录下安装依赖
npm init -y

修改入口文件路径:

// my-koa/package.json
"main": "lib/application.js",

添加文件:

// my-koa\lib\application.js
const http = require('http')class Application {listen(...args) {const server = http.createServer((req, res) => {res.end('My Koa')})server.listen(...args)}
}module.exports = Application

重新访问 http://localhost:3000 测试结果。

中间件

示例代码

const one = (ctx, next) => {console.log('>> one')next()console.log('<< one')
}const two = (ctx, next) => {console.log('>> two')// next()console.log('<< two')
}const three = (ctx, next) => {console.log('>> three')next()console.log('<< three')
}app.use(one)
app.use(two)
app.use(three)console.log(app.middleware)

模拟实现

// my-koa\lib\application.js
const http = require('http')class Application {constructor() {// 保存用户添加的中间件函数this.middleware = []}listen(...args) {const server = http.createServer(this.callback())server.listen(...args)}use(fn) {this.middleware.push(fn)}// 异步递归遍历调用中间件处理函数compose(middleware) {// 返回一个高级函数,允许接受其他参数return function () {const dispatch = index => {if (index >= middleware.length) {return Promise.resolve()}const fn = middleware[index]// 将中间件函数调用包装为一个 Promise 兼容异步处理return Promise.resolve(// fn(ctx, next)fn({}, () => dispatch(index + 1)))}// 返回并调用第一个中间件处理函数return dispatch(0)}}callback() {// 获取调用第一个中间件函数的方法const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {// 开始执行第一个中间件函数fnMiddleware().then(() => {console.log('end')res.end('My Koa')}).catch(err => {console.log(err.message)})}return handleRequest}
}module.exports = Application

分析 context 对象的内容组成

Koa Context 实际上是将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。

每一个请求都将创建一个 Context,并在中间件中作为参数引用。

// 打印 Koa Context:
{request: {method: 'GET',url: '/',header: {...}}, // Koa 封装的 request 对象response: {status: 404,message: 'Not Found',header: {...}}, // Koa 封装的 response 对象app: { ... }, // app 实例originalUrl: '/',req: '<original node req>', // node 原生 request 对象res: '<original node res>', // node 原生 response 对象socket: '<original node socket>' //  // node 原生 socket 对象
}

示例代码

app.use((ctx, next) => {console.log(ctx)// node 原生对象console.log(ctx.req)console.log(ctx.res)console.log(ctx.req.url)// Koa 封装的 Request 对象console.log(ctx.request)console.log(ctx.request.header)console.log(ctx.request.method)console.log(ctx.request.url)console.log(ctx.request.path)console.log(ctx.request.query)// Request 别名console.log(ctx.header)console.log(ctx.method)console.log(ctx.url)console.log(ctx.path)console.log(ctx.query)// Koa 封装的 Response 对象console.log(ctx.response)// ctx.response.status = 200// ctx.response.message = 'Success'// ctx.response.type = 'plain'// ctx.response.body = 'Hello Koa'// Response 别名ctx.status = 200ctx.message = 'Success'
})

初始化 Context 上下文对象

// my-koa\lib\application.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')class Application {constructor() {// 保存用户添加的中间件函数this.middleware = []// 拷贝创建,避免互相污染this.context = Object.create(context)this.request = Object.create(request)this.response = Object.create(response)}listen(...args) {...}use(fn) {...}// 异步递归遍历调用中间件处理函数compose(middleware) {// 返回一个高级函数,允许接受其他参数return function (context) {const dispatch = index => {...// 将中间件函数调用包装为一个 Promise 兼容异步处理return Promise.resolve(// fn(ctx, next)fn(context, () => dispatch(index + 1)))}// 返回并调用第一个中间件处理函数return dispatch(0)}}// 构造上下文对象createContext(req, res) {// 为了避免请求之间 context 数据交叉污染// 这里为每个请求单独创建 context 对象const context = Object.create(this.context)// 在 context 中可以获取 Requestconst request = context.request = Object.create(this.request)// 在 context 中可以获取 Responseconst response = context.response = Object.create(this.response)context.app = request.app = response.app = this// 原生 Node 请求/响应对象context.req = request.req = response.req = reqcontext.res = request.res = response.res = res// 在 Request 和 Respon 中也可以获取 context 上下文对象request.ctx = response.ctx = context// Requset 中也可以获取 Responserequest.response = response// Response 中也可以获取 Requsetresponse.request = request// 没有经过任何处理的请求路径context.originUrl = request.originUrl = req.url// 初始化 state 数据对象,用于给模板视图提供数据context.state = {}return context}callback() {// 获取调用第一个中间件函数的方法const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {// 每个请求都会创建一个独立的 Context 对象,它们之间不会互相污染const context = this.createContext()// 开始执行第一个中间件函数fnMiddleware(context).then(() => {console.log('end')res.end('My Koa')}).catch(err => {console.log(err.message)})}return handleRequest}
}module.exports = Application
// my-koa\lib\context.js
const context = {}module.exports = context
// my-koa\lib\request.jsmy-koa\lib\request.js/
const request = {}module.exports = request
// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {}module.exports = response

扩展 Request 和 Response

使用对象的访问器属性(get 和 set)动态获取和设置属性。

// my-koa\lib\request.jsmy-koa\lib\request.js/
const url = require('url')const request = {get header() {return this.req.headers},set header(val) {this.req.headers = val},get headers() {return this.req.headers},set headers(val) {this.req.headers = val},get url() {return this.req.url},get path() {return url.parse(this.req.url).pathname},get query() {return url.parse(this.req.url, true).query},get method() {return this.req.method}
}module.exports = request
// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {set status(val) {this.res.statusCode = val},set message(msg) {this.res.statusMessage = msg;},
}module.exports = response

处理 Context 中的代理别名

// my-koa\lib\context.js
const context = {get method() {return this.request.method},get header() {return this.request.header},...
}module.exports = context

可以看到 context 别名的 getter 函数处理逻辑都一样(归功于 request 中定义了同名的属性),所以可以将设置别名的操作提取为一个方法:

// my-koa\lib\context.js
const context = {}definePorpperty('request', 'method')
definePorpperty('request', 'header')
definePorpperty('request', 'url')
definePorpperty('request', 'path')
definePorpperty('request', 'query')function definePorpperty(target, name) {Object.defineProperty(context, name, {get() {return this[target][name]},set(value) {this[target][name] = value}})
}module.exports = context

注意:Koa 中使用的 delegates 包,内部使用 Object.prototype.__defineGetter__()(MDN)和 Object.prototype.__defineSetter__()(MDN)设置 get/set 属性,不过该特性已从 Web 标准中删除。

设置和发送 body 数据

  • 本质上使用的是 node 的 Response 对象发送数据
  • 多次设置 body 最终响应的应该是最后一次设置的内容

示例代码

app.use((ctx, next) => {ctx.body = 'Hello Koa1'next()ctx.body = 'Hello Koa3'
})app.use((ctx, next) => {ctx.body = 'Hello Koa2'
})// 最终应该响应 Hello Koa3

模拟实现

在 Response 对象中设置 body 的 getter 和 setter:

// my-koa\lib\request.jsmy-koa\lib\response.js/
const response = {set status(val) {this.res.statusCode = val},set message(msg) {this.res.statusMessage = msg;},_body: '', // 真正用来存数据的属性get body() {return this._body},set body(val) {this._body = val}
}module.exports = response

添加 context 的 body 别名:

// my-koa\lib\context.js
definePorpperty('response', 'body')

执行完中间件后(洋葱圈从进到出)将 body 返回给客户端:

// my-koa\lib\application.js
...class Application {...callback() {const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {const context = this.createContext(req, res)// 开始执行第一个中间件函数,并传入上下文对象fnMiddleware(context).then(() => {res.end(context.body)}).catch(err => {res.end(err.message)})}return handleRequest}
}module.exports = Application

处理 body 数据格式

示例代码

response.body 支持一下格式,如果 res.status 没有赋值,Koa会自动设置为 200204。:

// app.js
// const Koa = require('koa')
const Koa = require('./my-koa')
const fs = require('fs')
const fsPromises = require('fs').promisesconst app = new Koa()app.use(async (ctx, next) => {// 字符串ctx.body = 'string'// // 数字// ctx.body = 123// // buffer// ctx.body = await fsPromises.readFile('./package.json')// // 文件流// ctx.body = fs.createReadStream('./package.json')// // 对象&数组会转化成 JSON 字符串// ctx.body = { foo: 'bar' }// ctx.body = [1, 2, 3]// // 无响应内容// ctx.body = null
})app.listen(3000, () => {console.log('server is running on http://localhost:3000')
})

模拟实现

执行完中间件后,调用一个方法处理 body 并返回给客户端:

// my-koa\lib\application.js
...
// 引入 node 原生 stream 构造函数
const Stream = require('stream')class Application {...callback() {const fnMiddleware = this.compose(this.middleware)const handleRequest = (req, res) => {const context = this.createContext(req, res)fnMiddleware(context).then(() => {// res.end(context.body)// 调用函数处理 bodyrespond(context)}).catch(err => {res.end(err.message)})}return handleRequest}
}function respond(ctx) {const body = ctx.bodyconst res = ctx.res// 字符串 和 Buffer 直接返回if (typeof body === 'string') return res.end(body)if (Buffer.isBuffer(body)) return res.end(body)// 可读流通过管道发送给可写流(res)if (body instanceof Stream) return body.pipe(res)// 数字转化成字符串if (typeof body === 'number') return res.end(body + '')// 对象和数组转化成 JSON 字符串if (body !== null && typeof body === 'object') {const jsonStr = JSON.stringify(body)return res.end(jsonStr)}res.statusCode = 204res.end()
}module.exports = Application

Koa 学习 02 Koa 实现原理和极简模拟案例相关推荐

  1. Koa 学习 01 Koa 介绍和基本使用(路由、静态资源托管、中间件)

    Koa 介绍 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的一个更小.更富有表现力.更健壮的基石. 官网:https://k ...

  2. 做diff_Virtual Dom amp;amp; Diff原理,极简版

    前言 先介绍一个概念Virtual Dom,我猜大家或多或少都应该知道什么是Virtual Dom吧,简单来说就是用js来模拟DOM中的节点. Virtual Dom 下面就是一个Virtual Do ...

  3. Vue的双向数据绑定原理(极简版)

    先说面试答案: 答: vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者 ...

  4. 合适学习人工智能的小白的一本书《极简AI入门:一本书读懂人工智能思维与应用》

    今天看了一本书<极简AI入门:一本书读懂人工智能思维与应用> 对于初学人工智能的小白来说,应该是非常容易看得懂的,书里罗列了人工智能需要学习的各个技能,可以把这本书当作学习人工智能的目录( ...

  5. 【nodejs原理源码赏析(2)】KOA中间件的基本运作原理

    [摘要] KOA中间件的基本运作原理 示例代码托管在:http://www.github.com/dashnowords/blogs 在中间件系统的实现上,KOA中间件通过async/await来在不 ...

  6. Koa学习(一)——Koa介绍

    Koa--基于Node.js平台的下一代Web框架. Koa2介绍 1. Koa简介 2. Koa历史版本 3. 应用场景 4. Koa扩展框架 1. Koa简介 Koa 官网 Koa所谓的" ...

  7. Surf算法学习心得(一)——算法原理

    Surf算法学习心得(一)--算法原理 写在前面的话: Surf算法是对Sift算法的一种改进,主要是在算法的执行效率上,比Sift算法来讲运行更快!由于我也是初学者,刚刚才开始研究这个算法,然而网上 ...

  8. [深度学习基础] 深度学习基础及数学原理

    图像分类 (image classification) 问题是指, 假设给定一系列离散的类别(categories)(如猫, 狗, 飞机, 货车, ...), 对于给定的图像, 从这些类别中赋予一个作 ...

  9. 极简权限认证必须掌握【代码+原理+建议收藏】

    这个极简权限认证必须掌握,代码不过百,但是很关键 小白最近没有来问学委问题,不过前几天,有朋友问到如何进行访问控制,资源控制的,学委特地写了一篇. 这其实就是权限认证,理解并掌握其核心思想很重要,而且 ...

最新文章

  1. Java相对路径读取文件
  2. php 转化数字,php实现中文转数字
  3. C99 calloc、malloc和realloc区别
  4. 关于MFC共享DLL的模块状态切换 .
  5. 在Spark中自定义Kryo序列化输入输出API(转)
  6. java hibernate 分页查询_4 Hibernate HQL查询,分页查询
  7. python中创建集合的语句_Python 集合(set) 介绍
  8. 安卓布局中xml文件属性和ID简介
  9. 阿里中间件开源组件:Sentinel 0.2.0正式发布
  10. 我的YUV播放器MFC小笔记:注册表读写
  11. c++中的构造函数和析构函数
  12. 倒计时_考研倒计时30天,拼了
  13. bzoj4754: [Jsoi2016]独特的树叶
  14. 使 VC2013 编写的程序运行在其它电脑上
  15. java面试项目介绍,详细说明
  16. windows实用工具集
  17. 数据库-SQL语句创建与修改
  18. 英语语法新思维初级分享
  19. python xgboost建模过程_python - Dask中的XGBoost建模 - SO中文参考 - www.soinside.com
  20. Scanner类、Random类、ArrayList 类

热门文章

  1. python的复数四则运算代码_复数四则运算源代码
  2. docker 安装 mssql
  3. 零基础考二级python大概需要拿出多长时间?
  4. 免费的Windows系统的条形码打印工具
  5. JavaFX- Hyperlink
  6. ubuntu下如何使用USB转串口设备
  7. 达人评测 i5 1340P和i5 13420H对比 酷睿i51340P和i513420H差距
  8. QT应用编程:基于VLC开发音视频播放器(句柄方式)
  9. Docker微服务-Portainer
  10. 融云小程序解决方案-小程序,大能量!