KOA从0到0.5的演变过程

156 阅读5分钟

引言

在Node.js众多web框架中,koa可谓一枝独秀,接下来我们从零开始实现半个koa,为刚开始接触Node.js 和阅读koa源码的童鞋提供一些思路。

搭建基础服务框架

演变过程

在Node的世界里创建服务器可以说特别简单:

const http = require('http');

http.createServer((req, res) => {
  res.end('hello world!');
}).listen(8080)

如果只是对外提供一个接口,这就已经足够了。但是一个服务需要根据请求路径返回不同信息,这时就需要对请求进行划分。最简单的就是根据if 或者 switch 判断路径,然后调用不同的方法。

const http = require('http');

const homeHandle = (req, res)=> {
  res.end('home');
}
http.createServer(function(req, res){
  switch(req.url){
    case '/home':
      homeHandle();
      break;
    default:
      res.end('default');
      break;
  }
}).listen(8686)

抽离路由

如果只有几个路由这样处理无可厚非,但是如果有几十个路由呢?这样处理很显然是不及格的。我们需要把路由抽出来,抽出来方法有不少。
比如我们可以在一个路由文件保存一个数组,在数组里定义路径和对应方法,有请求时读取文件找到对应处理方法,此l类型可以把转变成将路由和对应方法保存到数据库或者Redis。
比如我们直接在代码里用一个数组保存路由和对应方法。接下来我们就直接在代码定义一个数组来保存路由。

const http = require('http');

// 对应请求方法
const handleHello = (req,res)=> {
  res.end('hello')
}

class Router {
  // 保存路由已经处理数组
  routerArray = []
  register(method, routePath, routeHandle){
    this.routerArray.push({ method, routePath, routeHandle })
  }
  get(routePath, routeHandle){
    this.register('get',routePath, routeHandle)
  }
  post(routePath, routeHandle){
    this.register('post', routePath, routeHandle)
  }
  handle(req, res){
    const existed = this.routerArray.find(item => item.routePath === req.url)
    existed.routeHandle(req, res)
  }
}

const requestHandle = (req, res) => {
  const  router = new Router();
  router.get('/hello', handleHello)
  router.handle(req, res)
}

http.createServer(requestHandle).listen(8686)

现在我们把路由模块放到单独类里,如果你想你随时可以把它放到单独文件里。\

新增模块需求

现在又来新需求了,比如必须要在每个请求前加上日志、权限控制等模块。 这时最直接的就是就是在执行路由处理前加上日志处理和权限控制

  const  router = new Router();
  router.get('/hello', handleHello)
  handleLogger();  // 日志处理
  handleAuth();    // 权限处理
  router.handle(req, res)

服务拆分

随着项目的进行、人数的增加、需求的增加,所有的业务处理和基础功能处理(比如每个实现路由的函数都希望请求是被格式化过的)都放到同一个函数里项目就就变得很难管理,这时需要进行拆分了,我们把基础服务框架从中抽出来。
这时基础服务框架需要保留哪些功能,需要提供哪些功能就是各个web服务框架的区别了,比如Express 把路由模块保留了而koa则把路由也做成中间件。
现在我们把上面路由、日志都拆离基础服务框架,而基础服务框架需要支持对这些功能的拓展,最直接的思路就是在基础服务框架维护一个数组队列,其他模块可以通过框架暴露出来的接口把自己加入这个队列,当有请求过来时,可以依次调用这些模块功能。代码如下:

const http = require('http');

const MyServer = function () {
  this.middles = []; // 模块队列
  
  // 通过此接口将自己加入队列
  this.use =  (middleFn)=> {
    this.middles.push(middleFn)
  }

  this.requestHandle = (req, res) => {
    this.middles.map(midItem => {
      midItem(req, res)
    })
  } 

  this.listen = (port)=>{
    http.createServer(this.requestHandle).listen(port)
  }
}

module.exports = MyServer;

调用use方法将需要执行的函数注册到服务中,具体方法如下:

const loggerMiddle = (req, res) => {
  console.log('enter logger');
}
const m = new mServer()
m.use(loggerMiddle)
m.listen(8686)

这种链式调用能满足注册进来的模块被执行,但是却只能依次执行里面的代码,不能在外层进行任何控制。
比如需要知道函数具体执行时间,可能就需要在开头和结尾都插入一个模块,如果能做到在外层直接控制下一个模块执行的时机,那就不需要插入两段代码来完成一个功能。
实现思路:把下一个需要执行的模块传递个当前模块,当前模块就可以随心所欲的控制下一个模块执行的时机了。只需对上面requestHandle代码改造如下:

  this.requestHandle = (req, res) => {
    let midIndex = 0;
    const callNext = ()=> {
      midIndex++;
      if(midIndex == this.middles.length) return;
      this.middles[midIndex](req, res, callNext)
    }

    this.middles[midIndex](req, res, callNext)
  }

而对应需要注册的模块也需要改成

const loggerMiddle = (req, res, next) => {
  console.log('enter logger');
  next();
  console.log('leave');
}

而请求都是异步的,所以上述requestHandle代码需要进行少许改动

  this.requestHandle = (req, res) => {
    let midIndex = 0;
    const callNext = ()=> {
      midIndex++;
      if(midIndex == this.middles.length) return Promise.resolve() ;
      Promise.resolve(this.middles[midIndex](req, res, callNext))
    }

    Promise.resolve(this.middles[midIndex](req, res, callNext))
  }

上述代码在功能上已经完成,但是从写法上有待改进

 this.handleRequest = (req, res) => {
    const dispatch = (index) => {
      if(index >= this.middles.length) return Promise.resolve()
      const fn = this.middles[index]
      return Promise.resolve(fn(req, res, ()=>dispatch(index+1) ))
    }

    dispatch(0)
  }

上下文实现

现在各个之间模块调用可控了,但是现在需要在各个模块之间传递一些数据。
思路其实很简单就是定义一个对象,把request和response也包含进去,然后用这个对象在各个模块之间传递:

  this.createContext = (req, res) => {
    const context = {}
    context.req = req;
    context.res = res;
    return context;
  }

  this.handleMiddles = (ctx) => {
    const dispatch = (index) => {
      if(index >= this.middles.length) return Promise.resolve()
      const fn = this.middles[index]
      return Promise.resolve(fn(ctx, ()=>dispatch(index+1) ))
    }

    dispatch(0)
  }

  const handleRequest = async (req, res) => {
    // 定一个对象将request 和response包含进去
    const ctx = this.createContext(req, res);

    // 使用该对象在模块之间传递
    await this.handleMiddles(ctx)
  }

而模块定义则是

const loggerMiddle = async (ctx, next) => {
  console.log('enter logger');
  await next();
  console.log('leave logger');
}

至此koa 的骨架已经完成,剩下的就是将context 可以提供的内容就行拓展,以及各个错误处理。

结束

第一次阅读koa源码时没能掌握主要脉络,甚至工作2年后,再回头阅读才猛然惊醒,希望此文章能给初学者一点思路和启发。