兼容接口就是一把梭——适配器的业务场景

大家知道我们现在有一个非常好用异步方案叫fetch,它的写法比ajax优雅很多。因此在不考虑兼容性的情况下,我们更愿意使用fetch、而不是使用ajax来发起异步请求。李雷是拜fetch教的忠实信徒,为了能更好地使用fetch,他封装了一个基于fetch的http方法库:

  1. export default class HttpUtils {
  2. // get方法
  3. static get(url) {
  4. return new Promise((resolve, reject) => {
  5. // 调用fetch
  6. fetch(url)
  7. .then(response => response.json())
  8. .then(result => {
  9. resolve(result)
  10. })
  11. .catch(error => {
  12. reject(error)
  13. })
  14. })
  15. }
  16. // post方法,data以object形式传入
  17. static post(url, data) {
  18. return new Promise((resolve, reject) => {
  19. // 调用fetch
  20. fetch(url, {
  21. method: 'POST',
  22. headers: {
  23. Accept: 'application/json',
  24. 'Content-Type': 'application/x-www-form-urlencoded'
  25. },
  26. // 将object类型的数据格式化为合法的body参数
  27. body: this.changeData(data)
  28. })
  29. .then(response => response.json())
  30. .then(result => {
  31. resolve(result)
  32. })
  33. .catch(error => {
  34. reject(error)
  35. })
  36. })
  37. }
  38. // body请求体的格式化方法
  39. static changeData(obj) {
  40. var prop,
  41. str = ''
  42. var i = 0
  43. for (prop in obj) {
  44. if (!prop) {
  45. return
  46. }
  47. if (i == 0) {
  48. str += prop + '=' + obj[prop]
  49. } else {
  50. str += '&' + prop + '=' + obj[prop]
  51. }
  52. i++
  53. }
  54. return str
  55. }
  56. }

当我想使用 fetch 发起请求时,只需要这样轻松地调用,而不必再操心繁琐的数据配置和数据格式化:

  1. // 定义目标url地址
  2. const URL = "xxxxx"
  3. // 定义post入参
  4. const params = {
  5. ...
  6. }
  7. // 发起post请求
  8. const postResponse = await HttpUtils.post(URL,params) || {}
  9. // 发起get请求
  10. const getResponse = await HttpUtils.get(URL) || {}

真是个好用的方法库!老板看了李雷的 HttpUtils 库,喜上眉梢——原来老板也是个拜 fetch 教。老板说李雷,咱们公司以后要做潮流公司了,写代码不再考虑兼容性,我希望你能把公司所有的业务的网络请求都迁移到你这个 HttpUtils 上来,这样以后你只用维护这一个库了,也方便。李雷一听,悲从中来——他是该公司的第 99 代员工,对远古时期的业务一无所知。而该公司第1代员工封装的网络请求库,是基于 XMLHttpRequest 的,差不多长这样:

  1. function Ajax(type, url, data, success, failed){
  2. // 创建ajax对象
  3. var xhr = null;
  4. if(window.XMLHttpRequest){
  5. xhr = new XMLHttpRequest();
  6. } else {
  7. xhr = new ActiveXObject('Microsoft.XMLHTTP')
  8. }
  9. ...(此处省略一系列的业务逻辑细节)
  10. var type = type.toUpperCase();
  11. // 识别请求类型
  12. if(type == 'GET'){
  13. if(data){
  14. xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
  15. }
  16. // 发送get请求
  17. xhr.send();
  18. } else if(type == 'POST'){
  19. xhr.open('POST', url, true);
  20. // 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
  21. xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
  22. // 发送post请求
  23. xhr.send(data);
  24. }
  25. // 处理返回数据
  26. xhr.onreadystatechange = function(){
  27. if(xhr.readyState == 4){
  28. if(xhr.status == 200){
  29. success(xhr.responseText);
  30. } else {
  31. if(failed){
  32. failed(xhr.status);
  33. }
  34. }
  35. }
  36. }
  37. }

实现逻辑我们简单描述了一下,这个不是重点,重点是它是这样调用的:

  1. // 发送get请求
  2. Ajax('get', url地址, post入参, function(data){
  3. // 成功的回调逻辑
  4. }, function(error){
  5. // 失败的回调逻辑
  6. })

李雷佛了 —— 不仅接口名不同,入参方式也不一样,这手动改要改到何年何日呢?

还好李雷学过设计模式,他立刻联想到了专门为我们抹平差异的适配器模式。要把老代码迁移到新接口,不一定要挨个儿去修改每一次的接口调用——正如我们想用 iPhoneX + 旧耳机听歌,不必挨个儿去改造耳机一样,我们只需要在引入接口时进行一次适配,便可轻松地 cover 掉业务里可能会有的多次调用(具体的解析在注释里):

  1. // Ajax适配器函数,入参与旧接口保持一致
  2. async function AjaxAdapter(type, url, data, success, failed) {
  3. const type = type.toUpperCase()
  4. let result
  5. try {
  6. // 实际的请求全部由新接口发起
  7. if(type === 'GET') {
  8. result = await HttpUtils.get(url) || {}
  9. } else if(type === 'POST') {
  10. result = await HttpUtils.post(url, data) || {}
  11. }
  12. // 假设请求成功对应的状态码是1
  13. result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
  14. } catch(error) {
  15. // 捕捉网络错误
  16. if(failed){
  17. failed(error.statusCode);
  18. }
  19. }
  20. }
  21. // 用适配器适配旧的Ajax方法
  22. async function Ajax(type, url, data, success, failed) {
  23. await AjaxAdapter(type, url, data, success, failed)
  24. }

如此一来,我们只需要编写一个适配器函数AjaxAdapter,并用适配器去承接旧接口的参数,就可以实现新旧接口的无缝衔接了~

生产实践:axios中的适配器

数月之后,李雷的老板发现了网络请求神库axios,于是团队的方案又整个迁移到了axios——对于心中有适配器的李雷来说,这现在已经根本不是个事儿。不过本小节我们要聊的可不再是“如何使现有接口兼容axios”了(这招我们上个小节学过了)。此处引出axios,一是因为大家对它足够熟悉(不熟悉的同学,点这里可以快速熟悉一下~),二是因为axios本身就用到了我们的适配器模式,它的兼容方案值得我们学习和借鉴。
在使用axios时,作为用户我们只需要掌握以下面三个最常用的接口为代表的一套api:

  1. // Make a request for a user with a given ID
  2. axios.get('/user?ID=12345')
  3. .then(function (response) {
  4. // handle success
  5. console.log(response);
  6. })
  7. .catch(function (error) {
  8. // handle error
  9. console.log(error);
  10. })
  11. .then(function () {
  12. // always executed
  13. })
  14. axios.post('/user', {
  15. firstName: 'Fred',
  16. lastName: 'Flintstone'
  17. })
  18. .then(function (response) {
  19. console.log(response);
  20. })
  21. .catch(function (error) {
  22. console.log(error);
  23. });
  24. axios({
  25. method: 'post',
  26. url: '/user/12345',
  27. data: {
  28. firstName: 'Fred',
  29. lastName: 'Flintstone'
  30. }
  31. })

便可轻松地发起各种姿势的网络请求,而不用去关心底层的实现细节。
除了简明优雅的api之外,axios 强大的地方还在于,它不仅仅是一个局限于浏览器端的库。在Node环境下,我们尝试调用上面的 api,会发现它照样好使 —— axios 完美地抹平了两种环境下api的调用差异,靠的正是对适配器模式的灵活运用。

axios 的核心逻辑中,我们可以注意到实际上派发请求的是 dispatchRequest 方法。该方法内部其实主要做了两件事:

  1. 数据转换,转换请求体/响应体,可以理解为数据层面的适配;
  2. 调用适配器。

调用适配器的逻辑如下:

  1. // 若用户未手动配置适配器,则使用默认的适配器
  2. var adapter = config.adapter || defaults.adapter;
  3. // dispatchRequest方法的末尾调用的是适配器方法
  4. return adapter(config).then(function onAdapterResolution(response) {
  5. // 请求成功的回调
  6. throwIfCancellationRequested(config);
  7. // 转换响应体
  8. response.data = transformData(
  9. response.data,
  10. response.headers,
  11. config.transformResponse
  12. );
  13. return response;
  14. }, function onAdapterRejection(reason) {
  15. // 请求失败的回调
  16. if (!isCancel(reason)) {
  17. throwIfCancellationRequested(config);
  18. // 转换响应体
  19. if (reason && reason.response) {
  20. reason.response.data = transformData(
  21. reason.response.data,
  22. reason.response.headers,
  23. config.transformResponse
  24. );
  25. }
  26. }
  27. return Promise.reject(reason);
  28. });

大家注意注释的第一行,“若用户未手动配置适配器,则使用默认的适配器”。手动配置适配器允许我们自定义处理请求,主要目的是为了使测试更轻松。

实际开发中,我们使用默认适配器的频率更高。默认适配器在axios/lib/default.js里是通过getDefaultAdapter方法来获取的:

  1. function getDefaultAdapter() {
  2. var adapter;
  3. // 判断当前是否是node环境
  4. if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
  5. // 如果是node环境,调用node专属的http适配器
  6. adapter = require('./adapters/http');
  7. } else if (typeof XMLHttpRequest !== 'undefined') {
  8. // 如果是浏览器环境,调用基于xhr的适配器
  9. adapter = require('./adapters/xhr');
  10. }
  11. return adapter;
  12. }

我们再来看看 Node 的 http 适配器和 xhr 适配器大概长啥样:

http 适配器:

  1. module.exports = function httpAdapter(config) {
  2. return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {
  3. // 具体逻辑
  4. }
  5. }

xhr 适配器:

  1. module.exports = function xhrAdapter(config) {
  2. return new Promise(function dispatchXhrRequest(resolve, reject) {
  3. // 具体逻辑
  4. }
  5. }

具体逻辑啥样,咱们目前先不关心,有兴趣的同学,可以狠狠地点这里阅读源码。咱们现在就注意两个事儿:

  • 两个适配器的入参都是 config;
  • 两个适配器的出参都是一个 Promise。

Tips:要是仔细读了源码,会发现两个适配器中的 Promise 的内部结构也是如出一辙。

这么一来,通过 axios 发起跨平台的网络请求,不仅调用的接口名是同一个,连入参、出参的格式都只需要掌握同一套。这导致它的学习成本非常低,开发者看了文档就能上手;同时因为足够简单,在使用的过程中也不容易出错,带来了极佳的用户体验,axios 也因此越来越流行。

这正是一个好的适配器的自我修养——把变化留给自己,把统一留给用户。在此处,所有关于 http 模块、关于 xhr 的实现细节,全部被 Adapter 封装进了自己复杂的底层逻辑里,暴露给用户的都是十分简单的统一的东西——统一的接口,统一的入参,统一的出参,统一的规则。用起来就是一个字 —— 爽!