要实现前端调用后端接口,可以通过多种方式来进行。以下是几种常见且高效的方法来实现你的需求:

# 方法一:使用 axios 发送 HTTP 请求

axios 是一个非常流行的 HTTP 请求库,支持 Promise API,可以方便地进行网络请求。

  1. 安装 axios

    首先,确保你已经安装了 axios,如果没有,可以通过 npm 安装:

    npm install axios
    
  2. 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),这样代码会更加模块化。

  1. 封装 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 };
    };
    
  2. 在组件中使用封装的 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)

(支持拦截器、超时、请求取消)

  • 模仿 Axiosrequest/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

相比 XMLHttpRequestfetch 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)