Home Reference Source

src/standard-http-client.js

import axios from 'axios';
import fetchJsonp from 'fetch-jsonp';
import QsMan from 'qsman';

import isAbsoluteURL from './helpers/isAbsoluteURL.js';
import combineURLs from './helpers/combineURLs.js';

/**
 * 符合接口规范的 HTTP 客户端
 * 
 * @see https://github.com/f2e-journey/treasure/blob/master/api.md
 */
class StandardHttpClient {
    /**
     * @param {AxiosRequestConfig} config
     */
    constructor(config) {
        /**
         * @type {AxiosInstance}
         */
        this.agent = axios.create(config);
        this.useInterceptors();
    }

    /**
     * 使用拦截器
     * 
     * 子类可以继承此方法来添加自己的拦截器
     */
    useInterceptors() {
        this._hook();
        this._isResponseSuccess();
        this._descResponseError();
        this._handleError();
        this._logResponseError();
    }

    /**
     * 通过拦截器判断接口调用是否成功
     */
    _isResponseSuccess() {
        this.agent.interceptors.response.use((response) => {
            var result = response.data;
            if (this._isApiSuccess(response)) {
                return Promise.resolve([result.data, response]);
            } else {
                var message = '接口调用出错但未提供错误信息';
                if (result && result.statusInfo && result.statusInfo.message) {
                    message = result.statusInfo.message;
                }

                var error = new Error(message);
                error.response = response;
                error.request = response.request;
                error.config = response.config;

                return Promise.reject(error);
            }
        });
    }

    /**
     * 通过拦截器描述请求的错误信息
     */
    _descResponseError() {
        this.agent.interceptors.response.use(undefined, (error) => {
            // 如果 transformResponse 执行异常, 进入到拦截器做错误处理,
            // 此时的 error 是没有 config 的,
            // 因为 transformResponse 是客户端执行的逻辑, 因此认定为客户端处理出错
            if (error.config) {
                var validateStatus = error.config.validateStatus || this.agent.defaults.validateStatus;

                var response = error.response;
                if (response) { // 请求发送成功, 即前端能够拿到 HTTP 请求返回的数据
                    if (validateStatus && validateStatus(response.status)) { // HTTP 成功, 但接口调用出错
                        // 错误描述
                        error._desc = '接口调用出错';
                        // 错误分类
                        error._errorType = 'B';
                        // 错误编号
                        // 如果接口调用出错但未提供错误编号, 错误编号默认为 0
                        error._errorNumber = response.data && response.data.status ?
                                             response.data.status : 0;
                    } else { // HTTP 异常
                        error._desc = '网络请求错误';
                        error._errorType = 'H';
                        error._errorNumber = response.status;
                    }
                } else { // 请求发送失败
                    error._desc = '网络请求失败';
                    error._errorType = 'A';
                    error._errorNumber = error.message.charCodeAt(0);
                }
            } else {
                this._descClientError(error);
            }

            // 错误码
            error._errorCode = `${error._errorType}${error._errorNumber}`;

            return Promise.reject(error);
        });
    }

    /**
     * 描述客户端错误
     * 
     * @param {Error} error 
     */
    _descClientError(error) {
        error._desc = '客户端处理出错';
        error._errorType = 'C';
        error._errorNumber = error.message.charCodeAt(0);
        error._errorCode = `${error._errorType}${error._errorNumber}`;
    }

    /**
     * 通过拦截器输出请求的错误日志
     */
    _logResponseError() {
        this.agent.interceptors.response.use(undefined, function(error) {
            var method = error.config ? error.config.method : undefined;
            var url = error.config ? error.config.url : undefined;

            console.warn(`${error._desc}(${error._errorCode})`,  
                         method, url,
                         error.message,
                         error.config, error.response,
                         error);

            return Promise.reject(error);
        });
    }

    /**
     * 通过拦截器增加发送请求的 hook
     * 
     * ```
     *                     ┌─> 成功 ─> afterSend
     * beforeSend ─> send ─┤
     *                     └─> 失败 ─> afterSend
     * ```
     */
    _hook() {
        this.agent.interceptors.request.use((config) => {
            this.beforeSend(config);
            return config;
        });

        this.agent.interceptors.response.use((response) => {
            this.afterSend(response);
            return response;
        }, (error) => {
            this.afterSend(error);
            return Promise.reject(error);
        });
    }

    /**
     * 通过拦截器处理请求的错误
     */
    _handleError() {
        this.agent.interceptors.response.use(undefined, (error) => {
            try {
                this.handleError(error);
            } catch (e) {
                e.response = error.response;
                e.request = error.request;
                e.config = error.config;

                this._descClientError(e);
                throw e;
            }
            return Promise.reject(error);
        });
    }

    /**
     * 发送请求之前统一要做的事情
     * 
     * @abstract
     * @param {AxiosRequestConfig} config 
     */
    beforeSend(config) {}
    /**
     * 请求完成之后统一要做的事情
     * 
     * @abstract
     * @param {AxiosResponse | AxiosError} responseOrError 
     */
    afterSend(responseOrError) {}
    /**
     * 请求出错之后如何处理错误
     * 
     * @abstract
     * @param {AxiosError} error
     */
    handleError(error) {}

    /**
     * 发送请求
     * 
     * @param {AxiosRequestConfig} [config={}] 扩展的 AxiosRequestConfig
     * @param {object} [config._data] 实现类似 jQuery.ajax 的 data 配置项机制
     * @param {boolean} [config._jsonp] 是否通过 JSONP 来发送请求(注意此时 config 仅支持 baseURL, url, params, timeout, transformResponse 参数)
     * @param {string} [config._jsonpCallback] name of the query string parameter to specify the callback(defaults to `callback`)
     * @return {Promise}
     */
    send(config = {}) {
        this._adapterDataOption(config);

        var promise = null;
        if (config._jsonp) {
            promise = this._jsonp({
                method: 'get',
                baseURL: config.baseURL,
                url: config.url,
                params: config.params,
                timeout: config.timeout,
                transformResponse: config.transformResponse,
                _jsonpCallback: config._jsonpCallback
            });
        } else {
            promise = this._dispatchRequest(config);
        }

        return promise;
    }

    /**
     * 将 config._data 适配为 config.params 和 config.data
     * 
     * 当为 post/put/patch 请求时会将 config._data 转成 URL 编码的字符串
     * 
     * @param {AxiosRequestConfig} 扩展的 AxiosRequestConfig
     * @param {object} config._data 实现类似 jQuery.ajax 的 data 配置项机制
     */
    _adapterDataOption(config) {
        if (config._data) {
            var method = '';
            if (config.method) {
                method = config.method.toLowerCase();
            }

            // request methods 'PUT', 'POST', and 'PATCH' can send request body
            var hasRequestBodyMethods = ['put', 'post', 'patch'];
            if (hasRequestBodyMethods.indexOf(method) !== -1) {
                // 已有 config.data 时不做任何操作
                if (!config.data) {
                    config.data = typeof config._data === 'object' ?
                                  new QsMan().append(config._data).toString() : config._data;
                }
            } else {
                // 已有 config.params 时不做任何操作
                config.params = config.params || config._data;
            }
        }
    }

    /**
     * 通过 JSONP 发送请求
     * 
     * @param {object} config
     * @param {string} config.url
     * @param {string} [config.baseURL]
     * @param {object} [config.params]
     * @param {number} [config.timeout]
     * @param {Array<Function>} [config.transformResponse]
     * @param {string} [config._jsonpCallback]
     * @return {Promise}
     */
    _jsonp(config) {
        // Support baseURL config
        var baseURL = config.baseURL || this.agent.defaults.baseURL;
        if (baseURL && !isAbsoluteURL(config.url)) {
            config.url = combineURLs(baseURL, config.url);
        }

        var url = config.url;
        if (config.params) {
            url = new QsMan(url).append(config.params).toString();
        }

        if (!config.timeout) {
            config.timeout = this.agent.defaults.timeout;
        }

        var transformResponse = config.transformResponse || this.agent.defaults.transformResponse;

        var promise = fetchJsonp(url, {
            timeout: config.timeout,
            jsonpCallback: config._jsonpCallback
        }).then(function(response) {
            return response.json();
        }).then(function(data) {
            // Support transformResponse config
            var _data = data;
            if (transformResponse && Object.prototype.toString.call(transformResponse) === '[object Array]') {
                transformResponse.forEach(function(fn) {
                    _data = fn(_data);
                });
            }

            return Promise.resolve({ // 返回 AxiosResponse
                data: _data,
                status: 200,
                statusText: 'OK',
                headers: {},
                config: config,
                request: 'script'
            });
        }).catch(function(error) { // 返回 AxiosError
            error.request = 'script';
            error.response = null;
            error.config = config;
            return Promise.reject(error);
        });

        // 兼容 axios 的 Interceptors 机制
        // https://github.com/axios/axios/blob/master/lib/core/Axios.js
        // 首先放入发送请求的 promise
        var chain = [promise, undefined];
        // 在链路的前面加入 intercept request
        this.agent.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
            chain.unshift(interceptor.fulfilled, interceptor.rejected);
        });
        // 在链路的后面加入 intercept response
        this.agent.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
            chain.push(interceptor.fulfilled, interceptor.rejected);
        });
        while (chain.length) {
            promise = promise.then(chain.shift(), chain.shift());
        }

        return promise;
    }

    /**
     * Dispatch a request to the server.
     * 
     * @param {AxiosRequestConfig} config
     * @return {Promise}
     */
    _dispatchRequest(config) {
        // axios promise 链: [...interceptors.request, dispatch request, ...interceptors.response]
        return this.agent(config);
    }

    /**
     * 判断接口调用是否成功
     * 
     * @param {AxiosResponse} response
     * @return {boolean}
     */
    _isApiSuccess(response) {
        var result = response.data;
        // 判断接口调用是否成功的依据
        // 1. 返回的数据应该是一个 object
        // 2. 要么没有 status 字段, 要么 status 字段等于 0
        return typeof result === 'object'
            && (typeof result.status === 'undefined' || result.status === 0);
    }
}

export default StandardHttpClient;