KOA源码学习

1. 简介

Koa是基于Node.js的web开发框架(有人说是框架的框架),特点是轻量可扩展性强并且使用async处理异步让代码更加清晰且错误处理更加容易。

优点

  • 拥抱async、await完美解决回掉地域
  • 使用中间件完成对http请求处理,代码逻辑清晰
  • 包装context对象代理req、res使开发更加简洁
  • 错误处理简单

缺点

  • koa本身只包含http服务,企业级应用需自己扩展
  • koa社区较小
KOA源码学习
功能概览

2. 启动服务

原生node

const http = require("http");

const server = http.createServer((req, res) => {
  res.end("hello world");
});

server.listen(3000, () => {
  console.log("server is running in localhost:3000");
});

KOA

const koa = require("koa");

const app = new koa();

app.use(async (ctx, next) => {
  ctx.body = "hello world";
});

app.listen(3000);

application.js

// 继承自Emitter,可方便使用发布订阅
class Application extends Emitter {
  constructor(options) {
    super();
    options = options || {};
    // 中间件初始化
    this.middleware = [];
    //  三个重要对象
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }
  // 1. 处理所有中间件并返回一个嵌套函数
  // 2. 基于req和res封装ctx对象
  // 3. 将ctx作为参数传递给中间件所组合成的一个嵌套函数
  // 4. 嵌套函数执行完向res中写入数据
  callback() {
    const fn = compose(this.middleware);
    if (!this.listenerCount("error")) this.on("error", this.onerror);
    const handleRequest = (req, res) => {
      // 创建context对象
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  // listen方法封装http.createServer方法
  listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

3. context

Context 对象,表示一次会话的上下文(req和res),每一次请求都会创建一个context,为了方便开发,context上很多方法都是对request和response的简单映射,

application.js

createContext(req, res) {
  const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
     //  request、response为Koa扩展的对象
    const response = context.response = Object.create(this.response);
    //  res、req为NodeJS原生对象,为了后面使用原生res、req的属性、方法
    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 = {};
    // 构建完成的context上下文对象
    return context;
}

获取request、response上的属性、方法

ctx.request.header

为了使用方便,koa将request和response的方法、属性代理到ctx上

const delegate = require('delegates');
delegate(proto, 'response')
  .method('attachment') // 代理方法
  .access('status') // 代理getter和setter
  .getter('writable'); // 代理getter
       ...

4. 中间件

KOA源码学习
洋葱模型

原生node

const http = require("http");

const server = http.createServer((req, res) => {
  console.log(req.url);
  const { url } = req;
  res.setHeader("Content-type", "text/html");
  if (url === "/home") {
    res.end("I am home");
  } else {
    res.end("I am other");
  }
});

server.listen(3000, () => {
  console.log("server is running in localhost:3000");
});

问题:业务逻辑复杂后,代码不清晰,

KOA

const koa = require("koa");
const app = new koa();

const mid1 = async (ctx, next) => {
  ctx.type = "text/html;";
  await next();
  ctx.body = ctx.body + " 中间件1";
};

const mid2 = async (ctx, next) => {
  ctx.body = "hello";
  await next();
  ctx.body = ctx.body + " 中间件2";
};

app.use(mid1);
app.use(mid2);

app.listen(3000);

//输出 hello 中间件2 中间件1

  • 运行到await next()的时候就会暂停当前程序,进入下一个中间件

application.js

  use(fn) {
    // 将中间件添加到this.middleware中
    this.middleware.push(fn);
    return this;
  }

koa-compose

function compose (middleware) {
  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) <span style="color:#2228f8">return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
// 这样就可能更好理解了。
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
   return Promise.resolve(
     fn1(context, function next(){
       return Promise.resolve(
         fn2(context, function next(){
             return Promise.resolve(
                 fn3(context, function next(){
                   return Promise.resolve();
                 })
             )
         })
       )
   })
 );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);  
                   

koa-compose返回的是一个Promise,Promise中取出第一个中间件并传入context和第一个next函数来执行。
第二个next函数里也是返回的是一个Promise,Promise中取出第二个中间件传入context和第二个next函数来执行。
第三个…
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。 这样就把所有中间件串联起来了。
koa中间件非常方便的实现后置处理逻辑

5. 错误处理

默认错误处理
application.js

// koa 会挂载一个默认的错误处理
if (!this.listenerCount("error")) this.on("error", this.onerror);

全局错误
application.js

  one rror(err) {
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 === err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error(`\n${msg.replace(/^/gm, '  ')}\n`);
  }

application.js

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const one rror = (err) => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, one rror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

每次http请求错误交给ctx.onerror处理
context.js

onerror(err) {
    const isNativeError =
      Object.prototype.toString.call(err) === '[object Error]' ||
      err instanceof Error;
    if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

    let headerSent = false;
    if (this.headerSent || !this.writable) {
      headerSent = err.headerSent = true;
    }

    this.app.emit('error', err, this);

    if (headerSent) {
      return;
    }

    const { res } = this;
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    this.set(err.headers);

    this.type = 'text';

    let statusCode = err.status || err.statusCode;

    if ('ENOENT' === err.code) statusCode = 404;
    if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

    // respond
    const code = statuses[statusCode];
    const msg = err.expose ? err.message : code;
    this.status = err.status = statusCode;
    this.length = Buffer.byteLength(msg);
    res.end(msg);
  }
上一篇:egg和koa洋葱模型


下一篇:如何解决异步接口请求快慢不均导致的数据错误问题? - DevUI