共计 20674 个字符,预计需要花费 52 分钟才能阅读完成。
本文基于
koa 3.0.0-alpha.1
版本源码进行剖析
因为
koa
的源码量非常少,然而体现的思维十分经典和难以记忆,如果忽然要手写koa
代码,可能还不肯定能很快写进去,因而本文将集中于如何了解以及记忆koa
的代码本文一些代码块为了演示不便,可能有一些语法排列谬误,因而本文所有代码均能够视为伪代码
文章内容
- 从
0 到 1
推导koa 3.0.0-alpha.1
版本源码的实现,一步一步欠缺简化版koa
的手写逻辑 - 剖析罕用中间件
koa-router
的源码以及进行对应的手写 - 剖析罕用中间件
koa-bodyparser
的源码以及进行对应的手写
外围代码剖析 & 手写
2.1 koa-compose
const Koa = require('koa'); | |
const app = new Koa(); | |
app.use(async (ctx, next) => {console.log("中间件 1 start"); | |
await next(); | |
console.log("中间件 1 end"); | |
}); | |
app.use(async (ctx, next) => {console.log("中间件 2 start"); | |
await next(); | |
console.log("中间件 2 end"); | |
}); | |
app.use(async (ctx, next) => {console.log("中间件 3 start"); | |
await next(); | |
console.log("中间件 3 end"); | |
}); | |
app.listen(3000); |
下面代码块中间件运行流程如下所示
下面的运行流程看起来就跟咱们平时开发不太一样,咱们能够看一个类似的场景,比方上面
- 咱们在
fn1()
中执行一系列的业务逻辑 - 然而咱们在
fn1()
遇到了await fn2()
,因而咱们得期待fn2()
执行结束后能力持续前面的业务逻辑
function fn1() {console.log("fn1 执行业务逻辑 1"); | |
await fn2(); | |
console.log("fn1 执行业务逻辑 2") | |
} |
async function fn2() {console.log("fn2 执行业务逻辑 1"); | |
} |
咱们将 fn2
作为参数传入
async function fn2() {console.log("fn2 执行业务逻辑 1"); | |
} | |
function fn1(fn2) {console.log("fn1 执行业务逻辑 1"); | |
await fn2(); | |
console.log("fn1 执行业务逻辑 2") | |
} |
如果咱们有 fn3
、fn4
呢?
async function fn1(fn2) {console.log("fn1 执行业务逻辑 1"); | |
await fn2(); | |
console.log("fn1 执行业务逻辑 2") | |
} | |
async function fn2(fn3) {console.log("fn2 执行业务逻辑 1"); | |
await fn3(); | |
console.log("fn2 执行业务逻辑 2") | |
} | |
async function fn3(fn4) {console.log("fn3 执行业务逻辑 1"); | |
await fn4(); | |
console.log("fn3 执行业务逻辑 2") | |
} | |
async function fn4() {console.log("fn4 执行业务逻辑 1"); | |
console.log("fn4 执行业务逻辑 2") | |
} |
那如果咱们还有fn5
、fn6
…. 呢?
咱们应用怎么的逻辑进行这种 function 的嵌套?
咱们能够从下面代码发现,每一个 fnX()
传递的都是上一个fn(X+1)()
2.1.1 应用 middleware 遍历所有 fn
咱们能够先应用一个数组进行 fn
的增加
middleware.push(fn);
当咱们取出一个 fn
时,咱们应该传入下一个fn
,即
let fn = middleware[i]; | |
fn(middleware[i+1]); |
如果咱们想要程序传入context
let fn = middleware[i]; | |
fn(context, middleware[i+1]); |
应用 middleware
整合下面的逻辑,如上面所示
- 咱们应用
app.use((ctx, next))
传入的next()
须要强制返回一个Promise
,因为它能够应用await
,因而咱们应用Promise.resolve()
包裹fn()
返回的值,避免返回的不是Promise
- 在调用
fn()
的时候,会传入下一个中间件作为第二个参数:middleware[i + 1]
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
function dispatch(i) {let fn = middleware[i]; | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, middleware[i + 1])); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.1.2 链式调用
async function fn1(fn2) {console.log("fn1 执行业务逻辑 1"); | |
await fn2(); | |
console.log("fn1 执行业务逻辑 2") | |
} | |
async function fn2(fn3) {console.log("fn2 执行业务逻辑 1"); | |
await fn3(); | |
console.log("fn2 执行业务逻辑 2") | |
} | |
async function fn3() {console.log("fn3 执行业务逻辑 1"); | |
console.log("fn3 执行业务逻辑 2") | |
} |
咱们如何实现
fn1
->fn2
->fn3
的链式调用呢?
fn1(fn2(fn3))
回到咱们下面实现的 koa
源码
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
function dispatch(i) {let fn = middleware[i]; | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, middleware[i + 1])); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
如下面所示,咱们执行了 fn1(context, fn2)
,然而咱们fn2()
并没有传入 fn3
,这导致了链式调用被中断了,而且fn2()
也不肯定会返回Promise
,因而咱们须要对上面代码进行调整
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
function dispatch(i) {let fn = middleware[i]; | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch(i + 1))); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
fn(context, middleware[i + 1])
调整为 fn(context, dispatch(i + 1))
这样咱们就能够实现
fn2()
返回的是Promise.resolve()
,无论fn2()
返回什么,都是一个Promise
fn2(context, dispatch(i + 1))
的第二个参数传入了fn3
,并且fn3
是一个Promise
2.1.3 细节优化
2.1.3.1 app.use 返回 this
app.use()
返回本人自身,能够应用链式调用
let app = {use(fn) {middleware.push(fn); | |
return this; | |
} | |
} | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}).use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
console.log("fn2 执行业务逻辑 2") | |
}); |
2.1.3.2 dispatch()返回办法
dispatch(i + 1)
返回的是一个执行结束的 Promise
状态,不是一个办法,须要改成bind
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
function dispatch(i) {let fn = middleware[i]; | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.1.3.3 最初一个中间件返回空的 Promise.resolve
最初一个中间件调用 next()
时没有执行的办法,应该间接返回一个空的办法,比方下面代码中
console.log("fn2 执行业务逻辑 1")
await next()
: 此时的next()
应该是一个空的Promise
办法console.log("fn2 执行业务逻辑 2")
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
function dispatch(i) {let fn = middleware[i]; | |
if (i === middleware.length) {return Promise.resolve(); | |
} | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.1.3.4 阻止中间件中反复调用 next()
阻止一个中间件反复调用 next()
办法,应用 index
记录以后的 i
,如果发现i<=index
,阐明反复调用了某一个中间件的next()
办法
let middleware = []; | |
let context = {}; | |
let app = {use(fn) {middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return new Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {return Promise.resolve(); | |
} | |
// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} | |
return dispatch(0); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.1.3.5 欠缺错误处理逻辑
- 反复调用
next()
抛出谬误 - 执行
fn()
过程中出错
将 dispatch()
的外层再包裹一个新的 function()
,而后咱们就能够应用这个function()
进行对立的 then()
和catch()
解决,即上面代码中的
let fn = compose(this.middleware)
fn().then(() => {}).catch(err => {})
function compose(middleware) {// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject() | |
return function (context) { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {return Promise.resolve(); | |
} | |
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} catch (err) {return Promise.reject(err) | |
} | |
} | |
return dispatch(0); | |
} | |
} | |
let app = {middleware: [], | |
use(fn) {this.middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() {let fn = compose(this.middleware); | |
let context = {}; | |
fn(context).then(() => { | |
// 失常执行最终触发 | |
console.log("fn 执行结束!"); | |
}).catch(error => {console.error("fn 执行谬误", error); | |
}); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
运行下面代码,失去的后果为:
2.1.3.6 兼容 compose 传入 next()办法
compose()
返回的 function(context)
减少传入参数next
,能够在内部进行定义传入,而后判断
- 当
i 等于 middleware.length
时,middleware[i]
必定为空,判断最初一个next()
是否为空 - 如果最初一个
next()
不为空,则继续执行最初一次next()
- 如果最初一个
next()
为空,则间接返回空的Promise.resolve
,跟下面咱们解决i 等于 middleware.length
时的逻辑一样
function compose(middleware) {// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject() | |
return function (context, next) { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空 | |
// 如果不为空,则继续执行最初一次 | |
// 如果为空,则返回 Promise.resolve() | |
fn = next; | |
} | |
if (!fn) {return Promise.resolve(); | |
} | |
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} catch (err) {return Promise.reject(err) | |
} | |
} | |
return dispatch(0); | |
} | |
} | |
let app = {middleware: [], | |
use(fn) {this.middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() {let fn = compose(this.middleware); | |
let context = {}; | |
const next = function () {console.log("最初一个 next()!"); | |
} | |
fn(context, next).then(() => { | |
// 失常执行最终触发 | |
console.log("fn 执行结束!"); | |
}).catch(error => {console.error("fn 执行谬误", error); | |
}); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.1.3.7 解决 middleware 不为数组时谬误的抛出
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!') | |
} | |
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject() | |
return function (context, next) { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空 | |
// 如果不为空,则继续执行最初一次 | |
// 如果为空,则返回 Promise.resolve() | |
fn = next; | |
} | |
if (!next) {return Promise.resolve(); | |
} | |
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} catch (err) {return Promise.reject(err) | |
} | |
} | |
return dispatch(0); | |
} | |
} | |
let app = {middleware: [], | |
use(fn) {this.middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {this.callback(); | |
}, | |
callback() {let fn = compose(this.middleware); | |
let context = {}; | |
const next = function () {console.log("最初一个 next()!"); | |
} | |
fn(context, next).then(() => { | |
// 失常执行最终触发 | |
console.log("fn 执行结束!"); | |
}).catch(error => {console.error("fn 执行谬误", error); | |
}); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
至此,咱们曾经齐全实现了官网
koa-compose
的残缺代码!
2.2 Node.js 原生 http 模块
Koa
是基于中间件模式的 HTTP 服务框架,底层原理就是封装了 Node.js 的 http 原生模块
在下面实现
koa-compose
中间件的根底上,咱们减少 Node.js 的 http 原生模块,根本就是Koa
的外围代码的实现
2.2.1 原生代码示例
const http = require('http'); | |
const server = http.createServer((req, res)=> {res.end(`this page url = ${req.url}`); | |
}); | |
server.listen(3001, function() {console.log("the server is started at port 3001") | |
}) |
2.2.2 减少原生 http
模块的相干代码
欠缺 listen()
和callback()
的相干办法,减少原生 http
模块的相干代码
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!') | |
} | |
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject() | |
return function (context, next) { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空 | |
// 如果不为空,则继续执行最初一次 | |
// 如果为空,则返回 Promise.resolve() | |
fn = next; | |
} | |
if (!next) {return Promise.resolve(); | |
} | |
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} catch (err) {return Promise.reject(err) | |
} | |
} | |
return dispatch(0); | |
} | |
} | |
let app = {middleware: [], | |
use(fn) {this.middleware.push(fn); | |
return this; | |
}, | |
listen(...args) {const server = http.createServer(this.callback()); | |
return server.listen(...args); | |
}, | |
callback() {let fn = compose(this.middleware); | |
return (req, res) => {let context = {}; | |
this.handleRequest(context, fn); | |
} | |
}, | |
handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!"); | |
} | |
fn(context, next).then(() => { | |
// 失常执行最终触发 | |
console.log("fn 执行结束!"); | |
}).catch(error => {console.error("fn 执行谬误", error); | |
}); | |
} | |
}; | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2") | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2") | |
}); | |
app.listen(200); |
2.3 初始化 context
将 app={}
的模式欠缺为 class Koa
的模式,而后在构造函数中初始化 context
、request
、response
的初始化,在 callback()
进行 http.createServer()
回调函数 req
和res
的赋值
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; | |
} |
残缺代码如下所示
const context = require("./context.js"); | |
const request = require("./request.js"); | |
const response = require("./response.js"); | |
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!"); | |
} | |
// 返回也是一个 Promise,可能是 Promise.resolve(),也有可能是 Promise.reject() | |
return function (context, next) { | |
// 要求每一个 fn 返回都是一个 Promise | |
let index = -1; | |
function dispatch(i) {if (i <= index) {return Promise.reject(new Error("next()反复调用屡次")); | |
} | |
index = i; | |
let fn = middleware[i]; | |
if (i === middleware.length) {// middleware[i]必定为空,判断最初一个 next()是否为空 | |
// 如果不为空,则继续执行最初一次 | |
// 如果为空,则返回 Promise.resolve() | |
fn = next; | |
} | |
if (!next) {return Promise.resolve(); | |
} | |
try {// 可能返回只是一个一般的数据,因而须要应用 Promise.resolve()进行包裹返回一个 Promise 数据 | |
return Promise.resolve(fn(context, dispatch.bind(null, i + 1))); | |
} catch (err) {return Promise.reject(err); | |
} | |
} | |
return dispatch(0); | |
}; | |
} | |
class Koa {constructor() {this.middleware = []; | |
this.context = Object.create(context); | |
this.request = Object.create(request); | |
this.response = Object.create(response); | |
} | |
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; | |
} | |
use(fn) {this.middleware.push(fn); | |
return this; | |
} | |
listen(...args) {const server = http.createServer(this.callback()); | |
return server.listen(...args); | |
} | |
callback() {let fn = compose(this.middleware); | |
return (req, res) => {let context = this.createContext(req, res); | |
this.handleRequest(context, fn); | |
}; | |
} | |
handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!"); | |
}; | |
fn(context, next) | |
.then(() => { | |
// 失常执行最终触发 | |
console.log("fn 执行结束!"); | |
}) | |
.catch((error) => {console.error("fn 执行谬误", error); | |
}); | |
} | |
} | |
const app = new Koa(); | |
app.use(async (ctx, next) => {console.log("fn1 执行业务逻辑 1"); | |
await next(); | |
await next(); | |
console.log("fn1 执行业务逻辑 2"); | |
}); | |
app.use(async (ctx, next) => {console.log("fn2 执行业务逻辑 1"); | |
await next(); | |
console.log("fn2 执行业务逻辑 2"); | |
}); | |
app.listen(200); |
2.4 欠缺响应数据的逻辑
由下面初始化 context 的代码能够晓得,咱们曾经将 http
原生模块的 req
和res
都放入到 context
中,因而咱们在执行结束中间件后,咱们应该对 context.res
进行解决,返回对应的值
handleRequest(context, fn) {const next = function () {console.log("最初一个 next()!"); | |
}; | |
const handleResponse = () => {return this.handleResponse(context); | |
}; | |
fn(context, next) | |
.then(handleResponse) | |
.catch((error) => {console.error("fn 执行谬误", error); | |
}); | |
} | |
handleResponse(ctx) { | |
const res = ctx.res; | |
let body = ctx.body; | |
if (!body) {return res.end(); | |
} | |
if (typeof body !== "string") {body = JSON.stringify(body); | |
} | |
res.end(body); | |
} |
至此,咱们实现了一个简化版本的
Koa
,残缺代码放在 github mini-koa
常见中间件剖析 & 手写
3.1 koa-router
3.1.1 不应用 koa-router
在不应用 koa-router
中间件时,咱们须要手动依据 ctx.request.url
去判断路由,如上面代码所示
const Koa = require("koa"); | |
const fs = require("fs"); | |
const app = new Koa(); | |
function readFile(path) {return new Promise((resolve, reject) => {let htmlUrl = `../front/${path}`; | |
fs.readFile(htmlUrl, "utf-8", (err, data) => {if (err) {reject(err); | |
} else {resolve(data); | |
} | |
}); | |
}); | |
} | |
async function parseUrl(url) { | |
let base = "404.html"; | |
switch (url) { | |
case "/": | |
base = "index.html"; | |
break; | |
case "/login.html": | |
base = "login.html"; | |
break; | |
case "/home.html": | |
base = "home.html"; | |
break; | |
} | |
// 从本地读取出该门路下 html 文件的内容,而后返回给客户端 | |
const data = await readFile(base); | |
return data; | |
} | |
app.use(async (ctx) => { | |
let url = ctx.request.url; | |
// 判断这个 url 是哪一个申请 | |
const htmlContent = await parseUrl(url); | |
ctx.status = 200; | |
ctx.body = htmlContent; | |
}); | |
app.listen(3000); | |
console.log("[demo] route is starting at port 3000"); |
因而咱们手写 koa-router
时,咱们须要关注几个问题:
- 依据
ctx.path
判断是否合乎注册的路由,如果合乎,则触发注册的办法 -
咱们须要依据
path
、methods
进行对应数据结构的构建3.1.2 应用 koa-router 的具体示例
const app = new Koa(); | |
const router = new Router(); | |
router.get("/", (ctx, next) => {// ctx.router available}); | |
router.get("/home", (ctx, next) => {// ctx.router available}); | |
app.use(router.routes()); |
3.1.3 依据示例实现 koa-router
依据 methods
初始化所有办法,造成 this["get"]
、this["put"]
的数据结构,提供给内部调用注册路由
当有新的申请产生时,会触发中间件的逻辑执行,会依据目前 ctx.path
和ctx.method
去寻找之前是否有注册过的门路,如果有则触发注册门路的 callback
进行逻辑的执行
function Router(opts) {this.register = function (path, methods, callback, opts) { | |
this.stack.push({ | |
path, | |
methods, | |
middleware: callback, | |
opts, | |
}); | |
return this; | |
}; | |
this.routes = function () { | |
// 返回所有注册的路由 | |
return async (ctx, next) => {// 每次执行中间件时,判断是否有合乎 register()的路由 | |
const path = ctx.path; | |
const method = ctx.method.toUpperCase(); | |
let callback; | |
for (const item of this.stack) {if (path === item.path && item.methods.indexOf(method) >= 0) { | |
// 找到对应的路由 | |
callback = item.middleware; | |
break; | |
} | |
} | |
if (callback) {callback(ctx, next); | |
return; | |
} | |
await next();}; | |
}; | |
this.opts = opts || {}; | |
this.methods = this.opts.methods || ["HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE"]; | |
this.stack = []; | |
// 依据 methods 初始化所有办法,造成 this["get"]、this["put"]的数据结构 | |
for (const _method of this.methods) {this[_method.toLowerCase()] = this[_method] = function (path, callback) {this.register(path, [_method], callback); | |
}; | |
} | |
} |
3.2 koa-bodyparser
该中间件能够简化申请体的解析流程
当咱们不应用
koa-bodyparser
时,如上面所示
3.2.1 不应用 koa-bodyparser
GET 申请
query
是格式化好的参数对象,比方query={a:1, b:2}
querystring
是申请字符串,比方querystring="a=1&b=2"
let request = ctx.request; | |
let query = request.query; | |
let queryString = request.querystring; | |
// 也能够间接省略 request,const {query, querystring} = request |
POST 申请
没有封装具体的办法,须要手动解析 ctx.req
这个原生的 node.js 对象
如上面例子所示,ctx.req
获取到 formData
为"userName=22&nickName=22323&email=32323"
咱们须要将 formData
解析为{userName: 22, nickName: 22323, email: 32323}
home.post("b", async (ctx) => {const body = await parseRequestPostData(ctx); | |
ctx.body = body; | |
}); | |
async function parseRequestPostData(ctx) {return new Promise((resolve, reject) => { | |
const req = ctx.req; | |
let postData = ""; | |
req.addListener("data", (data) => {postData = postData + data;}); | |
req.addListener("end", () => {if (postData) {let parseData = transStringToObject(postData); | |
resolve(parseData); | |
} else {resolve("没有数据"); | |
} | |
}); | |
}); | |
} | |
async function transStringToObject(data) {let result = {}; | |
let dataList = data.split("&"); | |
for (let [index, queryString] of dataList.entries()) {let itemList = queryString.split("="); | |
result[itemList[0]] = itemList[1]; | |
} | |
return result; | |
} |
3.2.2 应用 koa-bodyparser 的具体示例
const Koa = require("koa"); | |
const fs = require("fs"); | |
const app = new Koa(); | |
const Router = require("koa-router"); | |
const bodyParser = require("koa-bodyparser"); | |
// post 申请参数解析示例 | |
home.get("form", async (ctx) => { | |
let html = ` | |
<h1>koa2 request post demo</h1> | |
<form method="POST" action="/b"> | |
<p>userName</p> | |
<input name="userName" /><br/> | |
<p>nickName</p> | |
<input name="nickName" /><br/> | |
<p>email</p> | |
<input name="email" /><br/> | |
<button type="submit">submit</button> | |
</form> | |
`; | |
ctx.body = html; | |
}); | |
home.post("b", async (ctx) => { | |
// 一般解析逻辑 | |
// const body = await parseRequestPostData(ctx); | |
// ctx.body = body; | |
// 应用 koa-bodyparser 会主动解析表单的数据而后放在 ctx.request.body 中 | |
let postData = ctx.request.body; | |
ctx.body = postData; | |
}); | |
let router = new Router(); | |
router.use("/", home.routes()); //http://localhost:3000 | |
app.use(bodyParser()); // 这个中间件的注册应该放在 router 之前!app.use(router.routes()); | |
app.listen(3002); |
3.2.3 依据示例实现 koa-bodyparser
当 ctx.method
是POST
申请时,主动解析ctx.request.body
,次要分为:
form
类型json
类型text
类型
依据不同的类型调用不同的解析办法,而后赋值给ctx.request.body
/** | |
* 注册对应的监听办法,进行 request 流数据的读取 | |
* @param req | |
*/ | |
function readStreamBody(req) {return new Promise((resolve, reject) => { | |
let postData = ""; | |
req.addListener("data", (data) => {postData = postData + data;}); | |
req.addListener("end", () => {if (postData) {resolve(postData); | |
} else {resolve("没有数据"); | |
} | |
}); | |
}); | |
} | |
async function parseQuery(data) {let result = {}; | |
let dataList = data.split("&"); | |
for (let [index, queryString] of dataList.entries()) {let itemList = queryString.split("="); | |
result[itemList[0]] = itemList[1]; | |
} | |
return result; | |
} | |
async function parseJSON(ctx, data) {let result = {}; | |
try {result = JSON.parse(data); | |
} catch (e) {ctx.throw(500, e); | |
} | |
return result; | |
} | |
function bodyParser() {return async (ctx, next) => {if (!ctx.request.body && ctx.method === "POST") {let body = await readStreamBody(ctx.request.req); | |
// With Content-Type: text/html; charset=utf-8 | |
// this.is('html'); // => 'html' | |
// this.is('text/html'); // => 'text/html' | |
// this.is('text/', 'application/json'); // => 'text/html' | |
// | |
// When Content-Type is application/json | |
// this.is('json', 'urlencoded'); // => 'json' | |
// this.is('application/json'); // => 'application/json' | |
// this.is('html', 'application/'); // => 'application/json' | |
// | |
// this.is('html'); // => false | |
let result; | |
if (ctx.request.is("application/x-www-form-urlencoded")) {result = await parseQuery(body); | |
} else if (ctx.request.is("application/json")) {result = await parseJSON(ctx, body); | |
} else if (ctx.request.is("text/plain")) {result = body;} | |
ctx.request.body = result; | |
} | |
await next();}; | |
} | |
module.exports = bodyParser; |
参考
- Koa.js 设计模式 - 学习笔记
- Koa2 进阶学习笔记