要实现前端调用后端接口,可以通过多种方式来进行。以下是几种常见且高效的方法来实现你的需求:
# 方法一:使用 axios
发送 HTTP 请求
axios
是一个非常流行的 HTTP 请求库,支持 Promise API,可以方便地进行网络请求。
安装
axios
首先,确保你已经安装了
axios
,如果没有,可以通过 npm 安装:npm install axios
Vue 3 中调用接口
在
Vue 3
中,你可以在<script setup>
中直接引入axios
并调用后端接口。以下是一个示例:<template> <div> <ul v-if="authors.length"> <li v-for="author in authors" :key="author.author_id"> <img :src="author.avatar" alt="avatar" /> <div>{{ author.name }}</div> <div>{{ author.desc }}</div> </li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import axios from 'axios'; const authors = ref([]); const fetchAuthors = async () => { try { const response = await axios.post('http://106.53.29.159:8788/v1/site_ugc/author/get', { phone_number: '', status: 1, page: 1, size: 10, }); authors.value = response.data.data.rows; } catch (error) { console.error('Error fetching authors:', error); } }; onMounted(() => { fetchAuthors(); }); </script>
解释:
- 使用
axios.post
向后端发送POST
请求。 - 请求的参数是通过 JavaScript 对象传递。
- 成功响应后,
authors
数组会更新为返回的数据。
- 使用
# 方法二:使用 fetch
API
如果不想使用第三方库,可以使用原生的 fetch
API 来发送 HTTP 请求。fetch
是浏览器原生支持的,它返回一个 Promise 对象,并且支持现代浏览器。
<template>
<div>
<ul v-if="authors.length">
<li v-for="author in authors" :key="author.author_id">
<img :src="author.avatar" alt="avatar" />
<div>{{ author.name }}</div>
<div>{{ author.desc }}</div>
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const authors = ref([]);
const fetchAuthors = async () => {
try {
const response = await fetch('http://106.53.29.159:8788/v1/site_ugc/author/get', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone_number: '',
status: 1,
page: 1,
size: 10,
}),
});
const data = await response.json();
authors.value = data.data.rows;
} catch (error) {
console.error('Error fetching authors:', error);
}
};
onMounted(() => {
fetchAuthors();
});
</script>
解释:
- 使用
fetch
发起POST
请求。 Content-Type: application/json
告诉服务器请求体是 JSON 格式。- 使用
await
等待请求结果并解析 JSON 数据。
# 方法三:使用 Vue 3 Composition API 与 useHttp
封装
为了更高效地管理 API 请求和响应,很多时候我们可以将 HTTP 请求封装为一个可复用的钩子(composable),这样代码会更加模块化。
封装 HTTP 请求函数(
useHttp.js
)创建一个
useHttp.js
文件,封装请求逻辑:// useHttp.js import axios from 'axios'; export const useHttp = () => { const post = async (url, params) => { try { const response = await axios.post(url, params); return response.data; } catch (error) { console.error('API request error:', error); return null; } }; return { post }; };
在组件中使用封装的
useHttp
<template> <div> <ul v-if="authors.length"> <li v-for="author in authors" :key="author.author_id"> <img :src="author.avatar" alt="avatar" /> <div>{{ author.name }}</div> <div>{{ author.desc }}</div> </li> </ul> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import { useHttp } from './useHttp'; const { post } = useHttp(); const authors = ref([]); const fetchAuthors = async () => { const data = await useHttp.post('http://106.53.29.159:8788/v1/site_ugc/author/get', { phone_number: '', status: 1, page: 1, size: 10, }); if (data) { authors.value = data.data.rows; } }; onMounted(() => { fetchAuthors(); }); </script>
解释:
- 将 HTTP 请求逻辑封装到
useHttp
中,使得每个组件都可以复用相同的逻辑,代码更加清晰且可维护。
# 基于 Axios 的前端请求封装与拦截
1. http.ts
(封装 HTTP 请求核心逻辑)
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { ResultData } from '@/api/interface';
import { tryHideFullScreenLoading } from '@/config/serviceLoading';
import { ResultEnum } from '@/enums/httpEnum';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { getToken } from '@/utils';
import { AxiosCanceler } from './helper/axiosCancel';
import { checkStatus } from './helper/checkStatus';
import { getBaseURL, getAppDomain } from './helper/envHelper';
import { handleRequestError, handleResponse } from './helper/interceptors';
const axiosCanceler = new AxiosCanceler(); // 创建 Axios 取消请求对象
// 创建 Axios 实例
const service: AxiosInstance = axios.create({
baseURL: getBaseURL(), // 动态获取 API 地址
timeout: 60000, // 请求超时时间 60s
withCredentials: true, // 允许跨域请求携带 Cookies
});
// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
axiosCanceler.addPending(config); // 添加请求到取消队列
// 设置请求头
config.headers = {
Authorization: `Bearer ${getToken()}`, // 认证 Token
'APP-USERNAME': GlobalStore().appUserName, // 自定义用户名
Method: config.method?.toUpperCase() ?? 'POST', // 默认使用 POST 方法
'Content-Type': 'application/json', // 设定请求体类型为 JSON
'APP-DOMAIN': getAppDomain(), // 业务域名
...config.headers, // 合并其他自定义请求头
};
return config;
},
handleRequestError
);
// 响应拦截器
service.interceptors.response.use(handleResponse, async (error: AxiosError) => {
tryHideFullScreenLoading(); // 隐藏全局 loading
return handleRequestError(error);
});
// 封装 HTTP 请求方法
const request = {
get<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
return service.get(url, { params, ...config });
},
post<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
return service.post(url, params, config);
},
put<T>(url: string, params?: object, config = {}): Promise<ResultData<T>> {
return service.put(url, params, config);
},
delete<T>(url: string, params?: any, config = {}): Promise<ResultData<T>> {
return service.delete(url, { params, ...config });
},
};
export default request;
2. helper/envHelper.ts
(环境变量管理)
/**
* 获取 API 基础 URL
* - 开发环境: 读取 VITE_API_BASE_URL
* - 生产环境: 读取 window.VITE_API_BASE_URL
*/
export const getBaseURL = (): string => {
return import.meta.env.DEV ? import.meta.env.VITE_API_BASE_URL : (window.VITE_API_BASE_URL as string);
};
/**
* 获取 APP 业务域名
* - 可能存在多个域名,以 `,` 分隔,取第一个
*/
export const getAppDomain = (): string => {
const appDomainEnv = import.meta.env.DEV ? import.meta.env.VITE_APP_DOMAIN : (window.VITE_APP_DOMAIN as string);
return appDomainEnv?.split(',')?.[0] ?? '';
};
3. helper/interceptors.ts
(拦截器逻辑)
import { AxiosError, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
import { ResultEnum } from '@/enums/httpEnum';
import router from '@/routers';
import { GlobalStore } from '@/store';
import { checkStatus } from './checkStatus';
/**
* 处理请求错误
* @param error - Axios 错误对象
*/
export const handleRequestError = (error: AxiosError) => {
if (error.message.includes('timeout')) {
ElMessage.error('请求超时!请稍后重试');
}
if (error.response) {
checkStatus(error.response.status); // 处理 HTTP 状态码错误
}
if (!window.navigator.onLine) {
router.replace({ path: '/500' }); // 断网处理,跳转到 500 页面
}
return Promise.reject(error);
};
/**
* 处理服务器返回的响应
* @param response - Axios 响应对象
*/
export const handleResponse = (response: AxiosResponse) => {
const { data, config } = response;
const globalStore = GlobalStore();
// 处理登录失效
if (data.status === ResultEnum.OVERDUE || data.status === ResultEnum.NOT_AUTH) {
ElMessage.error(data.info);
router.replace({ path: '/login' });
globalStore.setToken('');
globalStore.setAppUserName('');
return Promise.reject(data);
}
// 错误信息拦截(排除下载文件的情况)
if (!config.headers?.ignoreError && data.status) {
ElMessage.error(data.data?.err_msg || data.info);
return Promise.reject(data);
}
return data;
};
4. helper/checkStatus.ts
(HTTP 状态码处理)
import { ElMessage } from 'element-plus';
/**
* 统一处理 HTTP 状态码错误
* @param status - HTTP 状态码
*/
export const checkStatus = (status: number) => {
switch (status) {
case 400:
ElMessage.error('请求参数错误,请检查');
break;
case 401:
ElMessage.error('未授权访问,请重新登录');
break;
case 403:
ElMessage.error('权限不足,禁止访问');
break;
case 404:
ElMessage.error('请求地址不存在');
break;
case 500:
ElMessage.error('服务器内部错误');
break;
default:
ElMessage.error('请求失败,请稍后重试');
}
};
调用示例
在 Vue 3
组件中,使用 request
调用 API:
import request from '@/api/http';
// 请求参数
const params = {
phone_number: '',
status: 1,
page: 1,
size: 10,
};
// 调用 API
request.post('/v1/site_ugc/author/get', params)
.then(data => {
console.log('获取成功:', data);
})
.catch(err => {
console.error('请求失败:', err);
});
# 手撸Axios
手撸 Axios
,核心就是基于 XMLHttpRequest(简称 XHR
)或者 fetch API
来实现 HTTP 请求,并封装请求拦截、响应拦截等功能。
# 方式 1:基于 XMLHttpRequest(接近 Axios)
(支持拦截器、超时、请求取消)
- 模仿
Axios
的request/get/post
- 支持 请求拦截 & 响应拦截
- 兼容 Promise
- 使用
XMLHttpRequest
进行底层封装
class MyAxios {
interceptors = {
request: [] as any[],
response: [] as any[],
};
request<T>(config: any): Promise<T> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(config.method || "GET", config.url, true);
xhr.setRequestHeader("Content-Type", "application/json");
// 执行请求拦截器
this.interceptors.request.forEach((interceptor) => {
config = interceptor(config);
});
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
let responseData = xhr.responseText;
try {
responseData = JSON.parse(xhr.responseText);
} catch (e) {}
const response = {
status: xhr.status,
data: responseData,
};
// 执行响应拦截器
let finalResponse = response;
this.interceptors.response.forEach((interceptor) => {
finalResponse = interceptor(response);
});
xhr.status >= 200 && xhr.status < 300
? resolve(finalResponse)
: reject(finalResponse);
}
};
xhr.onerror = function () {
reject({ status: xhr.status, message: "请求失败" });
};
xhr.send(JSON.stringify(config.data || {}));
});
}
get<T>(url: string, config = {}): Promise<T> {
return this.request({ ...config, url, method: "GET" });
}
post<T>(url: string, data: any, config = {}): Promise<T> {
return this.request({ ...config, url, method: "POST", data });
}
// 请求拦截器
useRequestInterceptor(interceptor: Function) {
this.interceptors.request.push(interceptor);
}
// 响应拦截器
useResponseInterceptor(interceptor: Function) {
this.interceptors.response.push(interceptor);
}
}
// 使用 MyAxios
const http = new MyAxios();
// 添加请求拦截器
http.useRequestInterceptor((config: any) => {
console.log("请求拦截:", config);
config.headers = {
...config.headers,
Authorization: `Bearer token123`,
};
return config;
});
// 添加响应拦截器
http.useResponseInterceptor((response: any) => {
console.log("响应拦截:", response);
return response;
});
// 发送请求
http
.get("https://jsonplaceholder.typicode.com/todos/1")
.then((res) => console.log("成功:", res))
.catch((err) => console.log("失败:", err));
# 方式 2:基于 Fetch API
相比 XMLHttpRequest
,fetch API
更现代,但不支持 请求取消,所以 Axios
仍然更流行。
没有 XMLHttpRequest
的 CORS 限制
# Fetch 版 Axios
class MyFetch {
interceptors = {
request: [] as any[],
response: [] as any[],
};
async request<T>(config: any): Promise<T> {
let finalConfig = config;
// 执行请求拦截器
this.interceptors.request.forEach((interceptor) => {
finalConfig = interceptor(finalConfig);
});
const response = await fetch(finalConfig.url, {
method: finalConfig.method || "GET",
headers: {
"Content-Type": "application/json",
...(finalConfig.headers || {}),
},
body: finalConfig.data ? JSON.stringify(finalConfig.data) : null,
});
let responseData = await response.json();
// 执行响应拦截器
this.interceptors.response.forEach((interceptor) => {
responseData = interceptor(responseData);
});
return responseData;
}
get<T>(url: string, config = {}): Promise<T> {
return this.request({ ...config, url, method: "GET" });
}
post<T>(url: string, data: any, config = {}): Promise<T> {
return this.request({ ...config, url, method: "POST", data });
}
useRequestInterceptor(interceptor: Function) {
this.interceptors.request.push(interceptor);
}
useResponseInterceptor(interceptor: Function) {
this.interceptors.response.push(interceptor);
}
}
// 使用 MyFetch
const http = new MyFetch();
// 请求拦截
http.useRequestInterceptor((config: any) => {
console.log("fetch 请求拦截:", config);
config.headers = { ...config.headers, Authorization: `Bearer token123` };
return config;
});
// 响应拦截
http.useResponseInterceptor((response: any) => {
console.log("fetch 响应拦截:", response);
return response;
});
// 发送请求
http
.get("https://jsonplaceholder.typicode.com/todos/1")
.then((res) => console.log("fetch 成功:", res))
.catch((err) => console.log("fetch 失败:", err));
# 企业的一个项目规范
一般在api文件夹下
/config
—— /servicePort.ts // 写的是后端服务端口名
// 注意是端口名,可以是通过代理处理过的
//比如:
export const PORT1 = '/v1'
export const PORT_ADMIN = '/admin'
export const MS_APP_API_PROXY = '/proxy/ms_app_api'
我们的接口一般是这样子的
http://106.53.29.159:8788/v1/site_ugc/author/get
http://106.53.29.159:8788/
接口地址/v1/site_ugc/就是端口名,一个端口包括多个接口
/helper
—— /axiosCancel.ts // 取消
—— /checkStatus.ts // 校验状态码
import router from '@/routers/router'
import { GlobalStore } from '@/store'
import { ElMessage } from 'element-plus'
/**
* @description: 校验网络请求状态码
* @param {Number} status
* @return void
*/
export const checkStatus = (status: number): void => {
switch (status) {
case 400:
ElMessage.error('请求失败!请您稍后重试')
break
case 401:
ElMessage.error('登录失效!请您重新登录')
GlobalStore().setToken('')
GlobalStore().setAppUserName('')
router.replace({ path: '/login' })
break
case 403:
ElMessage.error('当前账号无权限访问!')
GlobalStore().setToken('')
GlobalStore().setAppUserName('')
router.replace({ path: '/login' })
break
case 404:
ElMessage.error('你所访问的资源不存在!')
break
case 405:
ElMessage.error('请求方式错误!请您稍后重试')
break
case 408:
ElMessage.error('请求超时!请您稍后重试')
break
case 500:
ElMessage.error('服务异常!')
break
case 502:
ElMessage.error('网关错误!')
break
case 422:
ElMessage.error('TOKEN 异常,请重新登陆!')
GlobalStore().setToken('')
router.replace({
path: '/login',
})
break
case 503:
ElMessage.error('服务不可用!')
break
case 504:
ElMessage.error('网关超时!')
break
default:
ElMessage.error('请求失败!')
}
}
/interface // 类型一般是公用的,如果是特殊的看下面
比如下面的
common.ts
export interface NObject {
[key: string]: string | number | undefined | null | void | Object;
}
// * 请求响应参数(不包含data)
export interface Result {
status: string;
info: string;
}
// * 请求响应参数(包含data)
export interface ResultData<T = any> extends Result {
[x: string]: any;
data?: T;
}
// * 分页响应参数
export interface ResPage<T> {
rows: T[];
total: number;
}
export interface ResPageRow<T> {
rows: T[];
total: number;
}
export interface ResPageResult<T> {
result: T[];
total: number;
}
// modules就是连接接口的地方
以一个接口为例子
modules下创建tags文件夹,tags文件夹下创建3个文件:
interface.ts
export interface ITagsParams {
tag_id: string
seo_path: string
name: string
page_size: number
page: number
}
export interface IAddTagsParams {
tag_id: string
seo_path: string
name: string
}
export interface IUpdateTagsParams {
tag_id: string
seo_path: string
}
url.const.ts
import { MS_SHXL_API_PROXY } from '@/api/config/servicePort'
export const ADMIN_TAG_LIST = MS_SHXL_API_PROXY + '/admin/tag/list'
export const ADMIN_TAG_UPDATE = MS_SHXL_API_PROXY + '/admin/tag/update'
export const ADMIN_TAG_ADD = MS_SHXL_API_PROXY + '/admin/tag/add'
export const ADMIN_TAG_DELETE = MS_SHXL_API_PROXY + '/admin/tag/delete'
index.ts
import { update } from 'lodash-es'
import { ITagsParams, IAddTagsParams, IUpdateTagsParams } from './interface'
import { ADMIN_TAG_LIST, ADMIN_TAG_UPDATE, ADMIN_TAG_ADD, ADMIN_TAG_DELETE } from './url.const'
import request from '@/api'
const tagApi = {
getTagList: (params: ITagsParams) => request.post(ADMIN_TAG_LIST, params),
addTag: (params: IAddTagsParams) => request.post(ADMIN_TAG_ADD, params),
updateTag: (params: IUpdateTagsParams) => request.post(ADMIN_TAG_UPDATE, params),
delTag: (params: { tag_ids: string[] }) => request.post(ADMIN_TAG_DELETE, params),
}
export default tagApi
index.ts
import { ResultData } from '@/api/interface/common'
import { tryHideFullScreenLoading } from '@/config/serviceLoading'
import { ResultEnum } from '@/enums/httpEnum'
import router from '@/routers'
import { GlobalStore } from '@/store'
import { warningMessage } from '@/utils/notifications'
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import Fingerprint2 from 'fingerprintjs2'
import { generateSign } from '../utils/sign'
import { AxiosCanceler } from './helper/axiosCancel'
import { checkStatus } from './helper/checkStatus'
const axiosCanceler = new AxiosCanceler()
const baseURL = import.meta.env.VITE_API_BASE_URL as string
const AppId = import.meta.env.VITE_APP_ID
const fingerP = ref('')
const createFingerprint = () => {
Fingerprint2.get((components: any) => {
const values = components.map((components: any) => components.value)
const murmur = Fingerprint2.x64hash128(values.join(''), 31)
fingerP.value = murmur
})
}
createFingerprint()
const config = {
// 默认地址请求地址,可在 .env 开头文件中修改
baseURL: baseURL,
timeout: 1000 * 10,
// 跨域时候允许携带凭证
withCredentials: true,
}
class RequestHttp {
service: AxiosInstance
public constructor(config: AxiosRequestConfig) {
// 实例化axios
this.service = axios.create(config)
/**
* @description 请求拦截器
* 客户端发送请求 -> [请求拦截器] -> 服务器
* token校验(JWT) : 接受服务器返回的token,存储到vuex/pinia/本地储存当中
*/
this.service.interceptors.request.use(
async (config: AxiosRequestConfig) => {
const globalStore = GlobalStore()
const signParams = {
reqClient: import.meta.env.VITE_MS_REQ_CLIENT,
webSignToken: import.meta.env.VITE_APP_REQ_SECRET,
}
const extraHeader = await generateSign(config.data, signParams)
config.headers = {
Authorization: globalStore.token ?? '',
Method: config.method?.toUpperCase() ?? 'POST',
'Content-Type': 'application/json',
'APP-ID': AppId,
'Browser-Fingerprint': fingerP.value,
...config.headers,
...extraHeader,
}
return config
},
(error: AxiosError) => {
return Promise.reject(error)
},
)
/**
* @description 响应拦截器
* 服务器换返回信息 -> [拦截统一处理] -> 客户端JS获取到信息
*/
this.service.interceptors.response.use(
(response: AxiosResponse) => {
const globalStore = GlobalStore()
const { data, config } = response
// * 在请求结束后,移除本次请求,并关闭请求 loading
axiosCanceler.removePending(config)
tryHideFullScreenLoading()
// * 登陆失效(code == 599)
if (response.status == ResultEnum.SUCCESS) {
switch (response.data.status) {
case 0:
return response.data.data
case 10205: {
// ElMessage.warning(response.data.info);
warningMessage(response.data?.data?.err_msg || response.data.info)
return Promise.reject()
}
case 10203:
case 10217:
case 10210:
case 10211: {
window.open('/#/login', '_self')
return Promise.reject()
}
default: {
// ElMessage.warning(response.data.info);
warningMessage(response.data?.data?.err_msg || response.data.info)
//return response.data
return Promise.reject(response.data)
}
}
}
if (data.status == ResultEnum.OVERDUE || data.status == ResultEnum.NOT_AUTH) {
ElMessage.error(data.info)
router.replace({
path: '/login',
})
globalStore.setToken('')
globalStore.setAppUserName('')
return Promise.reject(data)
}
// * 全局错误信息拦截(防止下载文件得时候返回数据流,没有code,直接报错)
// 是否忽略错误
if (!config.headers?.ignoreError) {
if (data.status) {
ElMessage.error(data.data?.err_msg || data.info)
return Promise.reject(data)
}
}
console.log(`API REQUEST 【${config.url}】,RESPONSE IS `, data)
return data
},
async (error: AxiosError) => {
const { response } = error
tryHideFullScreenLoading()
// 请求超时单独判断,因为请求超时没有 response
if (error.message.indexOf('timeout') !== -1) ElMessage.error('请求超时!请您稍后重试')
// 根据响应的错误状态码,做不同的处理
if (response) checkStatus(response.status)
// 服务器结果都没有返回(可能服务器错误可能客户端断网),断网处理:可以跳转到断网页面
if (!window.navigator.onLine) router.replace({ path: '/500' })
return Promise.reject(error)
},
)
}
// * 常用请求方法封装
get<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.get(url, { params, ..._object })
}
post<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.post(url, params, _object)
}
put<T>(url: string, params?: object, _object = {}): Promise<ResultData<T>> {
return this.service.put(url, params, _object)
}
delete<T>(url: string, params?: any, _object = {}): Promise<ResultData<T>> {
return this.service.delete(url, { params, ..._object })
}
}
export default new RequestHttp(config)