阅读视图

发现新文章,点击刷新页面。

Vue3 组件通信全解析

组件通信是 Vue 开发中绕不开的核心知识点,尤其是 Vue3 组合式 API 普及后,通信方式相比 Vue2 有了不少变化和优化。本文将抛开 TypeScript,用最通俗易懂的方式,带你梳理 Vue3 中所有常用的组件通信方式,从基础的父子通信到复杂的跨层级通信,每一种都配实战示例,新手也能轻松上手。

一、父子组件通信(最基础也最常用)

父子组件通信是日常开发中使用频率最高的场景,Vue3 为这种场景提供了清晰且高效的解决方案。

1. 父传子:Props

Props 是父组件向子组件传递数据的官方标准方式,子组件通过定义 props 接收父组件传递的值。

父组件(Parent.vue)

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <!-- 向子组件传递数据 -->
    <Child 
      :msg="parentMsg" 
      :user-info="userInfo"
      :list="fruitList"
    />
  </div>
</template>

<script setup>
// 引入子组件
import Child from './Child.vue'
import { ref, reactive } from 'vue'

// 定义要传递给子组件的数据
const parentMsg = ref('来自父组件的问候')
const userInfo = reactive({
  name: '张三',
  age: 25
})
const fruitList = ref(['苹果', '香蕉', '橙子'])
</script>

子组件(Child.vue)

<template>
  <div class="child">
    <h4>我是子组件</h4>
    <p>父组件传递的字符串:{{ msg }}</p>
    <p>父组件传递的对象:{{ userInfo.name }} - {{ userInfo.age }}岁</p>
    <p>父组件传递的数组:{{ list.join('、') }}</p>
  </div>
</template>

<script setup>
// 定义props接收父组件数据
const props = defineProps({
  // 字符串类型
  msg: {
    type: String,
    default: '默认值'
  },
  // 对象类型
  userInfo: {
    type: Object,
    default: () => ({}) // 对象/数组默认值必须用函数返回
  },
  // 数组类型
  list: {
    type: Array,
    default: () => []
  }
})

// 在脚本中使用props(组合式API中可直接用props.xxx)
console.log(props.msg)
</script>

2. 子传父:自定义事件(Emits)

子组件通过触发自定义事件,将数据传递给父组件,父组件通过监听事件接收数据。

子组件(Child.vue)

<template>
  <div class="child">
    <h4>我是子组件</h4>
    <button @click="sendToParent">向父组件传递数据</button>
  </div>
</template>

<script setup>
// 声明要触发的自定义事件(可选,但推荐)
const emit = defineEmits(['childMsg', 'updateInfo'])

const sendToParent = () => {
  // 触发事件并传递数据(第一个参数是事件名,后续是要传递的数据)
  emit('childMsg', '来自子组件的消息')
  emit('updateInfo', {
    name: '李四',
    age: 30
  })
}
</script>

父组件(Parent.vue)

<template>
  <div class="parent">
    <h3>我是父组件</h3>
    <!-- 监听子组件的自定义事件 -->
    <Child 
      @childMsg="handleChildMsg"
      @updateInfo="handleUpdateInfo"
    />
    <p>子组件传递的消息:{{ childMsg }}</p>
    <p>子组件更新的信息:{{ newUserInfo.name }} - {{ newUserInfo.age }}岁</p>
  </div>
</template>

<script setup>
import Child from './Child.vue'
import { ref, reactive } from 'vue'

const childMsg = ref('')
const newUserInfo = reactive({
  name: '',
  age: 0
})

// 处理子组件的消息
const handleChildMsg = (msg) => {
  childMsg.value = msg
}

// 处理子组件的信息更新
const handleUpdateInfo = (info) => {
  newUserInfo.name = info.name
  newUserInfo.age = info.age
}
</script>

二、跨层级组件通信

当组件嵌套层级较深(比如爷孙组件、跨多级组件),使用 props + emits 会非常繁琐,这时需要更高效的跨层级通信方案。

1. provide /inject(依赖注入)

provide 用于父组件(或祖先组件)提供数据,inject 用于子孙组件注入数据,支持任意层级的组件通信。

祖先组件(GrandParent.vue)

<template>
  <div class="grand-parent">
    <h3>我是祖先组件</h3>
    <Parent />
  </div>
</template>

<script setup>
import Parent from './Parent.vue'
import { ref, reactive, provide } from 'vue'

// 提供基本类型数据
const theme = ref('dark')
provide('theme', theme)

// 提供对象类型数据
const globalConfig = reactive({
  fontSize: '16px',
  color: '#333'
})
provide('globalConfig', globalConfig)

// 提供方法(支持双向通信)
provide('changeTheme', (newTheme) => {
  theme.value = newTheme
})
</script>

孙组件(Child.vue)

<template>
  <div class="child">
    <h4>我是孙组件</h4>
    <p>祖先组件提供的主题:{{ theme }}</p>
    <p>全局配置:{{ globalConfig.fontSize }} / {{ globalConfig.color }}</p>
    <button @click="changeTheme('light')">切换为亮色主题</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

// 注入祖先组件提供的数据(第二个参数是默认值)
const theme = inject('theme', 'light')
const globalConfig = inject('globalConfig', {})
const changeTheme = inject('changeTheme', () => {})
</script>

2. Vuex/Pinia(全局状态管理)

当多个不相关的组件需要共享状态,或者项目规模较大时,推荐使用官方的状态管理库,Vue3 中更推荐 Pinia(比 Vuex 更简洁)。

示例:Pinia 实现全局通信

1. 安装 Pinia

npm install pinia

2. 创建 Pinia 实例(main.js)

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

3. 创建 Store(stores/user.js)

import { defineStore } from 'pinia'

// 定义并导出store
export const useUserStore = defineStore('user', {
  // 状态
  state: () => ({
    username: '默认用户名',
    token: ''
  }),
  // 计算属性
  getters: {
    // 处理用户名格式
    formatUsername: (state) => {
      return `【${state.username}】`
    }
  },
  // 方法(修改状态)
  actions: {
    // 更新用户信息
    updateUserInfo(newInfo) {
      this.username = newInfo.username
      this.token = newInfo.token
    },
    // 清空用户信息
    clearUserInfo() {
      this.username = ''
      this.token = ''
    }
  }
})

4. 组件中使用 Store

<template>
  <div>
    <h3>全局状态管理示例</h3>
    <p>用户名:{{ userStore.formatUsername }}</p>
    <p>Token:{{ userStore.token }}</p>
    <button @click="updateUser">更新用户信息</button>
    <button @click="clearUser">清空用户信息</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'

// 获取store实例
const userStore = useUserStore()

// 更新用户信息
const updateUser = () => {
  userStore.updateUserInfo({
    username: '掘金用户',
    token: '123456789'
  })
}

// 清空用户信息
const clearUser = () => {
  userStore.clearUserInfo()
}
</script>

三、其他常用通信方式

1. v-model 双向绑定

Vue3 中 v-model 支持自定义绑定属性,可实现父子组件的双向数据绑定,简化子传父的操作。

子组件(Child.vue)

<template>
  <div class="child">
    <input 
      type="text" 
      :value="modelValue" 
      @input="emit('update:modelValue', $event.target.value)"
    />
    <!-- 支持多个v-model -->
    <input 
      type="number" 
      :value="age" 
      @input="emit('update:age', $event.target.value)"
    />
  </div>
</template>

<script setup>
defineProps(['modelValue', 'age'])
const emit = defineEmits(['update:modelValue', 'update:age'])
</script>

父组件(Parent.vue)

<template>
  <div class="parent">
    <h3>父组件</h3>
    <Child 
      v-model="username"
      v-model:age="userAge"
    />
    <p>用户名:{{ username }}</p>
    <p>年龄:{{ userAge }}</p>
  </div>
</template>

<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const username = ref('')
const userAge = ref(0)
</script>

2. 事件总线(mitt)

Vue3 移除了 Vue2 的 $on/$emit 事件总线,可使用第三方库 mitt 实现任意组件间的通信。

1. 安装 mitt

npm install mitt

2. 创建事件总线(utils/bus.js)

import mitt from 'mitt'
const bus = mitt()
export default bus

3. 组件 A 发送事件

<template>
  <div>
    <button @click="sendMsg">发送消息到组件B</button>
  </div>
</template>

<script setup>
import bus from '@/utils/bus'

const sendMsg = () => {
  // 触发自定义事件并传递数据
  bus.emit('msgEvent', '来自组件A的消息')
}
</script>

4. 组件 B 接收事件

<template>
  <div>
    <p>组件A传递的消息:{{ msg }}</p>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import bus from '@/utils/bus'

const msg = ref('')

// 挂载时监听事件
onMounted(() => {
  bus.on('msgEvent', (data) => {
    msg.value = data
  })
})

// 卸载时移除监听(避免内存泄漏)
onUnmounted(() => {
  bus.off('msgEvent')
})
</script>

四、通信方式选型建议

表格

通信场景 推荐方式
父传子 Props
子传父 自定义事件(Emits)/v-model
爷孙 / 跨层级 provide / inject
全局共享状态 Pinia
任意组件临时通信 mitt 事件总线

总结

  1. Vue3 中父子组件通信优先使用 Props + Emits,v-model 可简化双向绑定场景;
  2. 跨层级通信推荐 provide / inject,全局状态管理首选 Pinia
  3. 临时的任意组件通信可使用 mitt 事件总线,注意及时移除监听避免内存泄漏。

组件通信的核心是 “数据流向清晰”,无论选择哪种方式,都要保证数据的传递路径可追溯,避免滥用全局通信导致代码维护困难。希望本文能帮助你彻底掌握 Vue3 组件通信,少走弯路~

Vue3 + Vite 性能优化实战

Vue3 + Vite 性能优化实战:从开发到生产,全方位提速指南

前言:在前端开发的江湖里,Vue3 + Vite 组合早已成为主流选择,凭借简洁的语法、高效的构建能力,成为很多项目的首选技术栈。但不少开发者迁移后却纷纷吐槽“不够快”——开发时冷启动卡顿、热更新延迟,生产环境首屏加载缓慢、打包体积臃肿。其实不是 Vue3 和 Vite 不给力,而是你的配置和用法没到位!今天就结合实战经验,分享一套从开发期到生产期的全方位性能优化技巧,把这套组合的性能压榨到极致,让你的项目开发飞起、运行丝滑✨

一、先搞懂:Vite 快的核心原理

在开始优化前,先简单理清 Vite 比传统构建工具(如 Webpack)快的核心逻辑,才能精准找到优化切入点,避免盲目操作。

Vite 的速度优势主要体现在两个阶段,吃透这两点,后续优化会更有方向:

  1. 开发期:原生 ESM + ESBuild 预构建:Vite 启动时不会打包整个项目,只需启动一个开发服务器,通过浏览器原生 ESM 加载源码;同时用 ESBuild(Go 语言编写)对 node_modules 中的依赖进行预构建,比 Webpack 的 JS 编写的构建器快 10-100 倍,冷启动速度大幅提升,相当于“打开一扇门就能进房间,不用拆了整个房子重建”。
  2. 生产期:Rollup 深度优化打包:生产环境下,Vite 会切换到 Rollup 进行打包(Rollup 对 ES 模块的 tree-shaking 更彻底),配合一系列优化配置,能最大程度精简打包体积,兼顾速度和体积双重优势。

小提醒:很多开发者误以为“用了 Vite 就一定快”,其实默认配置下,面对大型项目或不合理的依赖引入,依然会出现性能瓶颈——这也是我们今天优化的核心意义。

二、开发期优化:告别卡顿,提升开发体验

开发期的优化核心是“降低启动时间、减少热更新延迟”,让我们在写代码时不用等待,专注开发本身。以下技巧均经过实战验证,直接复制配置即可生效。

1. 依赖预构建优化:精准控制预构建范围

Vite 会自动预构建 node_modules 中的依赖,但默认配置可能会预构建一些不必要的依赖,或遗漏常用依赖,导致启动速度变慢。我们可以手动配置 optimizeDeps,精准控制预构建范围。

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src') // 路径别名,减少路径查找时间
    }
  },
  // 依赖预构建优化
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia', 'axios'], // 强制预构建常用依赖
    exclude: ['some-large-library'], // 排除大型第三方库(如echarts,按需引入即可)
    cacheDir: '.vite', // 缓存预构建结果,提升二次启动速度(默认就是.vite,可自定义路径)
  }
})

优化点说明:include 配置常用依赖,避免 Vite 重复判断是否需要预构建;exclude 排除大型库,避免预构建体积过大;路径别名不仅方便开发,还能减少 Vite 的路径查找时间,一举两得。

2. HMR 优化:解决热更新延迟问题

热更新(HMR)是开发期高频使用的功能,若出现延迟(修改代码后几秒才生效),会严重影响开发效率。尤其是在 Windows 或 Docker 环境下,大概率是文件监听配置不合理导致的,可通过以下配置优化:

// vite.config.ts 新增 server 配置
server: {
  watch: {
    usePolling: true, // Windows/Docker 环境必加,解决文件监听不灵敏问题
    ignored: ['**/node_modules/**', '**/.git/**', '**/dist/**'], // 忽略无需监听的目录
    interval: 100, // 监听间隔,单位ms,默认100,可根据需求调整
  },
  open: true, // 启动后自动打开浏览器
  port: 3000, // 固定端口,避免每次启动随机端口
  strictPort: true, // 端口被占用时,直接报错(避免自动切换端口导致的配置错乱)
}

补充:若项目体积过大,可额外配置 server.hmr.overlay: false,关闭热更新错误提示层(错误提示会打印到控制台),也能轻微提升热更新速度。

3. 多页面应用(MPA)优化:独立构建,提升效率

若你的项目是多页面应用(如后台管理系统 + 前台展示页面),默认配置下会构建所有页面,启动速度较慢。可通过配置多入口,让每个页面独立构建,按需加载:

// vite.config.ts 新增 build 配置
build: {
  rollupOptions: {
    input: {
      main: resolve(__dirname, 'index.html'), // 主页面入口
      admin: resolve(__dirname, 'admin.html'), // 后台页面入口
      mobile: resolve(__dirname, 'mobile.html') // 移动端页面入口
    },
  },
}

优化效果:启动时只会构建当前访问的页面,其他页面不加载,冷启动速度提升 50% 以上;打包时也能独立打包每个页面,后续部署可按需部署,降低部署成本。

三、生产期优化:精简体积,提升运行速度

生产期的优化核心是“减小打包体积、提升首屏加载速度”——用户不会等待一个加载十几秒的页面,首屏加载速度直接影响用户留存。以下优化从“体积精简、加载提速、性能监控”三个维度展开,覆盖生产期全场景。

1. 代码分割:合理分包,减少首屏加载体积

默认打包会将所有代码合并成一个大文件,首屏加载时需要加载整个文件,速度较慢。通过代码分割,将代码拆分成多个小文件,按需加载,能显著提升首屏加载速度。

// vite.config.ts build 配置新增
build: {
  rollupOptions: {
    output: {
      // 自定义分包策略
      manualChunks: {
        'vue-vendor': ['vue', 'vue-router', 'pinia'], // Vue 核心依赖打包成一个文件
        'ui-vendor': ['element-plus', 'ant-design-vue'], // UI 组件库打包成一个文件
        'utils': ['lodash-es', 'dayjs', 'axios'], // 工具库打包成一个文件
      },
      // 静态资源命名规范,便于缓存
      assetFileNames: 'assets/[name]-[hash].[extname]',
      chunkFileNames: 'chunks/[name]-[hash].js',
      entryFileNames: 'entry/[name]-[hash].js',
    },
  },
  // 开启压缩(默认开启,可进一步优化)
  minify: 'esbuild', // 用 esbuild 压缩,速度快;需要更极致压缩可改用 'terser'
}

优化逻辑:将核心依赖、UI 库、工具库分别打包,这些文件变更频率低,可利用浏览器缓存(后续用户访问时无需重新加载);业务代码单独打包,变更频率高,减小每次更新的加载体积。

2. 静态资源优化:减小传输体积,减少请求次数

前端项目中,图片、字体等静态资源往往是打包体积的“大头”,合理优化静态资源,能快速减小打包体积,提升加载速度。

(1)图片优化
// vite.config.ts 新增 assets 配置
build: {
  assetsInlineLimit: 4096, // 小于 4KB 的图片转 base64,减少 HTTP 请求
}
// 额外安装 vite-plugin-imagemin 插件,实现图片压缩(可选,需手动安装)
import imagemin from 'vite-plugin-imagemin'

plugins: [
  vue(),
  imagemin({
    gifsicle: { optimizationLevel: 7, interlaced: false }, // gif 压缩
    optipng: { optimizationLevel: 7 }, // png 压缩
    mozjpeg: { quality: 80 }, // jpg 压缩
    pngquant: { quality: [0.7, 0.8], speed: 4 }, // png 深度压缩
  })
]

补充建议:开发时尽量使用 WebP/AVIF 格式图片(体积比 JPG/PNG 小 30%-50%),可通过 picture 标签做降级兼容,兼顾兼容性和体积。

(2)字体优化

字体文件往往体积较大,可通过“按需引入字体子集”“压缩字体”优化:

  1. 使用 font-spider 工具,提取项目中实际用到的字体字符,生成字体子集(删除未用到的字符,体积可减小 80% 以上);
  2. 将字体文件放在 CDN 上,通过 preload 预加载关键字体,避免字体加载延迟导致的“闪屏”问题。

3. 组件懒加载:按需加载,减少首屏渲染压力

Vue3 提供了路由级懒加载和组件级懒加载两种方式,能有效减少首屏需要加载的组件数量,提升首屏渲染速度,尤其适合大型项目。

(1)路由级懒加载(最基础、最推荐)
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  {
    path: '/',
    name: 'Home',
    // 路由懒加载:点击路由时才加载对应的组件
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue')
  },
  {
    path: '/admin',
    name: 'Admin',
    // 嵌套路由也支持懒加载
    component: () => import('@/views/Admin/Admin.vue'),
    children: [
      { path: 'dashboard', component: () => import('@/views/Admin/Dashboard.vue') }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router
(2)组件级懒加载(针对大型组件)

对于体积较大的组件(如富文本编辑器、图表组件),即使在当前路由中,也可通过 defineAsyncComponent 实现懒加载,用到时再加载:

// 组件中使用
首页
    <!-- 懒加载大型组件 -->
    <HeavyComponent v-if="showHeavyComponent" />
    <button @显示大型组件<script setup 
import { ref, defineAsyncComponent } from 'vue'

// 定义异步组件(懒加载)
const HeavyComponent = defineAsyncComponent(() => import('@/components/HeavyComponent.vue'))

const showHeavyComponent = ref(false)
(3)第三方组件按需引入

若使用 Element Plus、Ant Design Vue 等 UI 组件库,一定要开启按需引入,避免打包整个组件库(体积会增加几百 KB):

// vite.config.ts 配置 Element Plus 按需引入
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

plugins: [
  vue(),
  Components({
    resolvers: [ElementPlusResolver()], // 自动按需引入 Element Plus 组件
  })
]

注意:无需手动引入组件和样式,插件会自动识别模板中使用的组件,按需打包对应的组件和样式。

4. 性能监控:精准定位性能瓶颈

优化完成后,需要通过工具监控性能,确认优化效果,同时定位未优化到位的瓶颈。推荐两个常用工具,简单易上手:

(1)打包体积分析:rollup-plugin-visualizer

通过该插件,可生成打包体积分析图,清晰看到每个模块的体积占比,快速找到体积过大的模块:

// 安装插件:npm i rollup-plugin-visualizer -D
import { visualizer } from 'rollup-plugin-visualizer'

plugins: [
  vue(),
  // 打包体积分析
  visualizer({
    open: true, // 打包完成后自动打开分析图
    gzipSize: true, // 显示 gzip 压缩后的体积
    brotliSize: true, // 显示 brotli 压缩后的体积
  })
]

使用方法:执行 npm run build 后,会在 dist 目录下生成 stats.html 文件,打开后即可看到体积分析图,针对性优化体积过大的模块。

(2)浏览器性能监控:Lighthouse

Chrome 浏览器自带的 Lighthouse 工具,可全面检测页面的性能、可访问性、SEO 等指标,给出具体的优化建议:

  1. 打开 Chrome 开发者工具(F12),切换到 Lighthouse 标签;
  2. 勾选“Performance”(性能),点击“Generate report”;
  3. 等待检测完成,根据报告中的“Opportunities”(优化机会),进一步优化性能。

四、TS 集成优化:兼顾类型安全与性能

现在很多 Vue3 项目都会搭配 TypeScript 使用,TS 虽能提升代码可维护性,但也可能带来性能损耗(如类型检查耗时过长),可通过以下配置优化:

// tsconfig.json 核心配置优化
{
  "compilerOptions": {
    "target": "es2020", // 目标 ES 版本,匹配 Vite 构建目标
    "module": "esnext", // 模块格式,支持 ESM
    "experimentalDecorators": true, // 支持装饰器(若使用)
    "useDefineForClassFields": true,
    "isolatedModules": true, // 提升大型项目类型检查效率
    "skipLibCheck": true, // 跳过第三方库的类型检查,减少耗时
    "noEmit": true, // 只做类型检查,不生成编译文件(Vite 负责构建)
    "strict": true, // 开启严格模式,兼顾类型安全
    "moduleResolution": "bundler", // 让 TS 使用 Vite 的模块解析逻辑,避免冲突
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules"]
}

优化点说明:skipLibCheck 跳过第三方库类型检查,可大幅减少类型检查耗时;isolatedModules 开启后,TS 会将每个文件视为独立模块,提升构建和类型检查效率;moduleResolution: "bundler" 避免 TS 和 Vite 的模块解析逻辑冲突,减少报错。

五、实战总结:优化前后对比 & 避坑指南

1. 优化前后效果对比(大型 Vue3 + Vite + TS 项目)

优化维度 优化前 优化后 提升比例
开发期冷启动时间 8-10 秒 1-2 秒 80%+
热更新延迟 2-3 秒 ≤300ms 85%+
生产打包体积(未压缩) 1.2MB 450KB 62.5%
首屏加载时间(3G 网络) 8-10 秒 2-3 秒 70%+

2. 常见避坑点(必看)

  • 不要盲目开启所有优化:按需优化即可,比如小型项目无需配置多页面入口、手动分包,反而会增加配置复杂度;
  • 避免过度压缩:用 terser 压缩虽能减小体积,但会增加打包时间,大型项目可权衡选择,小型项目用 esbuild 足够;
  • 图片转 base64 要适度:大于 4KB 的图片不建议转 base64,会增加 JS 文件体积,反而拖慢首屏加载;
  • 第三方库优化优先:很多时候性能瓶颈来自第三方库(如 echarts、xlsx),优先考虑按需引入、CDN 引入,而非自己优化源码。

六、结尾互动

以上就是 Vue3 + Vite 从开发到生产的全方位性能优化实战技巧,所有配置均经过真实项目验证,直接复制就能用!

你在使用 Vue3 + Vite 时,还遇到过哪些性能问题?比如冷启动卡顿、打包体积过大、热更新失效等,欢迎在评论区留言讨论,一起解决前端性能难题~

如果觉得这篇文章对你有帮助,别忘了点赞、收藏、关注,后续会分享更多 Vue3、Vite、TS 相关的实战干货!

掘金标签推荐:#前端 #Vue3 #Vite #性能优化 #TypeScript(3-5 个标签,贴合主题,提升曝光)

Vue3 封装 Axios 实战:从基础到生产级,新手也能秒上手

在 Vue3 项目开发中,Axios 是最常用的 HTTP 请求库,但直接在组件中裸写 Axios 会导致代码冗余、难以维护——比如每个请求都要写重复的 baseURL、请求头、错误处理,接口变更时要改遍所有组件。

合理封装 Axios 能解决这些问题:统一管理请求配置、全局处理拦截器、标准化错误提示、支持取消重复请求……既能提升开发效率,又能让代码更健壮。

今天这篇文章,就带你从零实现 Vue3 + Vite 项目中 Axios 的生产级封装,从基础结构到进阶优化,每一步都有完整代码示例,直接复制就能用!适配 Vue3 组合式 API(

一、前置准备:安装 Axios

首先确保你的 Vue3 项目已搭建完成(推荐用 Vite 搭建),然后安装 Axios,TS 项目需额外安装类型声明:

# 安装核心 Axios 库
npm install axios
# 可选:TS 项目必装(提供类型提示,避免报错)
npm install @types/axios --save-dev

二、基础版封装:核心结构(新手友好)

基础版封装聚焦「统一配置 + 简化调用」,适合小型项目或新手入门,核心实现 3 个功能:统一 baseURL、全局请求/响应拦截、简化请求调用。

封装步骤:在 src 目录下新建 utils/request.js(JS 项目)或 utils/request.ts(TS 项目),作为 Axios 封装的核心文件。

2.1 JS 版本(基础版)

// src/utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus' // 可选:结合UI库做错误提示(推荐)

// 1. 创建 Axios 实例,配置基础参数
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL, // 环境变量(推荐,区分开发/生产)
  timeout: 5000, // 超时时间(单位:ms),超过则中断请求
  headers: {
    'Content-Type': 'application/json;charset=utf-8' // 默认请求头
  }
})

// 2. 请求拦截器(请求发送前执行)
// 作用:添加token、统一修改请求参数格式等
service.interceptors.request.use(
  (config) => {
    // 示例:添加token(登录后存储在localStorage,根据实际项目调整)
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}` // 拼接token格式(后端约定)
    }
    return config // 必须返回config,否则请求会中断
  },
  (error) => {
    // 请求发送失败(如网络中断、参数错误)
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error) // 抛出错误,供组件捕获处理
  }
)

// 3. 响应拦截器(请求返回后执行,先于组件接收)
// 作用:统一处理响应数据、拦截错误(如token过期、接口报错)
service.interceptors.response.use(
  (response) => {
    // 只返回响应体中的data(多数后端接口会包裹一层code/message/data)
    const res = response.data

    // 示例:根据后端约定的code判断请求是否成功(常见约定:200=成功)
    if (res.code !== 200) {
      // 非200状态码,视为业务错误(如参数错误、权限不足)
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data // 返回真正的业务数据,组件可直接使用
  },
  (error) => {
    // 响应失败(如超时、后端报错、404/500状态码)
    let errorMsg = '请求异常,请联系管理员'
    // 区分不同错误类型,给出更精准提示
    if (error.response) {
      // 有响应,但状态码非2xx(如401token过期、404接口不存在、500后端报错)
      switch (error.response.status) {
        case 401:
          errorMsg = '登录已过期,请重新登录'
          // 额外操作:清除过期token,跳转到登录页(结合Vue Router)
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 404:
          errorMsg = '请求的接口不存在'
          break
        case 500:
          errorMsg = '后端服务异常,请稍后重试'
          break
        default:
          errorMsg = error.response.data?.message || errorMsg
      }
    } else if (error.request) {
      // 无响应(如网络中断、超时)
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装常用请求方法(get/post/put/delete),简化组件调用
// get请求:params传参(拼接在URL后)
export const get = (url, params = {}) => {
  return service({
    url,
    method: 'get',
    params
  })
}

// post请求:data传参(请求体中)
export const post = (url, data = {}) => {
  return service({
    url,
    method: 'post',
    data
  })
}

// put请求(修改数据)
export const put = (url, data = {}) => {
  return service({
    url,
    method: 'put',
    data
  })
}

// delete请求(删除数据)
export const del = (url, params = {}) => {
  return service({
    url,
    method: 'delete',
    params
  })
}

// 导出Axios实例(特殊场景可直接使用,如取消请求)
export default service

2.2 TS 版本(基础版,补充类型提示)

TS 项目需添加类型声明,避免类型报错,提升开发体验,核心修改的是「请求/响应类型」和「参数类型」:

// src/utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'

// 定义后端响应的统一格式(根据你的后端接口调整)
interface ResponseData<T = any> {
  code: number
  message: string
  data: T
}

// 1. 创建Axios实例
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 2. 请求拦截器
service.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token = localStorage.getItem('token')
    if (token && config.headers) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error: AxiosError) => {
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器
service.interceptors.response.use(
  (response: AxiosResponse<ResponseData>) => {
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data // 返回业务数据,自动推导类型
  },
  (error: AxiosError<ResponseData>) => {
    let errorMsg = '请求异常,请联系管理员'
    if (error.response) {
      const status = error.response.status
      switch (status) {
        case 401:
          errorMsg = '登录已过期,请重新登录'
          localStorage.removeItem('token')
          window.location.href = '/login'
          break
        case 404:
          errorMsg = '请求的接口不存在'
          break
        case 500:
          errorMsg = '后端服务异常,请稍后重试'
          break
        default:
          errorMsg = error.response.data?.message || errorMsg
      }
    } else if (error.request) {
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装请求方法,添加类型声明
// get请求
export const get = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'get',
    params,
    ...config
  })
}

// post请求
export const post = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'post',
    data,
    ...config
  })
}

// put请求
export const put = <T = any>(url: string, data?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'put',
    data,
    ...config
  })
}

// delete请求
export const del = <T = any>(url: string, params?: Record<string, any>, config?: AxiosRequestConfig): Promise<T> => {
  return service({
    url,
    method: 'delete',
    params,
    ...config
  })
}

export default service

2.3 环境变量配置(关键步骤)

上面封装中用到的import.meta.env.VITE_API_BASE_URL,是 Vite 的环境变量,用于区分「开发环境」和「生产环境」的接口地址,避免手动修改。

在项目根目录新建 2 个文件:.env.development(开发环境)和 .env.production(生产环境):

# .env.development(开发环境,npm run dev 时生效)
VITE_API_BASE_URL = 'http://localhost:3000/api' # 本地后端接口地址

# .env.production(生产环境,npm run build 时生效)
VITE_API_BASE_URL = 'https://api.yourdomain.com' # 线上后端接口地址

注意:Vite 环境变量必须以VITE_ 开头,否则无法读取。

2.4 组件中如何使用(简化调用)

封装完成后,在 Vue3 组件(支持

<script setup>
// 导入封装好的请求方法
import { get, post } from '@/utils/request'
import { ref, onMounted } from 'vue'

const userList = ref([])

// 1. get请求(获取用户列表,params传参)
const getUserList = async () => {
  try {
    // 直接调用,无需写baseURL、请求头
    const res = await get('/user/list', { page: 1, size: 10 })
    userList.value = res // 直接使用响应数据(已过滤外层code/message)
  } catch (error) {
    // 可选:组件内单独处理错误(全局已处理过,这里可省略)
    console.log('获取用户列表失败:', error)
  }
}

// 2. post请求(提交表单,data传参)
const submitForm = async (formData) => {
  try {
    const res = await post('/user/add', formData)
    ElMessage.success('提交成功')
  } catch (error) {
    // 无需额外提示,全局响应拦截器已做错误提示
  }
}

// 页面挂载时调用get请求
onMounted(() => {
  getUserList()
})
</script>

对比裸写 Axios,封装后的调用更简洁,且所有请求的配置、错误处理都统一管理,后续修改接口地址、token 格式,只需改 request.js/ts 一个文件。

三、进阶版封装:生产级优化(必看)

基础版封装能满足小型项目,但在中大型项目中,还需要补充「取消重复请求、请求loading、接口加密、异常重试」等功能,让封装更健壮、更贴合生产需求。

3.1 优化1:取消重复请求(避免接口冗余)

场景:用户快速点击两次按钮,会发起两次相同的请求(如提交表单),导致后端重复处理。解决方案:用 Axios 的 CancelToken(Axios 0.x)或 AbortController(Axios 1.x+)取消重复请求。

以下是 Axios 1.x+ 版本(当前最新版)的实现方式(AbortController 更规范):

// src/utils/request.js(仅修改新增部分,其余代码不变)
import axios from 'axios'
import { ElMessage } from 'element-plus'

// 存储正在请求的接口(key:请求标识,value:AbortController实例)
const pendingRequests = new Map()

// 生成请求标识(url + method + 参数,确保唯一)
const generateRequestKey = (config) => {
  const { url, method, params, data } = config
  // 序列化参数,避免相同请求因参数顺序不同被误判为不同请求
  const paramsStr = JSON.stringify(params || {})
  const dataStr = JSON.stringify(data || {})
  return `${url}-${method}-${paramsStr}-${dataStr}`
}

// 取消重复请求
const cancelPendingRequest = (config) => {
  const requestKey = generateRequestKey(config)
  // 如果有重复请求,取消之前的
  if (pendingRequests.has(requestKey)) {
    const controller = pendingRequests.get(requestKey)
    controller.abort() // 取消请求
    pendingRequests.delete(requestKey) // 移除取消的请求
  }
}

// 1. 创建Axios实例(新增signal配置)
const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json;charset=utf-8'
  }
})

// 2. 请求拦截器(修改:添加取消重复请求逻辑)
service.interceptors.request.use(
  (config) => {
    // 取消重复请求(发起当前请求前,取消之前相同的请求)
    cancelPendingRequest(config)
    // 创建AbortController实例,用于取消请求
    const controller = new AbortController()
    config.signal = controller.signal
    // 存储当前请求
    const requestKey = generateRequestKey(config)
    pendingRequests.set(requestKey, controller)
    
    // 添加token(原有逻辑不变)
    const token = localStorage.getItem('token')
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  (error) => {
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器(修改:移除已完成的请求)
service.interceptors.response.use(
  (response) => {
    const config = response.config
    const requestKey = generateRequestKey(config)
    pendingRequests.delete(requestKey) // 请求完成,移除存储
    
    const res = response.data
    if (res.code !== 200) {
      ElMessage.error(res.message || '接口请求失败,请重试')
      return Promise.reject(new Error(res.message || '请求失败'))
    }
    return res.data
  },
  (error) => {
    // 处理取消请求的错误(单独捕获,不提示用户)
    if (axios.isCancel(error)) {
      console.log('请求已取消:', error.message)
      return Promise.reject(new Error('请求已取消'))
    }
    
    // 移除失败的请求
    if (error.config) {
      const requestKey = generateRequestKey(error.config)
      pendingRequests.delete(requestKey)
    }
    
    // 原有错误处理逻辑不变
    let errorMsg = '请求异常,请联系管理员'
    if (error.response) {
      // ... 原有状态码判断逻辑
    } else if (error.request) {
      errorMsg = '网络异常或请求超时,请检查网络'
    }
    ElMessage.error(errorMsg)
    return Promise.reject(error)
  }
)

// 4. 封装请求方法(不变)
export const get = (url, params = {}) => { /* ... */ }
export const post = (url, data = {}) => { /* ... */ }
// ... 其余方法

3.2 优化2:全局请求 Loading(提升交互体验)

场景:请求耗时较长时,用户不知道是否在加载,容易重复点击。解决方案:添加全局 Loading,所有请求发起时显示 Loading,全部请求完成后隐藏。

结合 Element Plus 的 ElLoading 实现(需安装 Element Plus):

// src/utils/request.js(新增Loading相关逻辑)
import axios from 'axios'
import { ElMessage, ElLoading } from 'element-plus'

// 新增:Loading实例和请求计数
let loadingInstance = null // Loading实例
let requestCount = 0 // 请求计数器(避免多个请求重复显示/隐藏Loading)

// 显示Loading
const showLoading = () => {
  if (requestCount === 0) {
    // 只有当没有请求时,才显示Loading
    loadingInstance = ElLoading.service({
      lock: true,
      text: '加载中...',
      background: 'rgba(0, 0, 0, 0.5)'
    })
  }
  requestCount++
}

// 隐藏Loading
const hideLoading = () => {
  requestCount--
  if (requestCount === 0) {
    // 所有请求完成后,才隐藏Loading
    loadingInstance?.close()
  }
}

// 1. 创建Axios实例(不变)
const service = axios.create({ /* ... */ })

// 2. 请求拦截器(新增:显示Loading)
service.interceptors.request.use(
  (config) => {
    showLoading() // 发起请求时显示Loading
    // ... 原有取消重复请求、添加token逻辑
    return config
  },
  (error) => {
    hideLoading() // 请求失败,隐藏Loading
    ElMessage.error('请求发送失败,请检查网络或参数')
    return Promise.reject(error)
  }
)

// 3. 响应拦截器(新增:隐藏Loading)
service.interceptors.response.use(
  (response) => {
    hideLoading() // 请求成功,隐藏Loading
    // ... 原有移除重复请求、处理响应逻辑
    return res.data
  },
  (error) => {
    hideLoading() // 响应失败,隐藏Loading
    // ... 原有错误处理逻辑
    return Promise.reject(error)
  }
)

注意:requestCount 计数器是关键,避免多个请求同时发起时,单个请求完成就隐藏 Loading。

3.3 优化3:接口模块化管理(中大型项目必做)

场景:项目接口较多时,所有请求都写在组件中,会导致代码混乱,后续维护困难。解决方案:将接口按模块拆分,统一管理在 api 文件夹中。

步骤:在 src 目录下新建api 文件夹,按业务模块拆分文件(如 api/user.jsapi/goods.js):

// src/api/user.js(用户模块接口)
import { get, post, put, del } from '@/utils/request'

// 接口模块化封装,每个接口对应一个函数
export const userApi = {
  // 获取用户列表
  getUserList: (params) => get('/user/list', params),
  // 添加用户
  addUser: (data) => post('/user/add', data),
  // 修改用户信息
  editUser: (id, data) => put(`/user/${id}`, data),
  // 删除用户
  deleteUser: (id) => del('/user/delete', { id }),
  // 用户登录
  login: (data) => post('/user/login', data)
}

// src/api/goods.js(商品模块接口)
import { get, post } from '@/utils/request'

export const goodsApi = {
  // 获取商品详情
  getGoodsDetail: (id) => get(`/goods/${id}`),
  // 搜索商品
  searchGoods: (params) => get('/goods/search', params)
}

组件中使用时,直接导入对应模块的接口,代码更清晰、更易维护:

<script setup>
// 导入用户模块接口
import { userApi } from '@/api/user'
import { ref, onMounted } from 'vue'

const userList = ref([])

const getUserList = async () => {
  try {
    // 直接调用接口函数,参数清晰
    const res = await userApi.getUserList({ page: 1, size: 10 })
    userList.value = res
  } catch (error) {
    console.log(error)
  }
}

onMounted(() => {
  getUserList()
})
</script>

3.4 其他生产级优化(可选,按需添加)

  1. 请求重试:针对网络波动导致的请求失败,自动重试 1-2 次(避免用户手动重试),用 axios-retry 插件实现。
  2. 请求加密:敏感接口(如登录、支付)的参数加密(如 AES 加密),在请求拦截器中处理参数加密。
  3. 接口日志:开发环境打印请求/响应日志(便于调试),生产环境关闭日志(避免泄露敏感信息)。
  4. 自定义请求头:支持部分接口单独设置请求头(如文件上传接口设置 Content-Type: multipart/form-data)。

四、避坑指南(新手必看)

  1. 环境变量读取失败:Vite 环境变量必须以 VITE_ 开头,且只能在客户端代码中读取,不能在服务端代码中使用。
  2. token 失效未跳转:确保响应拦截器中 401 状态码的判断逻辑正确,且 window.location.href = '/login' 没有被注释,同时检查 token 是否正确存储/清除。
  3. 重复请求取消无效:请求标识(requestKey)必须唯一,确保 params 和 data 被正确序列化(避免因参数顺序不同导致标识不同)。
  4. Loading 闪烁:请求耗时过短(如 100ms 内完成),会导致 Loading 一闪而过,可添加 Loading 延迟显示(如 300ms 后显示,避免闪烁)。
  5. TS 类型报错:确保后端响应格式和定义的 ResponseData 接口一致,否则会出现类型不匹配报错。
  6. 文件上传接口失败:文件上传接口需单独设置请求头 'Content-Type': 'multipart/form-data',且传参用 FormData 格式。

五、总结

Vue3 封装 Axios 的核心是「统一管理 + 简化调用 + 异常处理」,从基础版的拦截器封装,到进阶版的重复请求取消、Loading 优化、接口模块化,一步步提升封装的健壮性和实用性。

总结几个关键要点:

  • axios.create() 创建实例,统一配置 baseURL、超时时间等。
  • 请求拦截器:添加 token、取消重复请求、显示 Loading。
  • 响应拦截器:统一处理响应数据、拦截错误(token 过期、404/500)、隐藏 Loading。
  • 中大型项目:接口按模块拆分,提升代码可维护性。
  • 生产环境:补充取消重复请求、请求加密等优化,让封装更健壮。

封装完成后,后续开发只需专注于业务逻辑,无需关注请求的底层配置,极大提升开发效率。本文的封装方案适配绝大多数 Vue3 项目,大家可根据自己的后端接口规范和业务需求,灵活调整拦截器逻辑和接口格式。

❌