Koa

142 阅读8分钟

koa

  • 作用:将原始http模块进行封装,简化流程
  • 相比express的优势:基于promise,而不是基于回调,可以在实例.onerror做异常统一处理

拆解

  • 模块导入的是一个类,需要new出一个实例
  • 实例是一个封装过的http服务实例,可以listen
  • 实例上有use方法,核心api
  • 那可以多次new,所以要保证数据的私有性

use

  • 传入一个函数
  • 函数接收两个参数,ctx与next
  • use的特性:可以多次调用,函数并不会马上执行,而是等前一个next调用后才执行
  • 内部是将参数封装成promise,等待外部resolve就执行end
    • 所以传递函数写成async与await最好,后面会阐述原因

ctx

  • 这个koa将http默认的req与res集成进来,并且内部封装了response与request,且在ctx上自定义了一些方法
  • req、res、response、request互相引用
body
  • 常用的一个操作:通过ctx.body进行写入内容
  • ctx.body可以写多次,以最后一次为准,koa会自己返回
  • 也可以写成流的形式,buffer、string也可以,json会在返回前自动stringify一层
  • 可以写多次,证明了一件事情,它并不是直接end的,因为多次end会报错
    • 后面实现
//浏览器访问127.0.0.1:3000返回ok1
const Koa = require("koa");
const app = new Koa();

app.use(function (ctx, next) {
  ctx.body = "ok";
  ctx.body = "ok1";
});

app.listen(3000, () => {
  console.log("3000");
});

next

  • 下一个实例
  • 递归操作,这个就是koa洋葱模型的由来[1[3[5,6]4]2]一层包一层
//不使用next,最终结果1,2 会发现3456没触发,页面展示的也是
app.use(function (ctx, next) {
  console.log(1);
  ctx.body = "ok";
  console.log(2);
});
app.use(function (ctx, next) {
  console.log(3);
  ctx.body = "ok1";
  console.log(4);
});
app.use(function (ctx, next) {
  console.log(5);
  ctx.body = "ok2";
  console.log(6);
});
  • 让next触发,会按先后顺序执行
//在每个ctx.body下面加个next
//打印结果135642 返回结果ok2
app.use(function (ctx, next) {
  console.log(1);
  ctx.body = "ok";
  next();
  console.log(2);
});
app.use(function (ctx, next) {
  console.log(3);
  ctx.body = "ok1";
  next();
  console.log(4);
});
app.use(function (ctx, next) {
  console.log(5);
  ctx.body = "ok2";
  console.log(6);
});
  • 解释:next代表的是它下一个函数,所以可以按照下面例子理解
function 1(ctx, next) {
  console.log(1);
  ctx.body = "ok";
  //next();
  function 2(ctx, next) {
    console.log(3);
    ctx.body = "ok1";
    // next();
    function 3(ctx, next) {
      console.log(5);
      ctx.body = "ok2";
      console.log(6);
    }
    console.log(4);
  }
  console.log(2);
}
  • 让2延迟一下执行
//134256 结果:ok
app.use(function (ctx, next) {
  console.log(1);
  ctx.body = "ok";
  next();
  console.log(2);
});
app.use(function (ctx, next) {
  console.log(3);
  //延迟一秒执行
  setTimeout(() => {
    ctx.body = "ok1";
    next();
  }, 1000);
  console.log(4);
});
app.use(function (ctx, next) {
  console.log(5);
  ctx.body = "ok2";
  console.log(6);
});
  • 解释:跟我们正常的开发流程一样,函数自上而下执行,先走同步,再走异步
  • 第一次走了同步,然后后面异步了,但并没有等待,同步代码执行完,直接返回了,所以返回值为ok
  • 所以koa推荐使用async与await操作,在上一个await回来之前,进入等待状态
  • 常见失误操作

//想着只有第二次是异步的,那么只给第二次加了一个async
//但这里需要注意,在执行到await的时候,把后面的流程放到微任务里了,然后回过头执行同步的2,然后结束了
//132564 返回ok
const sleep = (time, fn) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      fn();
      resolve();
    }, time);
  });
};
app.use(async function (ctx, next) {
  console.log(3);
  await sleep(1000, () => {
    ctx.body = "ok1";
    next();
  });
  console.log(4);
});
  • 正确做法
  • 就让它一步一步执行,最后一次加上next,如果没有就不执行,不影响的,这样也可以防止后续其他操作忘记在前面加next导致的程序卡壳,还得排查原因,麻烦
  • 而且不确定他是不是异步代码,防止意外,直接await
app.use(async function (ctx, next) {
  console.log(1);
  ctx.body = "ok";
  await next();
  console.log(2);
});
app.use(async function (ctx, next) {
  console.log(3);
  await sleep(1000, async () => {
    ctx.body = "ok1";
    await next();
  });
  console.log(4);
});
app.use(async function (ctx, next) {
  console.log(5);
  ctx.body = "ok2";
  await next();
  console.log(6);
});

源码分析

  • package.json中可以看到main指向lib下的applications.js
  • 进入lib文件夹下可以看到context、request、response、applications这四个js文件,这就是核心代码了
  • applications返回实例,内部引用context、request、response这三个状态,并且通过Object.create多次处理,防止多个实例与多次请求引用地址之间互相干扰,采用__proto_-的方式连接
  • 监听操作采用旧版方法: __defineGetter__、与__defineSetter__
  • 异常操作:用户可能传递的是非promise函数,所以会直接报错,那么使用try包一层
// 异常捕获示例
const Koa = require("koa");

const app = new Koa();

// 用户不一定用async
app.use(async function (ctx, next) {
  throw Error(11);
  console.log(ctx.body);
  ctx.body = "1";
  ctx.body = "2";
});
app.on("error", (err) => {
  console.log("--------", err);
});
app.listen(3000, () => {
  console.log("3000");
});
//异常示例2
//不允许调用两次next
app.use(async function (ctx, next) {
  next();
  next();
});
app.on("error", (err) => {
  console.log("--------", err);
});
app.listen(3000, () => {
  console.log("3000");
});

application.js

const context = require("./context");
const request = require("./request");
const response = require("./response");
const EventEmitter = require("events");
const Stream = require("stream");

const http = require("http");
class Application extends EventEmitter {
  constructor() {
    super();
    //如果new了多次,那么空间会污染,所以初始化的时候拷贝一份,让新对象的__porto__等于内容,找的时候就会通过原型链进行查找
    this.context = Object.create(context); //this.context.__proto__=context;
    this.request = Object.create(request);
    this.response = Object.create(response);
    this.middleware = [];
  }
  use(fn) {
    this.middleware.push(fn);
  }
  createContext(req, res) {
    //隔离请求,每次请求都是一次新的
    const ctx = Object.create(this.context);
    const request = Object.create(this.request);
    const response = Object.create(this.response);

    ctx.req = req; //原始
    ctx.res = res;
    ctx.request = request; //封装
    ctx.response = response;

    //再来一层
    ctx.request.req = ctx.response.req = req;
    ctx.request.res = ctx.response.res = res;

    ctx.app = this;
    ctx.context = ctx;
    return ctx;
  }
  compose(ctx) {
    let middleware = this.middleware;
    let p = -1;
    const dispatch = (i) => {
      //防止重复调用,重复调用的话,还是拿着旧i,那么小于等于p了
      if (i <= p) {
        throw Error("next() called multiple times");
      }
      p = i;
      //没中间件或最后一个了,直接成功
      if (i === middleware.length) return Promise.resolve();
      let fn = middleware[i];
      //返回promise,把下一个传给函数,执不执行看自己了
      try {
        //可能用户提供的不是promise,那么会直接报错,所以保险起见使用try包一层
        return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    };
    return dispatch(0);
  }
  handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    res.statusCode = 404;
    this.compose(ctx)
      .then(() => {
        //执行完
        if (typeof ctx.body === "string" || Buffer.isBuffer(ctx.body)) {
          res.statusCode = 200;
          res.end(ctx.body);
        } else if(ctx.body instanceof Stream) {
           ctx.body.pipe(res);
        } else {
          res.end("Not Found");
        }
      })
      .catch((err) => {
        this.emit("error", err);
        res.statusCode = 500;
        res.end("Internal Server Error");
      });
  };
  listen(...args) {
    const server = http.createServer(this.handleRequest);
    server.listen(...args);
  }
}

module.exports = Application;

context.js

const context = {};

//数据代理
function defineGetter(target, key) {
  context.__defineGetter__(key, function () {
    return this[target][key];
  });
}

function defineSetter(target, key) {
  context.__defineSetter__(key, function (value) {
    this[target][key] = value;
  });
}
defineGetter("request", "path");
defineGetter("request", "query");
defineGetter("request", "header");
defineGetter("request", "headers");

defineGetter("response", "body");
defineSetter("response", "body");

module.exports = context;

request.js

const url = require("url");
const request = {
  get path() {
    return url.parse(this.req.url).pathname;
  },
  get query() {
    return url.parse(this.req.url, true).query;
  },
};


module.exports = request;

response.js

const response = {
  set body(value) {
    this._body = value;
  },
  get body() {
    return this._body;
  },
};

module.exports = response;

中间件

  • 中间件就是将我们的代码进行解耦,通过next方法连接,本质还是对use的应用
  • 中间件推荐封装成一个函数,return出一个promise
    • 函数是为了可以根据一些需求进行传参,灵活

封装一个获取post请求体的中间件

  • post的请求要通过事件监听去拿到,那么多个post就要处理多次,我们就可以将它统一处理
//不处理
const Koa = require("koa");
const app = new Koa();
app.use((ctx, next) => {
  if (ctx.path === "/login" && ctx.method === "GET") {
    ctx.type = "text/html;charset=utf-8;";
    ctx.body = `
    <form action="/login" method="post" enctype="application/x-www-form-urlencoded">
      <input type="text" name="username">
      <input type="password" name="password">
      <button>提交</button>
  </form>
    `;
  } else {
    //不是get就往下走,中间件的作用体现在这里,逻辑分开处理,实现解耦
    //这里就不用await了,因为当前作用域下没有要执行的代码了,return的是个promise,那么会自动等待
    return next();
  }
});

app.use(async (ctx, next) => {
  if (ctx.path === "/login" && ctx.method === "POST") {
    let arr = [];
    //让它一步一步执行,否则就koa拿不到body
    ctx.body = await new Promise((resolve, reject) => {
      ctx.req.on("data", (chunk) => {
        arr.push(chunk);
      });
      ctx.req.on("end", () => {
        resolve(Buffer.concat(arr).toString());
      });
      ctx.req.on("error", (err) => {
        reject(err);
      });
    });
  }
});
app.listen(3000, () => {
  console.log("server start 3000");
});

  • 处理后
    • 公有逻辑抽离
const bodyparser = (params) => {
  return async (ctx, next) => {
    if (ctx.method === "POST") {
      let arr = [];
      //放request上
      ctx.request.body = await new Promise((resolve, reject) => {
        ctx.req.on("data", (chunk) => {
          arr.push(chunk);
        });
        ctx.req.on("end", () => {
          //这块的类型转换就可以根据params参数自行处理了
          resolve(Buffer.concat(arr).toString());
        });
        ctx.req.on("error", (err) => {
          reject(err);
        });
      });
    }
    return next();
  };
};
//调用中间件
app.use(bodyparser());

app.use(async (ctx, next) => {
  if (ctx.path === "/login" && ctx.method === "POST") {
    ctx.body = ctx.request.body;
  }
});
//多个post也直接取ctx.request.body,不用每次都处理了
  • 扩展一下,细分一下对类型的处理
const uuid = require("uuid");//第三方库,需下载
/**
 * 拓展split,buffer没有这个方法,可以基于slice实现一个
 * @param {*} bodunary 分隔符
 * @returns
 */
Buffer.prototype.split = function (bodunary) {
  let arr = [];
  let offset = 0; //偏移
  let curPosition = 0; //当前所在位置
  //循环去遍历bodunary,每次遍历后以上次的结果为本次的查询起始位置,知道找不到为止
  while (-1 !== (curPosition = this.indexOf(bodunary, offset))) {
    arr.push(this.slice(offset, curPosition)); //分隔,然后放到arr中
    offset = curPosition + bodunary.length;
  }
  arr.push(this.slice(offset));
  return arr;
};

const bodyparser = (params) => {
  return async (ctx, next) => {
    if (ctx.method === "POST") {
      let arr = [];
      //让它一步一步执行,否则就koa拿不到body
      ctx.request.body = await new Promise((resolve, reject) => {
        ctx.req.on("data", (chunk) => {
          arr.push(chunk);
        });
        ctx.req.on("end", () => {
          let body = Buffer.concat(arr);
          const contentType = ctx.get("Content-Type");
          if (contentType === "application/x-www-form-urlencoded") {
            //格式化成{username:'',password:''}
            resolve(querystring.parse(body.toString()));
          } else if (contentType === "application/json") {
            //转json
            resolve(body.toJSON());
          } else {
            /**
             * form-data格式
             * 请求类型:form-data会带上一个boundary=----标识,后面的内容以------标识+内容+空行(/r/n)分隔
             * 头上的是四个--,请求体上是六个,我们给它补俩,然后分隔内容
             * 最后有俩--标识结束了
               multipart/form-data; boundary=----WebKitFormBoundarybEBulg8S1tCUp4UT
             * 内容:
              ------WebKitFormBoundaryrJ1HO5TnsMyoKzCY
              Content-Disposition: form-data; name="username"

              12
              ------WebKitFormBoundaryrJ1HO5TnsMyoKzCY
              Content-Disposition: form-data; name="password"

              21
              ------WebKitFormBoundaryrJ1HO5TnsMyoKzCY--

             */
            if (contentType.includes("multipart/form-data")) {
              const boundary = "--" + contentType.split("=")[1];
              //第一个和最后一个不要,没用
              let lines = body.split(boundary).slice(1, -1);
              body = {};
              lines.forEach((lineBuffer) => {
                //head
                //  Content-Disposition: form-data; name="username"
                //  Content-Disposition: form-data; name="password"
                //  Content-Disposition: form-data; filename="xxx"

                let [head, content] = lineBuffer.split("\r\n\r\n");
                let key = head.toString().match(/name="(.+?)"/)[1];

                if (head.includes("filename")) {
                  //创建一个id
                  let filename = uuid.v4();
                  //减去头,减去尾
                  let content = lineBuffer.slice(head.length + 4, -2);
                  fs.writeFile(path.join(params, filename), content);
                  body[key] = {
                    originName: head.toString().match(/filename="(.+?)"/)[1], //原始名称
                    filename: path.join(params, filename),
                    size: content.length,
                  };
                  //如果是文件上传
                } else {
                  body[key] = content.toString().replace("\r\n", "");
                }
              });
              resolve(body);
            }
          }
        });
        ctx.req.on("error", (err) => {
          reject(err);
        });
      });
    }
    return next();
  };
};
app.use(bodyparser(path.join(__dirname, "upload")));
  • 测试文件改为这个,增加了两个提交
  <form action="/login" method="post" enctype="application/x-www-form-urlencoded">
      <input type="text" name="username">
      <input type="password" name="password">
      <button>提交</button>
  </form>
  修改为form-data格式 
  <form action="/login" method="post" enctype="multipart/form-data">
    <input type="text" name="username">
    <input type="password" name="password">
    <input type="file" name="file" id="">
    <button>提交</button>
  </form>
  文件上传
  <form action="/login" method="post" enctype="multipart/form-data">
    <input type="file" name="file" id="">
    <button>提交</button>
  </form>

路由中间件

  • @koa/router第三方库
  • 它可以让路由简化,不用去单独判断请求类型了
const Router = require("@koa/router");
const router = new Router();

app.use(router.routes());

router.post("/login", async (ctx, next) => {
  ctx.body = ctx.request.body;
  //后续想继续执行的话就加next,否则不加
});

实现一下

class Router {
  constructor() {
    this.stack = [];
  }
  compose(stack, ctx, next) {
    function dispath(i) {
      if (i === stack.length) return next();
      let { callback } = stack[i];
      return Promise.resolve(callback(ctx, dispath(i + 1)));
    }
    return dispath(0);
  }
  routes() {
    return async (ctx, next) => {
      let requestPath = ctx.path;
      let requestMethod = ctx.method.toLowerCase();
      const stack = this.stack.filter((layer) => {
        return (layer.path = requestPath && layer.method === requestMethod);
      });
      return this.compose(stack, ctx, next);
    };
  }
}
class Layer {
  constructor(path, method, callback) {
    this.path = path;
    this.method = method;
    this.callback = callback;
  }
}
["get", "post", "delete"].forEach((method) => {
  Router.prototype[method] = function (path, callback) {
    this.stack.push(new Layer(path, method, callback));
  };
});

静态资源托管

  • koa-static第三方库
  • 传一个地址,将对应目录文件托管
  • 可使用多次
//httpserver里实现过,思路一致,不重复写了
const server = require("koa-static");

app.use(server(path.join(__dirname, "/upload")));