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

ETL工程师
• 阅读 4013

介绍

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

执行结果如下图:

通过实例分析javascript中的“中间件”
不管什么,先睡2S


通过实例分析javascript中的“中间件”
然后做个介绍,吃东西,睡5S


通过实例分析javascript中的“中间件”
醒来,吃


但是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来对请求作处理了。
点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Peter20 Peter20
4年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
ETL工程师
ETL工程师
Lv1
秦时明月汉时关,万里长征人未还。
文章
4
粉丝
0
获赞
0