本文基於koa 3.0.0-alpha.1版本源碼進行分析
由於
koa的源碼量非常少,但是體現的思想非常經典和難以記憶,如果突然要手寫koa代碼,可能還不一定能很快寫出來,因此本文將集中於如何理解以及記憶koa的代碼本文一些代碼塊為了演示方便,可能有一些語法排列錯誤,因此本文所有代碼均可以視為偽代碼
1. 文章內容
- 從
0到1推導koa 3.0.0-alpha.1版本源碼的實現,一步一步完善簡化版koa的手寫邏輯 - 分析常用中間件
koa-router的源碼以及進行對應的手寫 - 分析常用中間件
koa-bodyparser的源碼以及進行對應的手寫
2. 核心代碼分析&手寫
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()返回什麼,都是一個Promisefn2(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. 常見中間件分析&手寫
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進階學習筆記