本文基于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()); //(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进阶学习笔记