数据 接口&联调
开发环境
如果前端应用和后端接口服务器没有运行在同一个主机上,你需要在开发环境下将接口请求代理到接口服务器。
如果是同一个主机,可以直接请求具体的接口地址。
配置
开发环境时候,接口地址在项目根目录下
.env.development 文件配置
# vite 服务端接口服务主机地址VITE_GLOB_BASE_URL=https://apigw-dev.trinablue.com/csde-nem-api/v1.0/test生产环境
生产环境接口地址在项目根目录下 .env.production 文件配置。
生产环境接口地址值需要修改 VITE_GLOB_BASE_URL,如果出现跨域问题,可以使用 nginx 或者后台开启 cors 进行处理
接口请求
在 v3-admin-template 中:
- 页面交互操作;
- 调用统一管理的 api 请求函数;
- 使用封装的 axios.ts 发送请求;
- 获取服务端返回数据
- 更新 data;
接口统一存放于 src/http/ 下面管理
以登陆接口为例:
在 src/api/ 内新建模块文件
import $http from '@/utils/axios';
// 登录export const postLoginSmsAPI = (data) => { return $http.post('/login/nem', { data });};axios 配置
axios 请求封装存放于 src/utils/axios 文件夹内部
除 index.js 文件内容需要根据项目自行修改外,其余文件无需修改
├── Axios.ts // axios实例├── axiosCancel.ts // axiosCancel实例,取消重复请求├── checkStatus.js // 返回状态值校验├── index.js // 接口返回统一处理index.ts 配置说明
const axios = new VAxios({ // 认证方案,例如: Bearer // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes authenticationScheme: '', // 接口超时时间 单位毫秒 timeout: 10 * 1000, headers: { 'Content-Type': ContentTypeEnum.JSON }, // 数据处理方式,见下方说明 transform: clone(transform), // 配置项,下面的选项都可以在独立的接口请求中覆盖 requestOptions: { // 是否返回原生响应头 比如:需要获取响应头时使用该属性 isReturnRaw: false, // post请求的时候添加参数到url joinParamsToUrl: false, // 格式化提交参数时间 formatDate: true, // 消息提示类型 errorMessageMode: 'message', // 接口地址 urlPrefix: import.meta.env.VITE_GLOB_API_PREFIX, // 是否加入时间戳 joinTime: false, // 忽略重复请求 ignoreCancelToken: true, // 是否携带token withToken: true, // 返回信息是否采用当地语言 withLocale: true, },});transform 数据处理说明
类型定义,见 axiosTransform.ts 文件
export abstract class AxiosTransform { /** * @description: 请求之前处理配置 */ beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
/** * @description: 请求成功处理 */ transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
/** * @description: 请求失败处理 */ requestCatch?: (e: Error) => Promise<any>;
/** * @description: 请求之前的拦截器 */ requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
/** * @description: 请求之后的拦截器 */ responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
/** * @description: 请求之前的拦截器错误处理 */ requestInterceptorsCatch?: (error: Error) => void;
/** * @description: 请求之后的拦截器错误处理 */ responseInterceptorsCatch?: (error: Error) => void;}项目默认 transform 处理逻辑,可以根据各自项目进行处理。一般需要更改的部分为下方代码,见代码注释说明
/** * @description: 数据处理,方便区分多种处理方式 */const transform = { // 请求前处理配置 beforeRequestHook: (config, options) => { const disposeGetMothod = (params, joinTime) => { if (isString(params)) { // 兼容restful风格传参 config.url = config.url + params + `${joinTimestamp(joinTime, true)}`; config.params = undefined; } else { // 非restful风格传参 config.params = Object.assign( params || {}, joinTimestamp(joinTime, false) ); } };
const disposeNonGetMothod = ( params, data, { formatDate, joinParamsToUrl } ) => { if (isString(params)) { // 兼容restful风格传参 config.url = config.url + params; config.params = undefined; } else { if (formatDate) { data && !isString(data) && formatRequestDate(data); } if ( Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0 ) { config.data = data; config.params = params; } else { // 非GET请求如果没有提供data,则将params视为data config.data = params; config.params = undefined; } if (joinParamsToUrl) { config.url = setObjToUrlParams( config.url, Object.assign({}, config.params, config.data) ); } } };
const { urlPrefix, joinTime = true, formatDate, joinParamsToUrl } = options;
if (urlPrefix) { config.url = `${urlPrefix}${config.url}`; }
const params = config.params || {}; const data = config.data || false; if (config.method?.toUpperCase() === RequestEnum.GET) { disposeGetMothod(params, joinTime); } else { disposeNonGetMothod(params, data, { formatDate, joinParamsToUrl }); }
return config; },
// 请求拦截器处理 requestInterceptors: (config, options) => { // 请求之前处理config const userStore = useUserStoreWithOut(); const localeStore = useLocaleStoreWithOut();
const token = userStore.getToken; if (token && config?.requestOptions?.withToken !== false) { if (/getResourceTree/.test(config.url)) { config.headers.Authorization = `Bearer ${token}`; } else { // jwt token config.headers.Authorization = options.authenticationScheme ? `${options.authenticationScheme} ${token}` : token; } }
const locale = localeStore.getLocale; if (config?.requestOptions?.withLocale && locale) { config.headers.LocaleLang = locale; }
return config; },
// 响应拦截器处理 responseInterceptors: (res) => res,
// 响应拦截器错误处理 responseInterceptorsCatch: (error) => { const { t } = useI18n(); const { response, code, message, config } = error || {}; const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none'; const msg = response?.data?.error?.message ?? ''; const err = error?.toString?.() ?? ''; let errMessage = '';
try { if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { errMessage = t('sys.api.apiTimeoutMessage'); } if (err?.includes('Network Error')) { errMessage = t('sys.api.networkExceptionMsg'); }
if (errMessage) { if (errorMessageMode === 'modal') { createErrorModal({ title: t('sys.api.errorTip'), content: errMessage, }); } else if (errorMessageMode === 'message') { createMessage.error(errMessage); } return Promise.reject(error); } } catch (error) { throw new Error(error); }
checkStatus(error?.response?.status, msg, errorMessageMode); return Promise.reject(error); },
/** * @param {*} res * @param {*} options * @returns */ transformRequestHook: (res, options) => { const { t } = useI18n(); const { isReturnRaw } = options; if (isReturnRaw) { return res; } if (res.data.bizcode) { const { data: axiosData } = res; if (!axiosData) { throw new Error(t('sys.api.apiRequestFailed')); }
const { bizcode, data, msg, bizmsg, code, errMsg } = axiosData;
const bizSuccess = Reflect.has(axiosData, 'bizcode') && bizcode === ResultEnum.SUCCESS; if (bizSuccess) { return data; }
// bizcode非10000的情况 let errorMsg = ''; const userStore = useUserStoreWithOut(); switch (bizcode) { case ResultEnum.UNAUTHORIZED: errorMsg = t('sys.api.errMsg401'); // eslint-disable-next-line no-case-declarations userStore.setToken(undefined); userStore.logout(true); break; case ResultEnum.TOKENINVALID: errorMsg = t('sys.api.errMsg402'); userStore.setToken(undefined); userStore.logout(true); break; default: if (msg) { errorMsg = msg; } else if (bizmsg) { errorMsg = bizmsg; } else if (errMsg) { errorMsg = errMsg; } break; }
// errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误 // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示 if (options.errorMessageMode === 'modal') { createErrorModal({ title: t('sys.api.errorTip'), content: errorMsg }); } else if (options.errorMessageMode === 'message') { createMessage.error(errorMsg); }
throw new Error(errorMsg || t('sys.api.apiRequestFailed')); } else { return res.data; } },};更改参数格式
项目接口默认为 Json 参数格式,即 headers: { 'Content-Type': ContentTypeEnum.JSON },
如果需要更改为 form-data 格式,更改 headers 的 'Content-Type 为 ContentTypeEnum.FORM_URLENCODED 即可
多个接口地址
当项目中需要用到多个接口地址时, 可以在 src/utils/axios/index.js 导出多个 axios 实例
// 目前只导出一个默认实例,接口地址对应的是环境变量中的 VITE_GLOB_BASE_URL 接口地址const $http = createAxios()
export default $http
// 需要有其他接口地址的可以在后面添加
// other api urlexport const otherHttp = createAxios({ requestOptions: { apiUrl: 'xxx', },});或者在 src/api/ 模块文件内通过环境变量
import $http from '@/utils/axios';const VITE_GLOB_SMS_SERVER = import.meta.env.VITE_GLOB_SMS_SERVER
export const getCompanies4TfsAPI = () => { return $http.get(`${VITE_GLOB_SMS_SERVER}/open/getCompanies4Tfs`)}删除请求 URL 携带的时间戳参数
如果不需要 url 上面默认携带的时间戳参数 ?_t=xxx
const axios = new VAxios({ requestOptions: { // 是否加入时间戳 joinTime: false, },});