8

来,我们手写一个简易版的mock.js吧(模拟fetch && Ajax请求)

 3 years ago
source link: https://zhuanlan.zhihu.com/p/91466774
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

来,我们手写一个简易版的mock.js吧(模拟fetch && Ajax请求)

广发证券 技术工程师

预期的mock的使用方式

首先我们从使用的角度出发,思考编码过程

  • M1. 通过配置文件配置url和response
  • M2. 自动检测环境为开发环境时启动Mock.js
  • M3. mock代码能直接覆盖global.fetch方法或者XMLHttpRequest构造函数,实现开发无感知
  • M4. mock配置不影响实际的请求,可无缝切换为实际请求

M1. 通过配置文件配置url和response

比较符合我们使用习惯的,也许是下面这种mock方式,有一个专门的配置文件,管理请求的url和返回值。每个请求对应输出数组中的一个对象,对象的rule属性可以是一个字符串或者一个正则表达式,用来匹配url,对象的res属性则是我们希望的从中请求中拿到的返回的数据 (也许这里面还应该加个type表示请求的类型,但是我这个是mock的最简化版,所以就不加了)

// api.js
module.exports = [
  {
    rule: '/mock',
    res: {
      a: 'data',
      b: [{c: 1}, {d: 1}],
    },
  },
  {
    rule: '/mock2',
    res: {
      j: {
         k: 'XXX'
      },
    },
  },
];

M2. 自动检测环境为开发环境时启动Mock.js

// __DEV__ 可能是webpack等配置的全局变量
if (__DEV__) {
  require ('./ajaxMock.js');
  require ('./fetchMock.js');
}

M3. mock代码能直接覆盖global.fetch方法或者XMLHttpRequest构造函数,实现开发无感知

// fetchMock.js
window.fetch = function (url) {
    // 覆盖默认fetch
}
// ajaxMock.js
class XMLHttpRequest { 
 // ...覆盖默认XHR
}
window.XMLHttpRequest = XMLHttpRequest;

M4.mock配置不影响实际的请求,可无缝切换为实际请求

mock配置不影响实际的请求,当请求没有命中mock配置文件中的url时,自动切换为实际请求

// fetch
window.fetch = (url, cfg) => {
  if (命中config文件中的url) {
      // 覆盖默认fetch
  } else {
      return originFetch (url, cfg);
  }
};
// Ajax
const RealXHR = window.XMLHttpRequest;
class XMLHttpRequest {
  open (type, url, bool) {
    if (命中config文件中的url) {
      // 覆盖Ajax
    } else {
      // 使用系统原有的Ajax
      this.xhr = new RealXHR ();
      this.xhr.open (type, url, bool);
    }
  }
  send (args) {
    if (命中config文件中的url) {
      // 覆盖Ajax
    } else {
      // 使用系统原有的Ajax
      this.xhr.send (args);
    }
  }
}
window.XMLHttpRequest = XMLHttpRequest;

模拟fetch

直接上代码

// 保存系统原生的fetch
const originFetch = window.fetch;

// 根据fetch的要求返回的response
const normalize = resp => {
  return {
    ok: true,
    status: 200,
    text () {
      return Promise.resolve (resp);
    },
    json () {
      return Promise.resolve (resp);
    },
  };
};

// 覆盖fetch
window.fetch = (url, cfg) => {
  // url所对应的JSON对象
  let res;
  // 表示是否config文件中是否有和url对应的配置
  let hit = false;
  // 遍历配置文件中输出的数组,检测并尝试获取匹配url的res对象
  fakeApi.forEach (item => {
    let rule = item.rule;
    if (typeof rule === 'string') {
      rule = new RegExp (rule);
    }
    if (rule && rule.test (url)) {
      res = item.res;
      hit = true;
      return false;
    }
  });
  // 如果命中,那么返回一个Promise,并且传递上面和url匹配的JSON对象
  if (hit) {
    return new Promise (resolve => {
      setTimeout (() => {
        resolve (normalize (res));
      }, 1000);
    });
  }
  // 如果没有命中,那么使用系统原有的fetch的API,实现无缝切换
  return originFetch (url, cfg);
};

模拟ajax

直接上代码

// 保存系统原生的XMLHttpRequest对象
const RealXHR = window.XMLHttpRequest;

class XMLHttpRequest {
  constructor () {
    this.url = null;
    this.type = null;
    this.hit = false;
    // 真实的xhr
    this.xhr = null;
  }
  open (type, url, bool) {
    // 遍历配置文件中输出的数组,检测并尝试获取匹配url的res对象
    fakeApi.forEach (item => {
      let rule = item.rule;
      if (typeof rule === 'string') {
        rule = new RegExp (rule);
      }
      if (rule && rule.test (url)) {
        this.res = item.res;
        this.hit = true;
        return false;
      }
    });
    // 如果没有命中,那么使用系统原有的Ajax的API,实现无缝切换
    if (!this.hit) {
      this.xhr = new RealXHR ();
      this.xhr.open (type, url, bool);
    }
  }
  send (args) {
    // 如果命中,就覆盖Ajax的API
    if (this.hit && this.onreadystatechange) {
      this.readyState = 4;
      this.status = 200;
      this.responseText = JSON.stringify (this.res);
      this.onreadystatechange ();
    } else {
      // 如果没有命中,那么使用系统原有的Ajax的API,实现无缝切换
      this.xhr.send (args);
    }
  }
}
// 覆盖
window.XMLHttpRequest = XMLHttpRequest;

测试

export default [
  {
    rule: '/mock',
    res: {
      a: 'data',
      b: [{c: 1}, {d: 1}],
    },
  }
];
const xhr = new XMLHttpRequest ();
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    console.log (JSON.parse (xhr.responseText));
  }
};
xhr.open ('GET', '/mock');
xhr.send ();

额外扩展

除了上面的功能外,我们还能做什么?

  • 加个type类型,区分同一url下的不同请求类型,例如get,post
  • 加个布尔值err,表示失败的请求

上面这两个功能再做了我觉得就已经很足够了,当然,如果你还不满足,那你还可以尝试:

  • 处理xhr.open的第三个参数:async值,控制同步和异步
  • 处理xhr的progress,load,error,abort等事件监听
  • 处理fetch返回的response的其他方法,例如Body.formData()等等

再谈mock.js

早在之前我就写过一篇关于mock.js的文章。这个库目前在github是13k, 当然我觉得这个库是很强大的,因为它覆盖了从名字,地名,文章甚至是图片资源的mock数据,但是在实际使用中却多少有那么一点点“鸡肋”的感觉,为什么我会有这样一种感觉呢

这是因为它有一套自己的独立的模板语法,以及API,需要你学习和遵循

// 模拟JSON数据
Mock.mock({
  "array|1-10": [
    "Hello",
    "Mock.js",
    "!"
  ]
})
// 模拟大段的文章或句子
Random.paragraph( min?, max? )

当然mock.js有它自己的好处,例如:

  • 当你需要动态地造大数据量的mock数据的时候很方便,例如mock.js的Random.paragraph的API能很方便的帮你造出来
  • 当你有一些特殊的需求点的时候,例如一个长度宽度变化的图片的时候,mock.js也可以很强大的胜任Random.image( size?, background?)
  • 造出来的数据看起来“很漂亮很真实”,单纯看完全发现不了是假的数据

但问题在于,我在实际的开发中发现,我们大多数的数据场景根本就没这么复杂

我们大多数时候需要的仅仅只是:写一个响应数据的模版,例如一个json文件,然后使得发一个请求过去的时候能在ajax的onreadystatechange或者fetch(url).then中拿到数据就可以了

如果符合我们预期的mock的“完美需求”是100%的话
mock.js这个社区应用实现了80%到99%的需求的过程
但是它的使用方式却额外增加了30% ~ 40%的成本,

因为,我们大多数时候也许不太需要这么多的模板和“看起来很漂亮的数据”

这是我写这个简易版的mock的实现的原因


才疏学浅,还多指教,本文完


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK