本文基于koa 3.0.0-alpha.1版本源码进行剖析

因为koa的源码量非常少,然而体现的思维十分经典和难以记忆,如果忽然要手写koa代码,可能还不肯定能很快写进去,因而本文将集中于如何了解以及记忆koa的代码

本文一些代码块为了演示不便,可能有一些语法排列谬误,因而本文所有代码均能够视为伪代码

文章内容

  1. 0到1推导koa 3.0.0-alpha.1版本源码的实现,一步一步欠缺简化版koa的手写逻辑
  2. 剖析罕用中间件koa-router的源码以及进行对应的手写
  3. 剖析罕用中间件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")}

如果咱们有fn3fn4呢?

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")}

那如果咱们还有fn5fn6....呢?

咱们应用怎么的逻辑进行这种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的模式,而后在构造函数中初始化contextrequestresponse的初始化,在callback()进行http.createServer()回调函数reqres的赋值

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原生模块的reqres都放入到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判断是否合乎注册的路由,如果合乎,则触发注册的办法
  • 咱们须要依据pathmethods进行对应数据结构的构建

    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.pathctx.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申请

  1. query是格式化好的参数对象,比方query={a:1, b:2}
  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.methodPOST申请时,主动解析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;

参考

  1. Koa.js 设计模式-学习笔记
  2. Koa2进阶学习笔记