前端JS高频面试题---8.适配器模式

150 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第19天,点击查看活动详情

定义

适配器模式(Adapter):将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • 用途:将无法使用的接口转换成可以使用的接口

  • 使用场景:库的适配、参数的适配、数据的适配

  • 注意事项:适配器模式本质上是一个亡羊补牢的模式,它解决的是现存的两个接口之间不兼容的问题,不应该在软件的初期开发阶段就使用该模式;如果在设计之初就能够统筹的规划好接口的一致性,那么适配器就应该尽量减少使用。

img

生活中有许多适配器的例子,以上这几个是特别常见的!

实践出真知

大家知道我们现在有一个非常好用异步方案叫fetch,它的写法比ajax优雅很多。因此在不考虑兼容性的情况下,我们更愿意使用fetch、而不是使用ajax来发起异步请求。

export default class HttpUtils {
  // get方法
  static get(url) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url)
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // post方法,data以object形式传入
  static post(url, data) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/json',
          'Content-Type': 'application/x-www-form-urlencoded'
        },
        // 将object类型的数据格式化为合法的body参数
        body: this.changeData(data)
      })
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // body请求体的格式化方法
  static changeData(obj) {
    var prop,
      str = ''
    var i = 0
    for (prop in obj) {
      if (!prop) {
        return
      }
      if (i == 0) {
        str += prop + '=' + obj[prop]
      } else {
        str += '&' + prop + '=' + obj[prop]
      }
      i++
    }
    return str
  }
}

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

// 定义目标url地址
const URL = "xxxxx"
// 定义post入参
const params = {
    ...
}

// 发起post请求
 const postResponse = await HttpUtils.post(URL,params) || {}
 
 // 发起get请求
 const getResponse = await HttpUtils.get(URL) || {}

当你遇到老项目网络请求封装库需要替换成你写的这个,一个一个改的工作量太多太大,而之前封装的网络请求库,是基于 XMLHttpRequest 的,差不多长这样:

function Ajax(type, url, data, success, failed){
    // 创建ajax对象
    var xhr = null;
    if(window.XMLHttpRequest){
        xhr = new XMLHttpRequest();
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP')
    }
 
   ...(此处省略一系列的业务逻辑细节)
   
   var type = type.toUpperCase();
    
    // 识别请求类型
    if(type == 'GET'){
        if(data){
          xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
        } 
        // 发送get请求
        xhr.send();
 
    } else if(type == 'POST'){
        xhr.open('POST', url, true);
        // 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
        xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
        // 发送post请求
        xhr.send(data);
    }
 
    // 处理返回数据
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
            if(xhr.status == 200){
                success(xhr.responseText);
            } else {
                if(failed){
                    failed(xhr.status);
                }
            }
        }
    }
}

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

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

这时候就可以使用适配器进行一次适配,不必一个一个改:

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
    const type = type.toUpperCase()
    let result
    try {
         // 实际的请求全部由新接口发起
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // 捕捉网络错误
        if(failed){
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
    await AjaxAdapter(type, url, data, success, failed)
}

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

生产实践:axios中的适配器

在使用axios时,作为用户我们只需要掌握以下面三个最常用的接口为代表的一套api:

// Make a request for a user with a given ID
axios.get('/user?ID=12345')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .then(function () {
    // always executed
  })   
     
axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });   

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
})

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

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

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

调用适配器的逻辑如下:

// 若用户未手动配置适配器,则使用默认的适配器
var adapter = config.adapter || defaults.adapter;
  
  // dispatchRequest方法的末尾调用的是适配器方法
  return adapter(config).then(function onAdapterResolution(response) {
    // 请求成功的回调
    throwIfCancellationRequested(config);

    // 转换响应体
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    // 请求失败的回调
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // 转换响应体
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });

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

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

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

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

http 适配器:

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

xhr 适配器:

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

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

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

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

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

感谢

谢谢你读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正。

我是Nicnic,如果觉得写得可以的话,请点个赞吧❤。

写作不易,「点赞」+「在看」+「转发」 谢谢支持❤

往期好文

《# Electron--快速入门》

《# Javascript高频手写面试题》

《# 高频CSS面试题》

《# JavaScript设计模式-前端开发不迷路》

《# vue组件汇总》