├── .gitignore ├── src ├── service │ ├── file.js │ ├── mock.js │ └── index.js └── utils │ ├── debounce.js │ ├── https.js │ └── https.ts ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /src/service/file.js: -------------------------------------------------------------------------------- 1 | import { callApi } from '../utils/https' 2 | 3 | export const mockGetQuery = () => 4 | callApi({ 5 | url: 'file/upload', 6 | contentType: 'multipart', 7 | }) 8 | -------------------------------------------------------------------------------- /src/service/mock.js: -------------------------------------------------------------------------------- 1 | import { callApi } from '../utils/https' 2 | 3 | export const mockGetQuery = () => 4 | callApi({ 5 | url: 'mock/getQuery', 6 | prefixUrl: 'api1', 7 | }) 8 | 9 | export const mockPostDel = (data) => 10 | callApi({ 11 | url: 'mock/postDel', 12 | data, 13 | method: 'post', 14 | prefixUrl: 'api1', 15 | }) 16 | 17 | export const mockPostAdd = (data) => 18 | callApi({ 19 | url: 'mock/postAdd', 20 | data, 21 | method: 'post', 22 | contentType: 'urlencoded', 23 | prefixUrl: 'api1', 24 | }) 25 | -------------------------------------------------------------------------------- /src/service/index.js: -------------------------------------------------------------------------------- 1 | import { callApi } from '../utils/https' 2 | 3 | export * from './mock' 4 | export * from './file' 5 | 6 | // get请求带参数 7 | export const getQuery = (data) => 8 | callApi({ 9 | url: 'admin/getQuery', 10 | data, 11 | }) 12 | 13 | export const postDel = (data) => 14 | callApi({ 15 | url: 'admin/postDel', 16 | data, 17 | method: 'post', 18 | }) 19 | 20 | export const postAdd = (data) => 21 | callApi({ 22 | url: 'admin/postAdd', 23 | data, 24 | method: 'post', 25 | contentType: 'urlencoded', 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export const debounce = (func, timeout, immediate) => { 2 | let timer 3 | 4 | return function () { 5 | let context = this 6 | let args = arguments 7 | 8 | if (timer) clearTimeout(timer) 9 | if (immediate) { 10 | var callNow = !timer 11 | timer = setTimeout(() => { 12 | timer = null 13 | }, timeout) 14 | if (callNow) func.apply(context, args) 15 | } else { 16 | timer = setTimeout(function () { 17 | func.apply(context, args) 18 | }, timeout) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios-ajax", 3 | "version": "1.0.0", 4 | "description": "基于axios二次封装,更友好的在浏览器发送ajax请求", 5 | "main": "https.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/zxyue25/axios-ajax.git" 12 | }, 13 | "keywords": [ 14 | "axios", 15 | "http" 16 | ], 17 | "author": "zxyue25", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/zxyue25/axios-ajax/issues" 21 | }, 22 | "homepage": "https://github.com/zxyue25/axios-ajax#readme", 23 | "dependencies": { 24 | "axios": "^0.21.1", 25 | "qs": "^6.10.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/https.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import qs from 'qs' 3 | import { debounce } from './debounce' 4 | 5 | const contentTypes = { 6 | json: 'application/json; charset=utf-8', 7 | urlencoded: 'application/x-www-form-urlencoded; charset=utf-8', 8 | multipart: 'multipart/form-data', 9 | } 10 | 11 | function toastMsg() { 12 | Object.keys(errorMsgObj).map((item) => { 13 | Message.error(item) 14 | delete errorMsgObj[item] 15 | }) 16 | } 17 | 18 | let errorMsgObj = {} 19 | 20 | const defaultOptions = { 21 | withCredentials: true, // 允许把cookie传递到后台 22 | headers: { 23 | Accept: 'application/json', 24 | 'Content-Type': contentTypes.json, 25 | }, 26 | timeout: 15000, 27 | } 28 | 29 | export const callApi = ({ 30 | url, 31 | data = {}, 32 | method = 'get', 33 | options = {}, 34 | contentType = 'json', // json || urlencoded || multipart 35 | prefixUrl = 'api', 36 | }) => { 37 | if (!url) { 38 | const error = new Error('请传入url') 39 | return Promise.reject(error) 40 | } 41 | const fullUrl = `/${prefixUrl}/${url}` 42 | 43 | const newOptions = { 44 | ...defaultOptions, 45 | ...options, 46 | headers: { 47 | 'Content-Type': 48 | (options.headers && options.headers['Content-Type']) || 49 | contentTypes[contentType], 50 | }, 51 | method, 52 | } 53 | if (method === 'get') { 54 | newOptions.params = data 55 | } 56 | 57 | if (method !== 'get' && method !== 'head') { 58 | newOptions.data = data 59 | if (data instanceof FormData) { 60 | newOptions.headers = { 61 | 'x-requested-with': 'XMLHttpRequest', 62 | 'cache-control': 'no-cache', 63 | } 64 | } else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) { 65 | newOptions.data = qs.stringify(data) 66 | } else { 67 | Object.keys(data).forEach((item) => { 68 | if ( 69 | data[item] === null || 70 | data[item] === undefined || 71 | data[item] === '' 72 | ) { 73 | delete data[item] 74 | } 75 | }) 76 | // 没有必要,因为axios会将JavaScript对象序列化为JSON 77 | // newOptions.data = JSON.stringify(data); 78 | } 79 | } 80 | 81 | axios.interceptors.request.use((request) => { 82 | // 移除起始部分 / 所有请求url走相对路径 83 | request.url = request.url.replace(/^\//, '') 84 | return request 85 | }) 86 | 87 | return axios({ 88 | url: fullUrl, 89 | ...newOptions, 90 | }) 91 | .then((response) => { 92 | const { data } = response 93 | if (data.code === 'xxx') { 94 | // 与服务端约定 95 | // 登录校验失败 96 | } else if (data.code === 'xxx') { 97 | // 与服务端约定 98 | // 无权限 99 | router.replace({ path: '/403' }) 100 | } else if (data.code === 'xxx') { 101 | // 与服务端约定 102 | return Promise.resolve(data) 103 | } else { 104 | const { message } = data 105 | if (!errorMsgObj[message]) { 106 | errorMsgObj[message] = message 107 | } 108 | setTimeout(debounce(toastMsg, 1000, true), 1000) 109 | return Promise.reject(data) 110 | } 111 | }) 112 | .catch((error) => { 113 | if (error.response) { 114 | const { data } = error.response 115 | const resCode = data.status 116 | const resMsg = data.message || '服务异常' 117 | // if (resCode === 401) { // 与服务端约定 118 | // // 登录校验失败 119 | // } else if (data.code === 403) { // 与服务端约定 120 | // // 无权限 121 | // router.replace({ path: '/403' }) 122 | // } 123 | if (!errorMsgObj[resMsg]) { 124 | errorMsgObj[resMsg] = resMsg 125 | } 126 | setTimeout(debounce(toastMsg, 1000, true), 1000) 127 | const err = { code: resCode, respMsg: resMsg } 128 | return Promise.reject(err) 129 | } else { 130 | const err = { type: 'canceled', respMsg: '数据请求超时' } 131 | return Promise.reject(err) 132 | } 133 | }) 134 | } 135 | -------------------------------------------------------------------------------- /src/utils/https.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import qs from 'qs' 3 | import { debounce } from './debounce' 4 | 5 | type OptionParams = { 6 | url: string, 7 | method?: 'OPTIONS' | 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | 'CONNECT', 8 | data?: object, 9 | contentType?: 'json' | 'urlencoded' | 'multipart', 10 | prefixUrl?: string, 11 | options?: any, 12 | } 13 | 14 | const contentTypes = { 15 | json: 'application/json; charset=utf-8', 16 | urlencoded: 'application/x-www-form-urlencoded; charset=utf-8', 17 | multipart: 'multipart/form-data', 18 | } 19 | 20 | function toastMsg() { 21 | Object.keys(errorMsgObj).map((item) => { 22 | // Message.error(item) 23 | delete errorMsgObj[item] 24 | }) 25 | } 26 | 27 | let errorMsgObj = {} 28 | 29 | const defaultOptions = { 30 | withCredentials: true, // 允许把cookie传递到后台 31 | headers: { 32 | Accept: 'application/json', 33 | 'Content-Type': contentTypes.json, 34 | }, 35 | timeout: 15000, 36 | } 37 | 38 | export const callApi = ({ 39 | url, 40 | data = {}, 41 | method = 'GET', 42 | options = {}, 43 | contentType = 'json', // json || urlencoded || multipart 44 | prefixUrl = 'api', 45 | }: OptionParams) => { 46 | if (!url) { 47 | const error = new Error('请传入url') 48 | return Promise.reject(error) 49 | } 50 | const fullUrl = `/${prefixUrl}/${url}` 51 | 52 | const newOptions = { 53 | ...defaultOptions, 54 | ...options, 55 | headers: { 56 | 'Content-Type': 57 | (options.headers && options.headers['Content-Type']) || 58 | contentTypes[contentType], 59 | }, 60 | method, 61 | } 62 | if (method === 'GET') { 63 | newOptions.params = data 64 | } 65 | 66 | if (method !== 'GET' && method !== 'HEAD') { 67 | newOptions.data = data 68 | if (data instanceof FormData) { 69 | newOptions.headers = { 70 | 'x-requested-with': 'XMLHttpRequest', 71 | 'cache-control': 'no-cache', 72 | } 73 | } else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) { 74 | newOptions.data = qs.stringify(data) 75 | } else { 76 | Object.keys(data).forEach((item) => { 77 | if ( 78 | data[item] === null || 79 | data[item] === undefined || 80 | data[item] === '' 81 | ) { 82 | delete data[item] 83 | } 84 | }) 85 | // 没有必要,因为axios会将JavaScript对象序列化为JSON 86 | // newOptions.data = JSON.stringify(data); 87 | } 88 | } 89 | 90 | axios.interceptors.request.use((request) => { 91 | // 移除起始部分 / 所有请求url走相对路径 92 | request.url = request.url.replace(/^\//, '') 93 | return request 94 | }) 95 | 96 | return axios({ 97 | url: fullUrl, 98 | ...newOptions, 99 | }) 100 | .then((response) => { 101 | const { data } = response 102 | if (data.code === 'xxx') { 103 | // 与服务端约定 104 | // 登录校验失败 105 | } else if (data.code === 'xxx') { 106 | // 与服务端约定 107 | // 无权限 108 | // router.replace({ path: '/403' }) 109 | } else if (data.code === 'xxx') { 110 | // 与服务端约定 111 | return Promise.resolve(data) 112 | } else { 113 | const { message } = data 114 | if (!errorMsgObj[message]) { 115 | errorMsgObj[message] = message 116 | } 117 | setTimeout(debounce(toastMsg, 1000, true), 1000) 118 | return Promise.reject(data) 119 | } 120 | }) 121 | .catch((error) => { 122 | if (error.response) { 123 | const { data } = error.response 124 | const resCode = data.status 125 | const resMsg = data.message || '服务异常' 126 | // if (resCode === 401) { // 与服务端约定 127 | // // 登录校验失败 128 | // } else if (data.code === 403) { // 与服务端约定 129 | // // 无权限 130 | // router.replace({ path: '/403' }) 131 | // } 132 | if (!errorMsgObj[resMsg]) { 133 | errorMsgObj[resMsg] = resMsg 134 | } 135 | setTimeout(debounce(toastMsg, 1000, true), 1000) 136 | const err = { code: resCode, respMsg: resMsg } 137 | return Promise.reject(err) 138 | } else { 139 | const err = { type: 'canceled', respMsg: '数据请求超时' } 140 | return Promise.reject(err) 141 | } 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axios-ajax 2 | 3 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/602a1c2d4dd441979f6614f259038868~tplv-k3u1fbpfcp-watermark.image?) 4 | ## 一、什么是axios,有什么特性 5 | 6 | #### 描述 7 | axios是一个基于`promise`的`HTTP`库,可以用在`浏览器`或者`node.js`中。本文围绕XHR。 8 | > axios提供两个http请求适配器,XHR和HTTP。XHR的核心是浏览器端的XMLHttpRequest对象;HTTP的核心是node的http.request方法。 9 | 10 | **特性**: 11 | - 从浏览器中创建XMLHttpRequests 12 | - 从node.js创建http请求 13 | - 支持promise API 14 | - 拦截请求与响应 15 | - 转换请求数据与响应数据 16 | - 取消请求 17 | - 自动转换JSON数据 18 | - 客户端支持防御XSRF 19 | #### 背景 20 | 自`Vue`2.0起,尤大宣布取消对 `vue-resource` 的官方推荐,转而推荐 `axios`。现在 `axios` 已经成为大部分 `Vue` 开发者的首选,目前在github上有87.3k star。`axios`的熟练使用和基本封装也成为了vue技术栈系列必不可少的一部分。如果你还不了解axios,建议先熟悉 21 | [axios官网文档](https://axios-http.com/docs/intro)。 22 | 23 | #### 基本使用 24 | 25 | 安装 26 | ```shell 27 | npm install axios -S 28 | ``` 29 | 使用 30 | ```javascript 31 | import axios from 'axios' 32 | // 为给定ID的user创建请求 33 | axios.get('/user?ID=12345') 34 | .then(function (response) { 35 | console.log(response); 36 | }) 37 | .catch(function (error) { 38 | console.log(error); 39 | }); 40 | // 上面的请求也可以这样做 41 | axios.get('/user', { 42 | params: {ID: 12345}}) 43 | .then(function (response) { 44 | console.log(response); 45 | }) 46 | .catch(function (error) { 47 | console.log(error); 48 | }); 49 | ``` 50 | ## 二、Vue项目中为什么要封装axios 51 | `axios`的API很友好,可以在项目中直接使用。但是在大型项目中,http请求很多,且需要区分环境, 52 | 每个网络请求有相似需要处理的部分,如下,会导致代码冗余,破坏工程的`可维护性`,`扩展性` 53 | ```js 54 | axios('http://www.kaifa.com/data', { 55 | // 配置代码 56 | method: 'GET', 57 | timeout: 3000, 58 | withCredentials: true, 59 | headers: { 60 | 'Content-Type': 'application/json' 61 | }, 62 | // 其他请求配置... 63 | }) 64 | .then((data) => { 65 | // todo: 真正业务逻辑代码 66 | console.log(data); 67 | }, (err) => { 68 | // 错误处理代码 69 | if (err.response.status === 401) { 70 | // handle authorization error 71 | } 72 | if (err.response.status === 403) { 73 | // handle server forbidden error 74 | } 75 | // 其他错误处理..... 76 | console.log(err); 77 | }); 78 | ``` 79 | 80 | - 环境区分 81 | - 请求头信息 82 | - 请求类型 83 | - 请求超时时间 84 | - timeout: 3000 85 | - 允许携带cookie 86 | - withCredentials: true 87 | - 响应结果处理 88 | - 登录校验失败 89 | - 无权限 90 | - 成功 91 | - ... 92 | 93 | ## 三、Vue项目中如何封装axios 94 | axios文件封装在目录`src/utils/https.js`,对外暴露`callApi`函数 95 | #### 1、环境区分 96 | `callApi`函数暴露`prefixUrl`参数,用来配置api url`前缀`,默认值为`api` 97 | ```js 98 | // src/utils/https.js 99 | import axios from 'axios' 100 | 101 | export const callApi = ({ 102 | url, 103 | ... 104 | prefixUrl = 'api' 105 | }) => { 106 | if (!url) { 107 | const error = new Error('请传入url') 108 | return Promise.reject(error) 109 | } 110 | const fullUrl = `/${prefixUrl}/${url}` 111 | 112 | ... 113 | 114 | return axios({ 115 | url: fullUrl, 116 | ... 117 | }) 118 | } 119 | ``` 120 | 121 | 看到这里大家可能会问,为什么不用axios提供的配置参数`baseURL`,原因是`baseURL`会给每个接口都加上对应前缀,而项目实际场景中,存在一个前端工程,对应多个`服务`的场景。需要通过不用的前缀代理到不同的服务,`baseURL`虽然能实现,但是需要二级前缀,不优雅,且在使用的时候看不到真实的api地址是啥,因为代理前缀跟真实地址混合在一起了 122 | 123 | 使用`baseURL`,效果如下 124 | 125 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d55ea16e20694bb4ad68ee506c830007~tplv-k3u1fbpfcp-watermark.image) 126 | 127 | 函数设置prefixUrl参数,效果如下 128 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/613abcca4b2b4a7095974f6748072671~tplv-k3u1fbpfcp-watermark.image) 129 | 130 | 利用`环境变量`及`webpack代理`(这里用vuecli3配置)来作判断,用来区分开发、测试环境。生产环境同理配置`nginx`代理 131 | ```js 132 | // vue.config.js 133 | const targetApi1 = process.env.NODE_ENV === 'development' ? "http://www.kaifa1.com" : "http://www.ceshi1.com" 134 | 135 | const targetApi2 = process.env.NODE_ENV === 'development' ? "http://www.kaifa2.com" : "http://www.ceshi2.com" 136 | module.exports = { 137 | devServer: { 138 | proxy: { 139 | '/api1': { 140 | target: targetApi1, 141 | changeOrigin: true, 142 | pathRewrite: { 143 | '/api1': "" 144 | } 145 | }, 146 | '/api2': { 147 | target: targetApi2, 148 | changeOrigin: true, 149 | pathRewrite: { 150 | '/api2': "" 151 | } 152 | }, 153 | } 154 | } 155 | } 156 | ``` 157 | #### 2、请求头 158 | 常见以下三种 159 | 160 | **(1)application/json** 161 | 162 | 参数会直接放在请求体中,以JSON格式的发送到后端。这也是axios请求的默认方式。这种类型使用最为广泛。 163 | 164 | ![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/10a319a7114d4f8b96c7cd6ee6f480d6~tplv-k3u1fbpfcp-zoom-1.image "image") 165 | 166 | **(2)application/x-www-form-urlencoded** 167 | 168 | 请求体中的数据会以普通表单形式(键值对)发送到后端。 169 | 170 | ![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59702b0ce1744f8e8eaca3482aebbd94~tplv-k3u1fbpfcp-zoom-1.image "image") 171 | 172 | **(3)multipart/form-data** 173 | 174 | 参数会在请求体中,以标签为单元,用分隔符(可以自定义的boundary)分开。既可以上传键值对,也可以上传文件。通常被用来上传文件的格式。 175 | 176 | ![image](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/704ca7c37dca4a9083ed5680e2b13b3c~tplv-k3u1fbpfcp-zoom-1.image "image") 177 | `callApi`函数暴露`contentType`参数,用来配置`请求头`,默认值为`application/json; charset=utf-8` 178 | 179 | 看到这里大家可以会疑惑,直接通过`options`配置`headers`不可以嘛,答案是可以的,可以看到`newOptions`的取值顺序,先取默认值,再取配置的`options`,最后取`contentType`,`contentType`能满足绝大部分场景,满足不了的场景下可用`options`配置 180 | 181 | 通过`options`配置`headers`,写n遍`headers: {'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8'}`;而通过`contentType`配置,传参`json || urlencoded || multipart`即可 182 | 183 | 当`contentType` === `urlencoded`时,`qs.stringify(data)` 184 | 185 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/109cab9228dd4aa39d7c521e14ae817d~tplv-k3u1fbpfcp-watermark.image) 186 | ```js 187 | // src/utils/https.js 188 | import axios from 'axios' 189 | import qs from 'qs' 190 | 191 | const contentTypes = { 192 | json: 'application/json; charset=utf-8', 193 | urlencoded: 'application/x-www-form-urlencoded; charset=utf-8', 194 | multipart: 'multipart/form-data', 195 | } 196 | 197 | const defaultOptions = { 198 | headers: { 199 | Accept: 'application/json', 200 | 'Content-Type': contentTypes.json, 201 | } 202 | } 203 | 204 | export const callApi = ({ 205 | url, 206 | data = {}, 207 | options = {}, 208 | contentType = 'json', // json || urlencoded || multipart 209 | prefixUrl = 'api' 210 | }) => { 211 | 212 | ... 213 | 214 | const newOptions = { 215 | ...defaultOptions, 216 | ...options, 217 | headers: { 218 | 'Content-Type': options.headers && options.headers['Content-Type'] || contentTypes[contentType], 219 | }, 220 | } 221 | 222 | const { method } = newOptions 223 | 224 | if (method !== 'get' && method !== 'head') { 225 | if (data instanceof FormData) { 226 | newOptions.data = data 227 | newOptions.headers = { 228 | 'x-requested-with': 'XMLHttpRequest', 229 | 'cache-control': 'no-cache', 230 | } 231 | } else if (options.headers['Content-Type'] === contentTypes.urlencoded) { 232 | newOptions.data = qs.stringify(data) 233 | } else { 234 | Object.keys(data).forEach((item) => { 235 | if ( 236 | data[item] === null || 237 | data[item] === undefined || 238 | data[item] === '' 239 | ) { 240 | delete data[item] 241 | } 242 | }) 243 | // 没有必要,因为axios会将JavaScript对象序列化为JSON 244 | // newOptions.data = JSON.stringify(data); 245 | } 246 | } 247 | 248 | return axios({ 249 | url: fullUrl, 250 | ...newOptions, 251 | }) 252 | } 253 | ``` 254 | 注意,在`application/json`格式下,JSON.stringify处理传参没有意义,因为axios会将JavaScript对象序列化为JSON,也就说无论你转不转化都是JSON 255 | #### 3、请求类型 256 | 请求类型参数为`axios`的`options`的`method`字段,传入对应的请求类型如`post`、`get`等即可 257 | 258 | 不封装,使用原生`axios`时,`发送带参数的get请求`如下: 259 | ```js 260 | // src/service/index.js 261 | import { callApi } from '@/utils/https'; 262 | 263 | export const delFile = (params) => callApi({ 264 | url: `file/delete?systemName=${params.systemName}&menuId=${params.menuId}&appSign=${params.appSign}`, 265 | option: { 266 | method: 'get', 267 | }, 268 | }); 269 | 270 | // 或者 271 | export const delFile = (params) => callApi({ 272 | url: 'file/delete', 273 | option: { 274 | method: 'get', 275 | params 276 | }, 277 | }); 278 | ``` 279 | 官方文档如下 280 | 281 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b45043c4691a40e6b00764d2534430e8~tplv-k3u1fbpfcp-watermark.image) 282 | 283 | `callApi`函数暴露`method`参数,用来配置`请求类型`,默认值为`get` 284 | 285 | 当请求类型为`get`时,将`callApi`函数暴露的`data`参数,设置为`options.params`,从而参数自动拼接到url地址之后 286 | 287 | ```js 288 | // src/utils/https.js 289 | import axios from 'axios' 290 | 291 | export const callApi = ({ 292 | url, 293 | data = {}, 294 | method = 'get', 295 | options = {}, 296 | ... 297 | prefixUrl = 'api' 298 | }) => { 299 | ... 300 | const newOptions = { 301 | ..., 302 | ...options, 303 | method 304 | } 305 | ... 306 | if(method === 'get'){ 307 | newOptions.params = data 308 | } 309 | ... 310 | 311 | return axios({ 312 | url: fullUrl, 313 | ...newOptions, 314 | }) 315 | } 316 | ``` 317 | #### 4、请求超时时间 318 | ```js 319 | // src/utils/https.js 320 | const defaultOptions = { 321 | timeout: 15000, 322 | } 323 | ``` 324 | #### 5、允许携带cookie 325 | ```js 326 | // src/utils/https.js 327 | const defaultOptions = { 328 | withCredentials: true, 329 | } 330 | ``` 331 | #### 6、响应结果处理 332 | 通过`.then`、`.catch()`处理 333 | 334 | 这块需要跟服务端约定`接口响应全局码`,从而统一处理`登录校验失败`,`无权限`,`成功`等结果 335 | 336 | 比如有些服务端对于`登录校验失败`,`无权限`,`成功`等返回的响应码都是200,在响应体内返回的状态码分别是20001,20002,10000,在`then()`中处理 337 | 338 | 比如有些服务端对于`登录校验失败`,`无权限`,`成功`响应码返回401,403,200,在`catch()`中处理 339 | ```js 340 | // src/utils/https.js 341 | import axios from 'axios' 342 | import { Message } from "element-ui"; 343 | 344 | export const callApi = ({ 345 | ... 346 | }) => { 347 | 348 | ... 349 | 350 | return axios({ 351 | url: fullUrl, 352 | ...newOptions, 353 | }) 354 | .then((response) => { 355 | const { data } = response 356 | if (data.code === 'xxx') { 357 | // 与服务端约定 358 | // 登录校验失败 359 | } else if (data.code === 'xxx') { 360 | // 与服务端约定 361 | // 无权限 362 | router.replace({ path: '/403' }) 363 | } else if (data.code === 'xxx') { 364 | // 与服务端约定 365 | return Promise.resolve(data) 366 | } else { 367 | const { message } = data 368 | if (!errorMsgObj[message]) { 369 | errorMsgObj[message] = message 370 | } 371 | setTimeout(debounce(toastMsg, 1000, true), 1000) 372 | return Promise.reject(data) 373 | } 374 | }) 375 | .catch((error) => { 376 | if (error.response) { 377 | const { data } = error.response 378 | const resCode = data.status 379 | const resMsg = data.message || '服务异常' 380 | // if (resCode === 401) { // 与服务端约定 381 | // // 登录校验失败 382 | // } else if (data.code === 403) { // 与服务端约定 383 | // // 无权限 384 | // router.replace({ path: '/403' }) 385 | // } 386 | if (!errorMsgObj[resMsg]) { 387 | errorMsgObj[resMsg] = resMsg 388 | } 389 | setTimeout(debounce(toastMsg, 1000, true), 1000) 390 | const err = { code: resCode, respMsg: resMsg } 391 | return Promise.reject(err) 392 | } else { 393 | const err = { type: 'canceled', respMsg: '数据请求超时' } 394 | return Promise.reject(err) 395 | } 396 | }) 397 | } 398 | ``` 399 | 上述方案在`Message.error(xx)`时,当多个接口返回的错误信息一致时,会存在`重复提示`的问题,如下图 400 | 401 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5dfffc98a241489d8d06470adb0a94a6~tplv-k3u1fbpfcp-watermark.image) 402 | 403 | 优化方案,利用`防抖`,实现错误提示一次,更优雅 404 | 405 | 406 | ## 四、完整封装及具体使用 407 | 代码可访问[github](https://github.com/zxyue25/axios-[ajax) 408 | 409 | #### axios-ajax完整封装 410 | ```js 411 | // src/utils/https.js 412 | import axios from 'axios' 413 | import qs from 'qs' 414 | import { debounce } from './debounce' 415 | 416 | const contentTypes = { 417 | json: 'application/json; charset=utf-8', 418 | urlencoded: 'application/x-www-form-urlencoded; charset=utf-8', 419 | multipart: 'multipart/form-data', 420 | } 421 | 422 | function toastMsg() { 423 | Object.keys(errorMsgObj).map((item) => { 424 | Message.error(item) 425 | delete errorMsgObj[item] 426 | }) 427 | } 428 | 429 | let errorMsgObj = {} 430 | 431 | const defaultOptions = { 432 | withCredentials: true, // 允许把cookie传递到后台 433 | headers: { 434 | Accept: 'application/json', 435 | 'Content-Type': contentTypes.json, 436 | }, 437 | timeout: 15000, 438 | } 439 | 440 | export const callApi = ({ 441 | url, 442 | data = {}, 443 | method = 'get', 444 | options = {}, 445 | contentType = 'json', // json || urlencoded || multipart 446 | prefixUrl = 'api', 447 | }) => { 448 | if (!url) { 449 | const error = new Error('请传入url') 450 | return Promise.reject(error) 451 | } 452 | const fullUrl = `/${prefixUrl}/${url}` 453 | 454 | const newOptions = { 455 | ...defaultOptions, 456 | ...options, 457 | headers: { 458 | 'Content-Type': 459 | (options.headers && options.headers['Content-Type']) || 460 | contentTypes[contentType], 461 | }, 462 | method, 463 | } 464 | if (method === 'get') { 465 | newOptions.params = data 466 | } 467 | 468 | if (method !== 'get' && method !== 'head') { 469 | newOptions.data = data 470 | if (data instanceof FormData) { 471 | newOptions.headers = { 472 | 'x-requested-with': 'XMLHttpRequest', 473 | 'cache-control': 'no-cache', 474 | } 475 | } else if (newOptions.headers['Content-Type'] === contentTypes.urlencoded) { 476 | newOptions.data = qs.stringify(data) 477 | } else { 478 | Object.keys(data).forEach((item) => { 479 | if ( 480 | data[item] === null || 481 | data[item] === undefined || 482 | data[item] === '' 483 | ) { 484 | delete data[item] 485 | } 486 | }) 487 | // 没有必要,因为axios会将JavaScript对象序列化为JSON 488 | // newOptions.data = JSON.stringify(data); 489 | } 490 | } 491 | 492 | axios.interceptors.request.use((request) => { 493 | // 移除起始部分 / 所有请求url走相对路径 494 | request.url = request.url.replace(/^\//, '') 495 | return request 496 | }) 497 | 498 | return axios({ 499 | url: fullUrl, 500 | ...newOptions, 501 | }) 502 | .then((response) => { 503 | const { data } = response 504 | if (data.code === 'xxx') { 505 | // 与服务端约定 506 | // 登录校验失败 507 | } else if (data.code === 'xxx') { 508 | // 与服务端约定 509 | // 无权限 510 | router.replace({ path: '/403' }) 511 | } else if (data.code === 'xxx') { 512 | // 与服务端约定 513 | return Promise.resolve(data) 514 | } else { 515 | const { message } = data 516 | if (!errorMsgObj[message]) { 517 | errorMsgObj[message] = message 518 | } 519 | setTimeout(debounce(toastMsg, 1000, true), 1000) 520 | return Promise.reject(data) 521 | } 522 | }) 523 | .catch((error) => { 524 | if (error.response) { 525 | const { data } = error.response 526 | const resCode = data.status 527 | const resMsg = data.message || '服务异常' 528 | // if (resCode === 401) { // 与服务端约定 529 | // // 登录校验失败 530 | // } else if (data.code === 403) { // 与服务端约定 531 | // // 无权限 532 | // router.replace({ path: '/403' }) 533 | // } 534 | if (!errorMsgObj[resMsg]) { 535 | errorMsgObj[resMsg] = resMsg 536 | } 537 | setTimeout(debounce(toastMsg, 1000, true), 1000) 538 | const err = { code: resCode, respMsg: resMsg } 539 | return Promise.reject(err) 540 | } else { 541 | const err = { type: 'canceled', respMsg: '数据请求超时' } 542 | return Promise.reject(err) 543 | } 544 | }) 545 | } 546 | ``` 547 | ```js 548 | // src/utils/debounce.js 549 | export const debounce = (func, timeout, immediate) => { 550 | let timer 551 | 552 | return function () { 553 | let context = this 554 | let args = arguments 555 | 556 | if (timer) clearTimeout(timer) 557 | if (immediate) { 558 | var callNow = !timer 559 | timer = setTimeout(() => { 560 | timer = null 561 | }, timeout) 562 | if (callNow) func.apply(context, args) 563 | } else { 564 | timer = setTimeout(function () { 565 | func.apply(context, args) 566 | }, timeout) 567 | } 568 | } 569 | } 570 | ``` 571 | #### 具体使用 572 | 573 | api管理文件在目录`src/service`下,`index.js`文件暴露其他模块,其他文件按`功能模块划分`文件 574 | 575 | get请求带参数 576 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/40ca15ccaafc451989a5b7d2ae60895f~tplv-k3u1fbpfcp-watermark.image) 577 | 自定义前缀代理不同服务 578 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c390bf12036242b495054ba17e22722d~tplv-k3u1fbpfcp-watermark.image) 579 | 文件类型处理 580 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd5be0ca5ece4f3da860ef6578407d6e~tplv-k3u1fbpfcp-watermark.image) 581 | 582 | ## 五、总结 583 | `axios`封装没有一个绝对的标准,且需要结合项目中`实际场景`来设计,但是毋庸置疑,axios-ajax的封装是非常有必要的 584 | --------------------------------------------------------------------------------