通过实例分析javascript中的“中间件”

shiyangzhaoa

介绍

如果你使用过redux或者nodejs,那么你对“中间件”这个词一定不会感到陌生,如果没用过这些也没关系,也可以通过这个来了解javascript中的事件流程。

一个例子

有一类人,非常的懒(比如说我),只有三种行为动作,sleep,eat,sleepFirst,伪代码就是:

var wang = new LazyMan('王大锤');
wang.eat('苹果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同于以下的代码
const wang = new LazyMan('王大锤');
wang.eat('苹果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);

执行结果如下图:

睡2S
不管什么,先睡2S


图片描述
然后做个介绍,吃东西,睡5S


图片描述
醒来,吃


但是javascript只有一个线程,也并没有像php的sleep的那种方法。实现的思路就是eat、sleep、sleepFirst这些事件放在任务列中,通过next去依次执行方法。我还是希望在看源码前先手动实现一下试试看,其实这就是个lazyMan的实现。


下面是我的实现方式:

class lazyMan{
    constructor(name) {
        this.tasks = [];
        const first = () => {
            console.log(`my name is ${name}`);
            this.next();
        }
        this.tasks.push(first);
        setTimeout(()=>this.next(), 0);
    }
    next() {
        const task = this.tasks.shift();
        task && task();
    }
    eat(food) {
        const eat = () => {
            console.log(`eat ${food}`);
            this.next();
        };
        this.tasks.push(eat);
        return this;
    }
    sleep(time) {
        const newTime = time * 1000;
        const sleep = () => {
            console.log(`sleep ${time}s!`);
            setTimeout(() => {
                this.next();
            }, newTime);
        };
        this.tasks.push(sleep);
        return this;
    }
    sleepFirst(time) {
        const newTime = time * 1000;
        const sleepzFirst = () => {
            console.log(`sleep ${time}s first!`);
            setTimeout(() => {
                this.next();
            }, newTime);
        };
        this.tasks.unshift(sleepzFirst);
        return this;
    }
}
const aLazy = new lazyMan('王大锤');
aLazy.eat('苹果').eat('香蕉').sleep(5).eat('葡萄').eat('橘子').sleepFirst(2)

我们上面说过

wang.eat('苹果').eat('香蕉').sleep(5).eat('葡糖').eat('橘子').sleepFirst(2);
//等同于以下的代码
wang.eat('苹果');
wang.eat('香蕉');
wang.sleep(5);
wang.eat('葡糖');
wang.eat('橘子');
wang.sleepFirst(2);

如果你使用过过node,你会发现,这种写法似乎有点熟悉的感觉,我们来看一下一个koa2(一个node的框架)项目的主文件:

const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const cors = require('koa-cors2');

const routers = require('./src/routers/index')

const app = new Koa();

app.use(cors());
app.use(bodyParser());
app.use(routers.routes()).use(routers.allowedMethods())

app.listen(3000);

有没有发现结构有一点像?

koa中的中间件

废话不多说,直接看源码...
app.use就是用来注册中间件的,我们先看use的实现:

 use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

先解释一下里面做了什么处理,fn就是传入的函数,首先肯定要判断是否是个函数,如果不是,抛出错误,其次是判断fn是否是一个GeneratorFunction,我用的是koa2,koa2中用asyncawait来替代koa1中的generator,如果判断是生成器函数,证明使用或者书写的中间件为koa1的,koa2中提供了库koa-convert来帮你把koa1中的中间件转换为koa2中的中间件,这里如果判断出是koa1的中间件会给你提醒,这里会主动帮你转换,就是代码中的convert方法。如果验证没出现问题,就注册这个中间件并放到中间件数组中。
这里我们只看到了把中间件加到数组中,然后就没有做其他处理了。
我们再看koa2中listen

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

这里只是启动了个server,然后传进了一个回调函数的结果,我们看原生启动一个server大概是什么样的:

https.createServer(options, function (req, res) {
  res.writeHead(200);
  res.end("hello world\n");
}).listen(3000);

原生的回调函数接受两个参数,一个是request一个是response,我们再去看koa2中这个回调函数的代码:

callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      const onerror = err => ctx.onerror(err);
      const handleResponse = () => respond(ctx);
      onFinished(res, onerror);
      return fn(ctx).then(handleResponse).catch(onerror);
    };

    return handleRequest;
  }

这里有一个const fn = compose(this.middleware);compose这种不知道大家用的多不多,compose是函数式编程中使用比较多的东西,这里将多个中间件组合起来。
我们去看compose的实现:

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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

首先判断是否是中间件数组,这个不用多说,for...of是ES6中的新特性,这里不做说明,需要注意的是,数组和Set集合默认的迭代器是values()方法,Map默认的是entries()方法。

这里的dispatch和next一样是所有的中间件的核心,dispatch的参数i其实也就是对应中间件的下标,,在第一次调用的时候传入了参数0,如果中间件存在返回Promise

return Promise.resolve(fn(context, function next () {
  return dispatch(i + 1)
}))

我们lazyMan链式调用时不断的shift()取出下一个要执行的事件函数,koa2里采用的是通过数组下标的方式找到下一个中间件,这里是用Promise.resolve包起来就达到了每一个中间件await next()返回的结果都刚好是下一个中间件的执行。不难看出此处dispatch是个递归调用,多个中间件会形成一个栈结构。其中i的值总是比上一次传进来的大,正常执行index的值永远小于i,但只要在同一个中间件中next执行两次以上,index的值就会等于i,同时会抛出错误。但如果不执行next,中间件的处理也会终止。

整理下流程:

  1. compose(this.middleware)(ctx)默认会执行中间件数组中的第一个,也就是代码中的dispatch(0),第一个中间件通过await next()返回的是第二个中间件的执行。
  2. 然后第二个中间件中执行await next(),然后返回第三个...以此类推
  3. 中间件全部处理结束以后,剩下的就是通过中间件中不断传递的context来对请求作处理了。
阅读 3.5k

我大EOI前端
[链接] 虽然公司尚小,但是志向不小

立志做一个好厨师!!!

425 声望
24 粉丝
0 条评论

立志做一个好厨师!!!

425 声望
24 粉丝
文章目录
宣传栏