Koa 的实现原理

143 阅读4分钟

koa 是一个web框架,通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理,使用起来非常方便

下面模拟实现一个精简版的Koa

初始化

初始化 Koa 对象

const Emitter = require("events");

class Koa extends Emitter {
  constructor() {
    super();
    this.middlewares = [];
  }
}

初始化 context

const context = {
  _body: null,
  req: null,
  res: null,
  get body() {
    return this._body
  },
  set body(val) {
    if (typeof val !== 'string') {
      val = JSON.stringify(val)
    }
    this._body = val
    this.res.end(this._body)
  }
  ...
}
class Koa extends Emitter {
  constructor() {
    ...
    this.context = Object.create(context)
  }
}

中间键

class Koa extends Emitter {
  constructor() {
    ...
    this.middlewares = []
  }
  use(middleware) {
    ...
    this.middlewares.push(middleware)
  }
}

启动 http 服务

class Koa extends Emitter {
  callback(req, res) {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/plain");
    res.end("Hello World");
  }
  listen() {
    const server = http.createServer(this.callback);
    return server.listen(...arguments);
  }
}

http 服务结合 compose 使用

class Koa extends Emitter {
  constructor() {
    super();
    this.middlewares = [];
    this.ctx = Object.create(context);
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback(req, res) {
    res.statusCode = 200;
    this.ctx.req = req;
    this.ctx.res = res;
    compose(this.middlewares)(this.ctx);
  }
  listen() {
    const server = http.createServer(this.callback.bind(this));
    return server.listen(...arguments);
  }
}

完整源码

通过上面几个步骤,我们得到了下面的代码

const http = require("http");
const Emitter = require("events");
const compose = require("./compose"); //参考上页内容

const context = {
  _body: null,
  req: null,
  res: null,
  get body() {
    return this._body;
  },
  set body(val) {
    if (typeof val !== "string") {
      val = JSON.stringify(val);
    }
    this._body = val;
    this.res.end(this._body);
  },
};
class Koa extends Emitter {
  constructor() {
    super();
    this.middlewares = [];
    this.ctx = Object.create(context);
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  callback(req, res) {
    res.statusCode = 200;
    this.ctx.req = req;
    this.ctx.res = res;
    compose(this.middlewares)(this.ctx);
  }
  listen() {
    const server = http.createServer(this.callback.bind(this));
    return server.listen(...arguments);
  }
}

module.exports = Koa;

其中 compose 的源码可以参考 koa 中间键的洋葱模型 ,也可以使用 koa-compose

测试代码

const Koa = require("./koa");
const app = new Koa();
function middleware1() {
  return async (ctx, next) => {
    console.log("middleware1 start");
    await next();
    console.log("middleware1 end");
  };
}
function middleware2() {
  return async (ctx, next) => {
    console.log("middleware2 start");
    await next();
    console.log("middleware2 end");
  };
}

app.use(middleware1());
app.use(middleware2());
app.use(async (ctx, next) => {
  ctx.body = {
    status: 1,
  };
});
console.log(app.middlewares);
app.listen(3000, () => {
  console.log("sever listen http://localhost:3000");
});

访问 http://localhost:3000 ,控制台会输出,同时页面会显示 {"status": 1}

middleware1 start
middleware2 start
middleware2 end
middleware1 end

完善

完善请求处理

class Koa extends Emitter {
  constructor() {
    super();
    this.middlewares = [];
  }
  callback() {
    const fn = compose(this.middlewares);
    const handleRequest = (req, res) => {
      const ctx = creatContext(req, res);
      this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  use(middleware) {
    this.middlewares.push(middleware);
  }
  handleRequest(ctx, fnMiddleware) {
    fnMiddleware(ctx).then(() => {
      if (!ctx.body) {
        ctx.throw(404, "Not Found");
      }
      ctx.res.end(ctx.body);
    });
  }
  listen() {
    const server = http.createServer(this.callback());
    server.listen(...arguments);
  }
}

function creatContext(req, res) {
  const ctx = Object.create(context);
  ctx.req = req;
  ctx.res = res;
  return ctx;
}

重定向

const context = {
  ...
  redirect(url) {
    this.res.statusCode = 302
    this.set('Location', url)
    this.body = `Redirecting to ${url}.`
  }
  ...
}

完善 url 解析和 query 参数处理

const { URL } = require("url");

function parseQuery(str) {
  const res = Object.create(null);

  str
    .substr(1)
    .split("&")
    .forEach((item) => {
      if (item) {
        const i = item.search("=");
        const key = item.substr(0, i);
        const val = item.substr(i + 1);
        res[key] = res[key]
          ? Array.isArray(res[key])
            ? [...res[key], val]
            : [res[key], val]
          : val;
      }
    });
  return res;
}
const context = {
  get path() {
    return new URL(this.req.url, "http://localhost").pathname;
  },
  // 将?a=1&b=2 解析为 {a: "1", b: "2"}
  get query() {
    return parseQuery(new URL(this.req.url, "http://localhost").search);
  },
};

其他

const context = {
  ...
   get url() {
    return this.req.url
  },
  set(filed, val) {
    this.res.setHeader(filed, val)
  },
  throw(code, text) {
    this.res.statusCode = code
    this.res.end(text || 'Error')
  },
  get path() {
    return this.req.url
  }
  ...
}

完整代码

// koa.js
const Emitter = require("events");
const http = require("http");
const creatContext = require("./lib/context");
const compose = require("./lib/compose");

class Koa extends Emitter {
  constructor() {
    super();
    this.middlewares = [];
  }
  callback() {
    const fn = compose(this.middlewares);
    const handleRequest = (req, res) => {
      const ctx = creatContext(req, res);
      this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  use(middleware) {
    // console.log(middleware)
    this.middlewares.push(middleware);
  }
  handleRequest(ctx, fnMiddleware) {
    fnMiddleware(ctx).then(() => {
      if (!ctx.body) {
        ctx.throw(404, "Not Found");
      }
      ctx.res.end(ctx.body);
    });
  }
  listen() {
    const server = http.createServer(this.callback());
    server.listen(...arguments);
  }
}
module.exports = Koa;

上下文 context

// context.js
const { URL } = require("url");

function parseQuery(str) {
  const res = Object.create(null);

  str
    .substr(1)
    .split("&")
    .forEach((item) => {
      if (item) {
        const i = item.search("=");
        const key = item.substr(0, i);
        const val = item.substr(i + 1);
        res[key] = res[key]
          ? Array.isArray(res[key])
            ? [...res[key], val]
            : [res[key], val]
          : val;
      }
    });
  return res;
}
const context = {
  _body: null,
  req: null,
  res: null,
  get body() {
    return this._body;
  },
  set body(val) {
    if (typeof val !== "string") {
      val = JSON.stringify(val);
    }
    this._body = val;
    // this.res.end(this._body)
  },
  get method() {
    return this.req.method;
  },
  set(filed, val) {
    this.res.setHeader(filed, val);
  },
  throw(code, text) {
    this.res.statusCode = code;
    this.res.end(text || "Error");
  },
  redirect(url) {
    this.res.statusCode = 302;
    this.set("Location", url);
    this.body = `Redirecting to ${url}.`;
  },
  get path() {
    return new URL(this.req.url, "http://localhost").pathname;
  },
  get query() {
    return parseQuery(new URL(this.req.url, "http://localhost").search);
  },
};
function creatContext(req, res) {
  const ctx = Object.create(context);
  ctx.req = req;
  ctx.res = res;
  return ctx;
}
module.exports = creatContext;

compose

// compose.js
function compose(middlewares) {
  console.log(middlewares);
  return (context, next) => {
    return dispatch(0);
    function dispatch(i) {
      let fn = middlewares[i];
      if (i === middlewares.length) fn = next;
      if (!fn) return Promise.resolve();
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    }
  };
}
module.exports = compose;

模拟实现 koa 中间键的洋葱模型

在 koa 源码中,可以看到引入了 koa-compose

实现 koa 中间键的洋葱模型非常简单,实现思路就是每次执行中间键 await next() ,都要等到下次中间键执行完毕再往后执行

function middleware1() {
  return async (ctx, next) => {
    console.log("logger1", ctx.path);
    // 等待下个中间键执行完毕
    await next();
    console.log("logger1", ctx.body);
  };
}

实现代码

实现代码也非常简单

/**
 * @param {Array} middleware
 * @return {Function}
 */
function compose(middlewares) {
  return (context, next) => {
    return dispatch(0);
    function dispatch(i) {
      let fn = middlewares[i];
      if (i === middlewares.length) fn = next;
      if (!fn) return Promise.resolve();
      // 返回一个Promise
      // fn(context, dispatch.bind(null, i+1))
      // context 上下文对象
      // dispatch.bind(null, i+1) 就是next ,可以将next传递给中间键
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    }
  };
}

下面是 koa-compose 的源码

"use strict";

/**
 * Expose compositor.
 */

module.exports = compose;

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        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, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

koa-router

使用 koa-router

var Koa = require('koa')
var Router = require('koa-router')

var app = new Koa()
var router = new Router()

router.get('/', (ctx, next) => {
  // ctx.router available
})
router.get('/:category/:title', (ctx, next) => {
  console.log(ctx.params)
  // => { category: 'programming', title: 'how-to-node' }
})
app.use(router.routes()).use(router.allowedMethods())

实现

调用route.get('/xxx', middleware)route.post('/xxx', middleware)等时,会将注册的方法和中间键都保存起来

实现代码也很简单

router.get('/', (ctx, next) => {
  // ctx.router available
})
router.get('/aa', middleware)
app.use(router.routes())

调用app.use(router.routes())后,当有请求过来时,就会处理我们注册的路由,当访问路径和方法都匹配时,就执行该中间键

class Router {
  routes() {
    return async (ctx, next) => {
      for (let i = 0, len = stack.length; i < len; i++) {
        const routeItem = stack[i]
        // 方法和路径都匹配
        if (routeItem.path === ctx.path && routeItem.method === ctx.method) {
          route = stack[i].middleware
          route(ctx, next)
          break
        }
      }
      await next()
    }
  }
}

完整处理流程

class Layer {
  constructor(path, methods, middleware, opts) {
    this.path = path
    this.methods = methods
    this.middleware = middleware
  }
}
class Router {
  constructor() {
    this.methods = ['HEAD', 'OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE']
    this.stack = []
    this.initMethods()
  }
  initMethods() {
    this.methods.forEach((method) => {
      // 当调用router.get...,会register注册路由,会将方法和中间键保存下来
      this[method] = this[method.toLowerCase()] = (path, middleware) => {
        this.register(path, [method], middleware)
      }
    })
  }
  // 路由注册,会将方法和中间键保存下来
  register(path, methods, middleware, opts) {
    let route = new Layer(path, methods, middleware, opts)
    this.stack.push(route)
    return this
  }
  routes() {
    return async (ctx, next) => {
      let stack = this.stack
      let route
      for (let i = 0, len = stack.length; i < len; i++) {
        const routeItem = stack[i]
        if (
          routeItem.path === ctx.path &&
          routeItem.methods.indexOf(ctx.method) > -1
        ) {
          route = stack[i].middleware
          break
        }
      }
      if (typeof route === 'function') {
        route(ctx, next)
      }
      await next()
    }
  }
}

下面实现动态路由

  • 实现思路就是根据注册的路由生产动态正则
    • /a/:id 生成正则/^\/a\/([^/]+?)[/]?$/,并保存动态的键[id]
    • 发送请求时,根据正则去匹配,获取对应 id 的值,并将值添加到上下文对象上,如{id: 1}
class Layer {
  constructor(path, methods, middleware, opts) {
    this.path = path
    this.methods = methods
    this.middleware = middleware
    this.pathRegStrList = []
    this.pathPramsKeyList = []
    this.initPathToRegxExpConfig(path)
  }
  // 生产动态正则和参数
  initPathToRegxExpConfig(path) {
    const pathItemReg = /\/([^\/]{2,})/g
    const paramsKeyReg = /\/\:([\w\_]+)/
    // 所有地址
    const pathItems = path.match(pathItemReg)
    // 用来保存动态地址
    const pathPramsKeyList = []
    // 路径匹配正则数组
    const pathRegList = []
    if (Array.isArray(pathItems)) {
      pathItems.forEach((path) => {
        if (paramsKeyReg.test(path)) {
          pathRegList.push(`/([^\/]+?)`)
          pathPramsKeyList.push(path.replace(/\/\:/g, ''))
        } else {
          pathRegList.push(path)
        }
      })
    }
    this.pathPramsKeyList = pathPramsKeyList
    this.pathReg = new RegExp(`^${pathRegList.join('')}[\/]?$`)
  }
  match(path, method) {
    return this.methods.indexOf(method) > -1 && this.pathReg.test(path)
  }
  getParams(path) {
    const execRes = this.pathReg.exec(path)
    if (!execRes) {
      return {}
    }
    const res = {}
    this.pathPramsKeyList.forEach((item, index) => {
      res[item] = execRes[index + 1]
    })
    return res
  }
}