Axios实现

452 阅读1分钟

该版本axios包含官方axios整体实现流程,实现了拦截器,取消请求,浏览器发送get,post请求等功能,该html包含连个js脚本,第一个脚本负责实现axios(全代码注释),第二个脚本负责测试axios,服务器的话需要自己实现,推荐json-server(github),应该叫这个名字,30s搭建一个服务器(readme有说如何使用json-server),直接使用。

<!DOCTYPE html>
<html lang="en">

<head>
</head>

<body>
    <button class="btn" id='addPost'>post</button>
    <!-- 此脚本实现基础axios,包括拦截器,取消请求,axios(),axios.request(),axios.get(),axios.post()四种请求方式 -->
    <script>
        // Interceptor: 拦截器构造函数,this.handles保存拦截器成功失败回调
        function Interceptor() {
            this.handles = []
        }
        // Interceptor.prototype.use:将拦截器成功失败回调推入this.handles
        Interceptor.prototype.use = function (successCallback, errorCallback) {
            this.handles.push({ successCallback, errorCallback })
        }

        // Axios:axios构造函数
        // this.defaults保存默认axios网络请求的配置(比如默认请求方式get)
        // this.interceptors保存request与response拦截器对象(添加拦截器的操作: axios.interceptors.request.use(success,errCallback))
        function Axios(defaultConfig) {
            this.defaults = defaultConfig
            this.interceptors = {
                request: new Interceptor,
                response: new Interceptor
            }
        }
        // Axios.prototype.request:axios.request请求方式实现
        // 其实axios不同的请求发起方式(axios(),axios.request(),axios.get()...) 实际上最终都是通过调用Axios.prototype.request实现的
        Axios.prototype.request = function (config) {
            // 将使用者传入的网络请求配置对象与默认配置合并
            config = Object.assign(this.defaults, config)
            // 创建一个promise对象,value为合并后的配置config
            let promise = Promise.resolve(config)
            // 创建一个promise.then的函数执行链 即未来 promise.then(dispatchRequest, undefined)
            let chain = [dispatchRequest, undefined]
            // 将请求响应拦截器加入到chain中,位于chain中不同位置对应不同执行时机
            this.interceptors.request.handles.forEach(({ successCallback, errorCallback }) => {
                chain.unshift(successCallback, errorCallback)
            })
            this.interceptors.response.handles.forEach(({ successCallback, errorCallback }) => {
                chain.push(successCallback, errorCallback)
            })
            // promise.then(chain.shift(), chain.shift()) 形式依次执行请求拦截器,请求发送,响应拦截器
            while (chain.length) {
                promise = promise.then(chain.shift(), chain.shift())
            }
            // 返回最终经由请求拦截器,请求发送,响应拦截器 所获得最终服务器返回数据
            return promise
        }
        // Axios.prototype.get:axios.get请求方式实现
        Axios.prototype.get = function (config) {
            return this.request(Object.assign(config, { method: 'get' }))
        }
        // Axios.prototype.post:axios.post请求方式实现
        Axios.prototype.post = function (config) {
            return this.request(Object.assign(config, { method: 'post' }))
        }
        // Axios.prototype.CancelToken:axios取消请求实现
        // CancelToken基本使用: 作为一个构造函数,传入一个函数,该函数接受一个为函数的参数,执行该函数即可取消当前网络请求,即 new CancelToken(c=>window.cancel = c),window.cancel()即可取消当前网络请求
        // axios取消请求实现原理:new CancelToken将创建一个promise,promise状态改变通过传入的函数交给外界,外界执行该函数(window.cancel())即可改变构造函数内部promise的状态
        // 而promise状态最终会关联到网络请求取消与否上,即promise.then(()=>{xhr.abort()}),所以一旦外界改变promise状态,promise.then执行,网络请求取消,而只要外界不改变promise状态,promise.then就不会执行,网络请求就不会取消
        // 这样就实现了由使用者控制何时进行网络请求的取消
        Axios.prototype.CancelToken = function (excutor) {
            let promiseResolve
            this.promise = new Promise(resolve => promiseResolve = resolve)
            excutor(() => promiseResolve())
        }

        // dispatchRequest:axios可以再浏览器环境与node环境中发送请求,因为浏览器使用的是xhr,node使用http,所以dispatchRequest在这里做个区分使用哪种方式发送网络请求
        // 这里只实现浏览器网络请求发送,node请求暂未实现
        function dispatchRequest(config) {
            // 直接调用xhrAdapter发送网络请求
            return xhrAdapter(config).then(
                response => response,
                err => { throw (err) }
            )
        }

        // xhrAdapter:原生浏览器网络请求封装成Promise形式
        function xhrAdapter(config) {
            return new Promise((resolve, reject) => {
                const xhr = window.XMLHttpRequest ? new XMLHttpRequest : new ActiveXObject
                //  xhr.onreadystatechange:监听网络请求状态,合适的实际resolve/reject当前Promise
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
                            // 将响应成功结果,处理成axios返回的数据格式
                            resolve({
                                config: config,
                                data: JSON.parse(xhr.response),
                                status: xhr.status,
                                statusText: xhr.statusText,
                                request: xhr,
                                headers: xhr.getAllResponseHeaders()
                            })
                        } else {
                            reject(new Error(`请求失败 ${xhr.status}`))
                        }
                    }
                }
                // 准备一个xhr请求,等待发送
                xhr.open(config.method, config.url)
                // 在请求发送前处理设置好请求头
                Object.entries(config.headers).forEach(([key, value]) => {
                    xhr.setRequestHeader(key, value)
                })
                // 请求发送
                xhr.send(JSON.stringify(config.data))
                // 实现axios取消请求
                config.cancelToken && config.cancelToken.promise.then(() => xhr.abort())
            })
        }

        // createAxios:创建axios实例的函数,因为axios需要实现axios(),axios.request(),axios.get()等方式发起请求
        // 仅仅new Axios()返回的axios实例并不支持axios()方式发起请求,所以该函数目的即创建一个可以使用多种请求方式的axios实例
        function createAxios(defaultConfig) {
            // 创建Axios实例
            let axiosInstance = new Axios(defaultConfig)
            // 创建axios,此时axios()请求即可实现,但是axios.request()/axios.get()等方式还未实现(注意维持this指向)
            let axios = Axios.prototype.request.bind(axiosInstance)
            // 实现axios.request()/axios.get()请求方式,原理很简单,将request、get直接挂到axios函数对象上即可(注意维持this指向)
            Object.keys(Axios.prototype).forEach(key => axios[key] = Axios.prototype[key].bind(axiosInstance))
            // 实现axios.defaults获取默认配置对象,axios.interceptors获取拦截器对象,同上,直接就将Axios实例axiosInstance中这些数据拷贝到axios函数对象上即可
            Object.keys(axiosInstance).forEach(key => axios[key] = axiosInstance[key])
            // 返回缝合后的axios
            return axios
        }

        // defaultConfig: 默认网络请求配置,这里仅简单配置了默认请求方式get,以及两个请求头字段,源码中该默认请求配置更多
        let defaultConfig = {
            "method": "get",
            "headers": {
                "Accept": "application/json,text/plain,*/*",
                "Content-Type": "application/json;charset=utf-8"
            }
        }
        // 将axios挂载到全局对象上,这里我们只实现浏览器的axios,所以挂到window上,这样引入该文件,即可直接使用axios
        window.axios = createAxios(defaultConfig)
    </script>
    <!-- 此脚本可测试实现的axios,如果期望快速搭建一个测试服务器(in 30s),推荐json-server,readme写的很清楚这家伙如何使用-->
    <script>
        // 1,测试请求与响应拦截器功能
        axios.interceptors.request.use(
            config => { console.log('请求拦截器1'); return config },
            err => Promise.reject(err)
        )
        axios.interceptors.request.use(
            config => { console.log('请求拦截器2'); return config },
            err => Promise.reject(err)
        )

        axios.interceptors.response.use(
            res => { console.log('响应拦截器1'); return res },
            err => Promise.reject(err)
        )
        axios.interceptors.response.use(
            res => { console.log('响应拦截器2'); return res },
            err => Promise.reject(err)
        )
        // 2,测试取消请求功能(连续点击button即可取消上一次网络请求,当然网络请求请确保有点时延)
        let cancel = null
        // 3,测试axios进行网络请求(点击按钮发送网络请求)
        addPost.addEventListener('click', function () {
            cancel && cancel()
            // 4,当然这里也可以换成axios.request、axios.post形式测试
            axios({
                method: 'post',
                url: 'http://localhost:3000/posts',
                data: { title: "json-server-01", author: "axios" },
                cancelToken: new axios.CancelToken(c => cancel = c)
            }).then(res => console.log(res))
        })

    </script>
</body>

</html>