如何实现axios的自定义适配器adapter

编辑: admin 分类: javascript 发布时间: 2022-03-11 来源:互联网
目录
  • 1. 适配器要实现的功能
    • 1.1 promise 和工具
    • 1.2 响应的格式
    • 1.3 超时设置
    • 1.4 主动取消请求
  • 2. 编写自定义适配器
    • 3. 将适配器添加到 axios 中
      • 4. 总结

        1. 适配器要实现的功能

        我们在基于 axios 实现额外的数据模块时,应当与 axios 的模式进行对齐。因此在返回的数据格式上,实现的功能上尽量保持一致。

        1.1 promise 和工具

        所有的适配均应当实现为 Promise 方式。

        而且,有些功能的实现,axios 将其下放到了适配器中自己进行实现,例如

        • url 的拼接:即 baseURL 和 url 的拼接,若存在 baseURL 且 url 为相对路径,则进行拼接,否则直接使用 url;
        • 参数的拼接:若是 get 请求,需要自行将 object 类型拼接为 url 参数的格式并与 url 拼接完成;

        这是自己需要实现的两个基本的工具方法。

        1.2 响应的格式

        这里我们要注意到请求接口正常和异常的格式。

        接口正常时:

        const result = {
            status: 200, // 接口的http 状态
            statusText: 'ok',
            config: 'config', // 传入的config配置,原样返回即可,方便在响应拦截器和响应结果中使用
            data: {}, // 真实的接口返回结果
        };

        接口异常时,我们可以看下 axios 源码中对错误信息的处理createError,enhanceError(createError 中调用了 enhanceError),首先会创建一个 error 实例,然后给这个 error 实例添加一个属性:

        module.exports = function enhanceError(error, config, code, request, response) {
            error.config = config;
            if (code) {
                error.code = code;
            }
        
            error.request = request;
            error.response = response;
            error.isAxiosError = true;
        
            error.toJSON = function toJSON() {
                return {
                    // Standard
                    message: this.message,
                    name: this.name,
                    // Microsoft
                    description: this.description,
                    number: this.number,
                    // Mozilla
                    fileName: this.fileName,
                    lineNumber: this.lineNumber,
                    columnNumber: this.columnNumber,
                    stack: this.stack,
                    // Axios
                    config: this.config,
                    code: this.code,
                };
            };
            return error;
        };

        可以看到,除了正常的错误信息外,还加入了很多别的属性,例如 request, response, config 等。这里我们在自己实现适配器时,最好也要这样统一编写,方便更上层的业务层统一处理,避免为单独的适配器进行特殊处理。

        关于 1.1 和 1.2 中的内容,若不进行打包编译,则需要自己实现。若还要通过 webpack 等打包工具编译一下的,可以直接引用 axios 中的方法,不用自己实现了,参考官方基于 axios 实现的mock-axios。例如:

        import axios from 'axios';
        import buildURL from 'axios/lib/helpers/buildURL';
        import isURLSameOrigin from 'axios/lib/helpers/isURLSameOrigin';
        import btoa from 'axios/lib/helpers/btoa';
        import cookies from 'axios/lib/helpers/cookies';
        import settle from 'axios/lib/core/settle';
        import createError from 'axios/lib/core/createError';

        然后直接使用就行了,不用再进行二次开发。

        1.3 超时设置

        我们不能无限地等待第三方服务的响应,如果第三方服务无响应或者响应时间过长,应当适时的终止掉。在 axios 中,前端使用了XMLHttpRequest,在 node 端使用了http,来实现接口的请求,两者都有超时的设定,可以设置 timeout 字段来设置超时的时间,自动取消当前的请求。

        像有的发起的请求,自己并没有超时的设定,例如 jsonp,是用创建一个 script 标签来发起的请求,这个请求必须等到服务器有响应才会终止(成功或者失败)。这时,就需要我们自己用一个setTimeout来模拟了,但这样,即使返回给业务层说“超时了,已取消当前请求”,但实际上请求还在,只不过若超过规定时间,只是不再执行对应的成功操作而已。

        1.4 主动取消请求

        我们也会有很多并没有到超时时间,就需要主动取消当前请求的场景,例如在请求返回之前就切换了路由;上次请求还没响应前,又需要发出新的请求等。都需要主动地取消当前请求。

        axios 中已经提供了取消请求的功能,我们只需要按照规则接入即可。我们来看下 XMLHttpRequest 请求器中是怎么取消请求的,在写自定义请求器时也可以照理使用。

        在lib/adapters/xhr.js#L158中:

        // 若config中已经配置了cancelToken
        if (config.cancelToken) {
            // Handle cancellation
            // 若在外城执行了取消请求的方法,则这里将当前的请求取消掉
            config.cancelToken.promise.then(function onCanceled(cancel) {
                if (!request) {
                    return;
                }
        
                // xhr中使用abort方法取消当前请求
                request.abort();
                reject(cancel);
                // Clean up request
                request = null;
            });
        }

        我们在写自己的适配器时,也可以将这段拷贝过去,将内部取消的操作更换为自己的即可。

        到这里,若把上面的功能都实现了,就已经完成了一个标准的适配器了。

        2. 编写自定义适配器

        每个人需要的适配器肯定也不一样,复杂度也不一样,例如有的想接入小程序的请求,我自己想接入客户端里提供的数据请求方式等。我们这里只是通过实现一个简单的jsonp适配器来讲解下实现方式。

        我们以 es6 的模块方式来进行开发。所有的实现均在代码中进行了讲解。

        // 这里的config是axios里所有的配置
        const jsonpAdapter = (config) => {
            return new Promise((resolve, reject) => {
                // 是否已取消当前操作
                // 因jsonp没有主动取消请求的方式
                // 这里使用 isAbort 来标识
                let isAbort = false;
        
                // 定时器标识符
                let timer = null;
        
                // 执行方法的名字,
                const callbackName = `jsonp${Date.now()}_${Math.random()
                    .toString()
                    .slice(2)}`;
        
                // 这里假设已经实现了baseURL和url的拼接方法
                const fullPath = buildFullPath(config.baseURL, config.url);
        
                // 这里假设已经实现了url和参数的拼接方法
                // 不太一样的地方在于,jsonp需要额外插入一个自己的回调方法
                const url = buildURL(
                    fullPath,
                    {
                        ...config.params,
                        ...{ [config.jsonpCallback || 'callback']: callbackName },
                    },
                    config.paramsSerializer
                );
        
                // 创建一个script标签
                let script = document.createElement('script');
        
                // 成功执行操作后
                function remove() {
                    if (script) {
                        script.onload = script.onerror = null;
        
                        // 移除script标签
                        if (script.parentNode) {
                            script.parentNode.removeChild(script);
                        }
                        // 取消定时器
                        if (timer) {
                            clearTimeout(timer);
                        }
        
                        script = null;
                    }
                }
        
                // 成功请求后
                window[callbackName] = (data) => {
                    // 若已需要请求,则不再执行
                    if (isAbort) {
                        return;
                    }
        
                    // 返回的格式
                    const response = {
                        status: 200,
                        statusText: 'ok',
                        config,
                        request: script,
                        data: data,
                    };
                    remove();
                    // 实际上这里上一个settle操作,会额外判断是否是合理的status状态
                    // 若我们在config.validateStatus中设置404是合理的,也会进入到resolve状态
                    // 但我们这里就不实现这个了
                    // settle(resolve, reject, response);
                    resolve(response);
                };
        
                // 请求失败
                script.onerror = function (error) {
                    remove();
        
                    reject(createError('Network Error', config, 404));
                };
        
                // 若设置了超时时间
                if (config.timeout) {
                    timer =【本文由:香港大带宽服务器提供】 setTimeout(function () {
                        remove();
                        // 取消当前操作
                        isAbort = true;
                        reject(
                            createError(
                                'timeout of ' + config.timeout + 'ms exceeded',
                                config,
                                405
                            )
                        );
                    }, config.timeout);
                }
        
                // 若定义了取消操作
                if (config.cancelToken) {
                    config.cancelToken.promise.then(function () {
                        if (!script) {
                            return;
                        }
                        remove();
                        isAbort = true;
        
                        reject(createError('Cancel Error', config, 404));
                    });
                }
        
                script.src = url;
                const target =
                    document.getElementsByTagName('script')[0] || document.head;
                target.parentNode && target.parentNode.insertBefore(script, target);
            });
        };
        
        export default jsonpAdapter;

        3. 将适配器添加到 axios 中

        axios 的 config 提供了 adapter 字段让我们插入自己的适配器。使用自定义适配器又有两种情况:

        1.完全只使用自定义的适配器;

        2.在某种情况下使用自定义适配器,其他情况时还是使用 axios 自己的适配器。

        第 1 种情况还好,只需要 return 自己适配器返回的结果结果即可;而第 2 种情况中,则有个小坑需要踩一下,我们这里也只讲解下第 2 种情况。我要把刚才实现的 jsonp 适配器添加到 axios 中,并且只在参数有format=jsonp时才调用该适配器,其他还是用的 axios 提供的适配器。

        import Axios from 'axios';
        import jsonpAdapter from './jsonpAdater';
        
        const request = Axios.create({
            adapter: (config) => {
                if (config?.params?.format === 'jsonp') {
                    return jsonpAdapter(config);
                }
        
                // 这里需要将config.adapter设置为空
                // 否则会造成无限循环
                return defaultAxios({ ...config, ...{ adapter: undefined } });
            },
        });

        使用自定义的适配器 jsonp 发起请求。

        // 使用自定义的适配器jsonp发起请求
        var options = {
            params: {
                format: 'jsonp',
            },
        };
        request(
            'https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336',
            options
        )
            .then(function (response) {
                console.log('jsonp response', response);
            })
            .catch(function (error) {
                console.error('jsonp error', error);
            });

        使用 axios 默认的适配器发起请求。

        // 使用axios默认的适配器发起请求
        request('https://api.prize.qq.com/v1/newsapp/answer/share/oneQ?qID=506336')
            .then(function (response) {
                console.log('axios response', response);
            })
            .catch(function (error) {
                console.error('axios error', error);
            });

        4. 总结

        这里,我们就已经实现了一个自定义适配器了,在满足一定条件时可以触发这个适配器。通过这个思路,我们也可以实现一个自定义的 mock 方法,例如当参数中包含format=mock时则调用 mock 接口,否则就正常请求。

        以上就是如何实现axios的自定义适配器adapter的详细内容,更多关于axios自定义适配器adapter的资料请关注海外IDC网其它相关文章!