Koa 学习 02 Koa 实现原理和极简模拟案例
通过模拟一个极简版本的 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/koa
的 package.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会自动设置为 200
或 204
。:
// 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 实现原理和极简模拟案例相关推荐
- Koa 学习 01 Koa 介绍和基本使用(路由、静态资源托管、中间件)
Koa 介绍 Koa 是一个新的 web 框架,由 Express 幕后的原班人马打造,致力于成为 web 应用和 API 开发领域中的一个更小.更富有表现力.更健壮的基石. 官网:https://k ...
- 做diff_Virtual Dom amp;amp; Diff原理,极简版
前言 先介绍一个概念Virtual Dom,我猜大家或多或少都应该知道什么是Virtual Dom吧,简单来说就是用js来模拟DOM中的节点. Virtual Dom 下面就是一个Virtual Do ...
- Vue的双向数据绑定原理(极简版)
先说面试答案: 答: vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者 ...
- 合适学习人工智能的小白的一本书《极简AI入门:一本书读懂人工智能思维与应用》
今天看了一本书<极简AI入门:一本书读懂人工智能思维与应用> 对于初学人工智能的小白来说,应该是非常容易看得懂的,书里罗列了人工智能需要学习的各个技能,可以把这本书当作学习人工智能的目录( ...
- 【nodejs原理源码赏析(2)】KOA中间件的基本运作原理
[摘要] KOA中间件的基本运作原理 示例代码托管在:http://www.github.com/dashnowords/blogs 在中间件系统的实现上,KOA中间件通过async/await来在不 ...
- Koa学习(一)——Koa介绍
Koa--基于Node.js平台的下一代Web框架. Koa2介绍 1. Koa简介 2. Koa历史版本 3. 应用场景 4. Koa扩展框架 1. Koa简介 Koa 官网 Koa所谓的" ...
- Surf算法学习心得(一)——算法原理
Surf算法学习心得(一)--算法原理 写在前面的话: Surf算法是对Sift算法的一种改进,主要是在算法的执行效率上,比Sift算法来讲运行更快!由于我也是初学者,刚刚才开始研究这个算法,然而网上 ...
- [深度学习基础] 深度学习基础及数学原理
图像分类 (image classification) 问题是指, 假设给定一系列离散的类别(categories)(如猫, 狗, 飞机, 货车, ...), 对于给定的图像, 从这些类别中赋予一个作 ...
- 极简权限认证必须掌握【代码+原理+建议收藏】
这个极简权限认证必须掌握,代码不过百,但是很关键 小白最近没有来问学委问题,不过前几天,有朋友问到如何进行访问控制,资源控制的,学委特地写了一篇. 这其实就是权限认证,理解并掌握其核心思想很重要,而且 ...
最新文章
- Java相对路径读取文件
- php 转化数字,php实现中文转数字
- C99 calloc、malloc和realloc区别
- 关于MFC共享DLL的模块状态切换 .
- 在Spark中自定义Kryo序列化输入输出API(转)
- java hibernate 分页查询_4 Hibernate HQL查询,分页查询
- python中创建集合的语句_Python 集合(set) 介绍
- 安卓布局中xml文件属性和ID简介
- 阿里中间件开源组件:Sentinel 0.2.0正式发布
- 我的YUV播放器MFC小笔记:注册表读写
- c++中的构造函数和析构函数
- 倒计时_考研倒计时30天,拼了
- bzoj4754: [Jsoi2016]独特的树叶
- 使 VC2013 编写的程序运行在其它电脑上
- java面试项目介绍,详细说明
- windows实用工具集
- 数据库-SQL语句创建与修改
- 英语语法新思维初级分享
- python xgboost建模过程_python - Dask中的XGBoost建模 - SO中文参考 - www.soinside.com
- Scanner类、Random类、ArrayList 类