通过模拟一个极简版本的 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 = 200
ctx.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 中可以获取 Request
const request = context.request = Object.create(this.request)
// 在 context 中可以获取 Response
const response = context.response = Object.create(this.response)
context.app = request.app = response.app = this
// 原生 Node 请求/响应对象
context.req = request.req = response.req = req
context.res = request.res = response.res = res
// 在 Request 和 Respon 中也可以获取 context 上下文对象
request.ctx = response.ctx = context
// Requset 中也可以获取 Response
request.response = response
// Response 中也可以获取 Requset
response.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').promises
const 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)
// 调用函数处理 body
respond(context)
}).catch(err => {
res.end(err.message)
})
}
return handleRequest
}
}
function respond(ctx) {
const body = ctx.body
const 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 = 204
res.end()
}
module.exports = Application