资讯专栏INFORMATION COLUMN

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

zhangyucha0 / 3062人阅读

摘要:如果验证没出现问题,就注册这个中间件并放到中间件数组中。但如果不执行,中间件的处理也会终止。整理下流程默认会执行中间件数组中的第一个,也就是代码中的,第一个中间件通过返回的是第二个中间件的执行。

介绍

如果你使用过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


然后做个介绍,吃东西,睡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
");
}).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,中间件的处理也会终止。

整理下流程:

compose(this.middleware)(ctx)默认会执行中间件数组中的第一个,也就是代码中的dispatch(0),第一个中间件通过await next()返回的是第二个中间件的执行。

然后第二个中间件中执行await next(),然后返回第三个...以此类推

中间件全部处理结束以后,剩下的就是通过中间件中不断传递的context来对请求作处理了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85225.html

相关文章

  • Express 实战(一):概览

    摘要:一个标准性的事件就是年的横空出世。引擎快速处理能力和异步编程风格,让开发者从多线程中解脱了出来。其次,通过异步编程范式将其高并发的能力发挥的淋漓尽致。它也仅仅是一个处理请求并作出响应的函数,并无任何特殊之处。 showImg(https://segmentfault.com/img/remote/1460000010819116); 在正式学习 Express 内容之前,我们有必要从大...

    zhaochunqi 评论0 收藏0
  • 玩转Koa -- 核心原理分析

    摘要:三中间件实现原理首先需要明确是中间件并不是中的概念,它只是和框架衍生的概念。中间件的执行流程主要由与函数决定依次取出中间件终止条件路由匹配规则函数中使用闭包函数来检测是否与当前路由相匹配,匹配则执行该上的中间件函数,否则继续检查下一个。 Koa作为下一代Web开发框架,不仅让我们体验到了async/await语法带来同步方式书写异步代码的酸爽,而且本身简洁的特点,更加利于开发者结合业务...

    jsbintask 评论0 收藏0
  • Express 实战(四):中间件

    摘要:调用函数执行下一个中间件函数。然后,该中间件调用函数检查文件是否存在。为了代码更加清晰,你也可以将代码改写为另外,这里在调用函数是使用的是作为输出选项。事实上,中间件有两种类型。 原生 Node 的单一请求处理函数,随着功能的扩张势必会变的越来越难以维护。而 Express 框架则可以通过中间件的方式按照模块和功能对处理函数进行切割处理。这样拆分后的模块不仅逻辑清晰,更重要的是对后期维...

    mochixuan 评论0 收藏0
  • 中间件执行模块koa-Compose源码分析

    摘要:原文博客地址,欢迎学习交流点击预览读了下的源码,写的相当的精简,遇到处理中间件执行的模块决定学习一下这个模块的源码。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。 原文博客地址,欢迎学习交流:点击预览 读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。 阅读本文可以学到: Koa中间件的加载...

    imtianx 评论0 收藏0
  • Express 搭建服务器

    摘要:指定需要处理的路由回调函数,即请求此路由的处理函数,它可以接收两个参数三个参数,四个参数。如果匹配到自定义的路由,立即执行回调函数,如果处理函数中没有则不再往下执行,如果执行了会继续向下匹配。 简介 Node.js® is a JavaScript runtime built on Chromes V8 JavaScript engine. Node.js uses an event-...

    CrazyCodes 评论0 收藏0

发表评论

0条评论

zhangyucha0

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<