Vue 3 前端工程化:架构、核心原理与生产实践
Vue 3 前端工程化:架构、核心原理与生产实践
摘要:本报告将从前端架构师的视角出发,深度剖析 Vue 3 全栈开发体系。我们将摒弃浅尝辄止的语法罗列,转而深入探讨如何利用 Feature-Sliced Design (FSD) 构建可扩展架构,如何配置 Vite 进行微粒化的构建优化,以及如何利用 Composition API 和 Pinia 管理复杂的业务状态。
1. 引言:现代前端工程化的演变与 Vue 3 的定位
在 2025 年的前端开发版图中,Vue 3 已经不再仅仅是一个用于构建用户界面的渐进式框架,而是演变成了一个能够支撑大规模企业级应用的完整生态系统。随着 Vue 3.4 和 3.5 版本的发布,以及 Vite 构建工具的统治级地位确立,前端工程化的标准已经被重新定义。企业不再满足于“页面能跑”,而是追求极致的构建速度、严格的类型安全、可维护的架构设计以及卓越的运行时性能。
2. 工程架构与项目结构设计
❓ 为什么要精心设计架构?
在大型项目中,如果不采用严格的架构模式,代码库很快就会陷入“依赖地狱”和由循环引用导致的维护噩梦。清晰的目录结构是为了解决可维护性和可扩展性问题,确保团队协作时不会互相踩脚。
2.1 包管理器的选型:pnpm 的统治地位
在 2025 年,pnpm 已经成为 Vue 3 生态系统的首选包管理器,逐渐取代了 npm 和 Yarn。
| 特性 | npm | Yarn (Berry) | pnpm |
|---|---|---|---|
| 依赖存储机制 | 扁平化 node_modules,每个项目重复拷贝 | PnP (Plug’n’Play) 或 node_modules | 全局内容寻址存储 (Content-addressable store),硬链接 |
| 磁盘空间效率 | 低 (重复占用) | 中/高 | 极高 (跨项目共享依赖,节省高达 80%) |
| 安装速度 | 中等 | 快 | 极快 (尤其是冷安装和 Monorepo 环境) |
| Monorepo 支持 | 基础 Workspaces | 优秀 | 卓越 (原生 Workspace 支持,避免幽灵依赖) |
工程洞察:
pnpm 的核心优势在于它使用硬链接(Hard Links)和符号链接(Symlinks)来管理依赖。这意味着,如果你的机器上有 10 个项目都使用了 vue@3.5.0,pnpm 只会在磁盘上保存一份 Vue 的物理文件,所有项目都链接到这个位置。这不仅节省了 GB 级别的空间,更避免了 npm 扁平化结构带来的“幽灵依赖”(即可以访问未在 package.json 中声明的依赖)的安全隐患。对于企业级 Monorepo 仓库,pnpm 几乎是唯一的正确选择。
📊 架构流程图解:pnpm 硬链接机制
2.2 架构模式演进:从扁平化到 FSD
传统的 Vue 项目通常采用按技术类型分层的“扁平化结构”(Flat Structure),即根目录下分为 components、views、store 等文件夹。
❓ 为什么要抛弃扁平化结构?
随着业务复杂度增加,相关的业务逻辑(如“用户管理”)被分散在 views(页面)、components(组件)、store(状态)三个不同的文件夹中。每次修改功能都要在三个文件夹间反复横跳,导致代码内聚性低,重构困难。
为了解决这一问题,现代 Vue 工程化推荐采用 模块化架构 (Modular Architecture) 或更严格的 Feature-Sliced Design (FSD)。
2.2.1 模块化架构 (Modular Monolith) & 2.2.2 Feature-Sliced Design (FSD)
推荐目录结构:
src/
├── app/ # 全局应用配置 (Providers, Global Styles)
│ ├── App.vue
│ ├── main.ts
│ └── router.ts # 聚合所有模块路由
├── modules/ # 业务领域模块
│ ├── auth/ # 用户认证模块
│ │ ├── components/ # 仅供 Auth 模块使用的组件
│ │ ├── composables/ # Auth 相关逻辑复用
│ │ ├── store/ # Auth Pinia Store
│ │ ├── api/ # Auth API 定义
│ │ └── views/ # Auth 页面 (Login, Register)
│ └── dashboard/ # 仪表盘模块
│ └── ...
├── shared/ # 跨模块共享的底层资源
│ ├── ui/ # 通用 UI 组件库 (Button, Input)
│ ├── utils/ # 通用工具函数 (Date formatting)
│ └── api/ # Axios 封装实例
└── assets/ # 静态资源
FSD 架构分层详解:
| 层级 (Layer) | 职责描述 | 示例内容 |
|---|---|---|
| App | 应用的入口,初始化环境、全局样式、Providers | main.ts, App.vue, Global Config |
| Processes | 跨页面的复杂业务流程 | 结账流程、用户注册向导 |
| Pages | 路由页面,由 Widgets 和 Features 组装而成 | HomePage.vue, UserProfilePage.vue |
| Widgets | 独立的、功能完整的 UI 区块 | Header, ProductList, UserCard |
| Features | 处理特定的用户交互场景,带来业务价值 | AddToCart, LikePost, ThemeSwitcher |
| Entities | 业务实体,包含数据模型和基础展示组件 | User, Product, Order (Models, API) |
| Shared | 基础设施,与具体业务无关的通用代码 | Button, Input, apiClient, useToggle |
架构洞察:
FSD 的精髓在于它的单向依赖原则。Feature 层可以引用 Entities 层和 Shared 层,但绝不能引用 Pages 层或 App 层。这种严格的约束迫使开发者在编写代码前思考:“这段逻辑属于业务实体,还是属于用户交互特性?”从而避免了代码库随着时间推移变成“大泥球”(Big Ball of Mud)。
🏢 生产实战案例:电商后台重构
某大型跨境电商 ERP 系统原先采用扁平化结构,导致 src/components 下堆积了 500+ 组件,修改“订单详情”时需要同时在 api/order.ts, views/Order.vue, store/order.ts 之间反复横跳。
实施 FSD 后:
-
创建
modules/order,将所有订单相关的 API、Store、Components 内聚。 -
定义
Shared层,提取CurrencyFormatter,DateRangePicker等通用资源。 -
定义
Widgets层,如OrderSummaryCard,它组合了Entities中的数据展示。
结果: 新人上手时间从 2 周缩短至 3 天,模块间耦合度降低,代码删除重构变得极度安全。
3. 构建工具链深度配置 (Vite & TypeScript)
❓ 为什么要深度配置 Vite?
npm create vue@latest 生成的配置仅适用于 Demo。生产环境需要处理跨域代理、打包体积压缩、自动化导入等复杂问题,这些都是默认配置不具备的。
3.1 Vite 生产环境配置全解
一个健壮的 vite.config.ts 需要处理路径别名、环境变量、代理配置、构建优化、压缩以及可视化分析。
import { defineConfig, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
import { visualizer } from 'rollup-plugin-visualizer';
import viteCompression from 'vite-plugin-compression';
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
// 导出配置函数,以便访问 `mode` 环境变量
export default defineConfig(({ mode }) => {
// 加载当前模式下的环境变量 (.env.development, .env.production)
// 第三个参数 '' 表示加载所有前缀的变量,不仅限于 VITE_
const env = loadEnv(mode, process.cwd(), '');
return {
// 基础路径,解决非根目录部署问题
base: env.VITE_BASE_URL || '/',
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
'@api': path.resolve(__dirname, './src/api'),
},
},
plugins: [
vue(),
// 自动导入 API (ref, reactive, watch 等)
AutoImport({
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
resolvers: [],
}),
// 组件自动按需导入 (Tree-shaking)
Components({
dts: 'src/components.d.ts',
resolvers: [],
}),
// 生产环境构建产物压缩 (Gzip/Brotli)
viteCompression({
algorithm: 'brotliCompress', // 优先使用 Brotli
ext: '.br',
threshold: 1024, // 仅压缩 > 1kb 的文件
deleteOriginFile: false,
}),
// 构建产物可视化分析
visualizer({
open: false,
filename: 'stats.html',
gzipSize: true,
brotliSize: true,
}),
],
build: {
target: 'esnext', // 利用现代浏览器特性
minify: 'esbuild', // 快 20-50 倍
sourcemap: mode === 'development',
chunkSizeWarningLimit: 1000,
rollupOptions: {
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('element-plus')) return 'vendor-ui';
if (id.includes('echarts')) return 'vendor-charts';
return 'vendor-core';
}
},
},
},
},
server: {
// ... 开发服务器配置
proxy: {
'/api': {
target: env.VITE_API_TARGET,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
};
});
配置详解与工程实践:
-
环境变量注入:通过
loadEnv动态加载.env文件。 -
Tree-Shaking (摇树优化):
unplugin-vue-components是现代 Vue 开发的标配,在构建时自动剔除未使用的组件代码。 -
压缩策略:优先使用 Brotli (
.br),其压缩率比 Gzip 高 15-20%。
3.2 TypeScript 严格配置 (tsconfig.json)
TypeScript 的配置直接决定了类型系统的健壮性。
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "bundler", // Vite 最佳实践
"strict": true,
"isolatedModules": true, // Vite 硬性要求
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"verbatimModuleSyntax": true // Vue 3.4+ 优化
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
📊 架构流程图解:Vite 构建流水线
graph LR
src[源代码 .vue/.ts] -->|esbuild| devServer[开发服务器 (内存中编译)]
devServer -->|HMR| browser[浏览器热更新]
src -->|Rollup| buildPipeline[生产构建流水线]
buildPipeline -->|AutoImport| shake[Tree Shaking]
shake -->|Terser/Esbuild| minify[代码压缩]
minify -->|Gzip/Brotli| compress[产物压缩]
compress --> dist[dist 目录]
4. 质量保证体系:ESLint 9 与 Prettier
❓ 为什么要使用 Lint 工具?
在多人协作中,有人用双引号,有人用单引号,有人不写分号。ESLint 和 Prettier 强制统一代码风格,避免无意义的 Code Review 争论,并自动发现潜在 Bug。
2025 年,ESLint 已经全面转向 Flat Config (eslint.config.js) 格式,废弃了旧的 .eslintrc。
4.1 Flat Config 配置实战
import js from '@eslint/js';
import pluginVue from 'eslint-plugin-vue';
import vueTsEslintConfig from '@vue/eslint-config-typescript';
import prettierConfig from '@vue/eslint-config-prettier';
import globals from 'globals';
export default [
{
name: 'app/ignores',
ignores: ['**/dist/**', '**/coverage/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/recommended'],
...vueTsEslintConfig(),
{
name: 'app/custom-rules',
files: ['**/*.{ts,mts,tsx,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser },
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off',
'vue/block-order': ['error', {
order: ['script', 'template', 'style']
}],
'vue/component-api-style': ['error', ['script-setup']], // 强制 Script Setup
}
},
...prettierConfig(), // 必须放在最后
];
4.2 Husky 与 Git Hooks
🏢 生产实战案例:团队代码规范强制落地
在一个 20 人的前端团队中,由于成员水平不一,经常出现 any 满天飞、缩进不统一的问题。
解决方案:
-
部署
husky+lint-staged。 -
配置
commit-msg钩子,强制 commit message 必须符合feat: xxx格式。 -
配置
pre-commit钩子,仅对暂存区(Staged)文件执行eslint --fix和prettier --write。
效果: 任何不符合规范的代码都无法被 commit 到本地仓库,从源头杜绝了“屎山”代码的产生,Code Review 时间减少 50%。
5. Vue 3 核心语法与组件设计全解
5.1 模板语法与核心指令 (Directives)
Vue 的模板语法是声明式的,旨在让数据驱动 DOM。
5.1.1 文本与属性绑定
-
文本插值:
{{ msg }}。注意:不能在 HTML 属性中使用,属性必须用v-bind。 -
v-bind (
:): 动态绑定属性。<!-- 基础用法 --> <div :id="dynamicId"></div> <!-- 绑定布尔值 (Vue 3 优化:如果 isButtonDisabled 为 false/null/undefined,disabled 属性会被移除) --> <button :disabled="isButtonDisabled">Click</button> <!-- 动态绑定对象 (一次性绑定多个属性) --> <div v-bind="{ id: 'container', class: 'wrapper' }"></div>
5.1.2 条件渲染: v-if vs v-show
-
v-if: 真实的条件渲染。如果是 false,元素根本不会存在于 DOM 中。
-
v-show: 仅仅是切换 CSS
display: none。元素始终存在于 DOM。
工程决策: 如果需要频繁切换(如 Tab 页签、Hover 提示),必须使用 v-show 以节省 DOM 销毁和重建的开销。如果运行时条件很少改变(如用户权限控制),使用 v-if。
5.1.3 列表渲染: v-for
<ul>
<!-- item 是元素,index 是索引 -->
<!-- :key 是必须的!它是 Vue 识别节点的唯一 ID -->
<li v-for="(item, index) in list" :key="item.id">
{{ item.name }}
</li>
</ul>
关键陷阱: 永远不要用 index 作为 key,除非列表是静态的且不会重新排序。使用 index 会导致 Vue 在 Diff 算法中复用错误的 DOM 状态(如输入框内容不更新)。
5.1.4 事件绑定: v-on (@)
<!-- 基础用法 -->
<button @click="count++">Add</button>
<!-- 传递参数与事件对象 -->
<button @click="handleClick('hello', $event)">Submit</button>
<!-- 事件修饰符 (工程中常用) -->
<form @submit.prevent="onSubmit">...</form> <!-- 阻止默认提交 -->
<div @click.stop="doSomething">...</div> <!-- 阻止冒泡 -->
5.2 组件通信机制全解 (Component Communication)
组件通信是 Vue 应用的血脉。我们将介绍 5 种核心模式。
5.2.1 父传子: Props (数据输入)
父组件通过属性传值,子组件通过 defineProps 接收。
<!-- Parent.vue -->
<Child title="Dashboard" :count="10" />
<!-- Child.vue -->
<script setup lang="ts">
// 使用 TypeScript 定义 Props,提供完整的类型推断
const props = defineProps<{
title: string;
count?: number; // 可选属性
}>();
// props.title 可以直接在 js 中使用,模板中直接用 title
</script>
5.2.2 子传父: Emits (事件输出)
子组件通过触发事件通知父组件,父组件监听该事件。
<!-- Parent.vue -->
<Child @change-page="handlePageChange" />
<!-- Child.vue -->
<script setup lang="ts">
// 声明抛出的事件,TypeScript 会校验事件名
const emit = defineEmits<{
(e: 'change-page', page: number): void;
(e: 'delete', id: string): void;
}>();
const onClick = () => {
emit('change-page', 2); // 触发事件
};
</script>
5.2.3 双向绑定: v-model (Vue 3.4+ defineModel)
这是父子同步数据的最佳实践。
<!-- Parent.vue -->
<CustomInput v-model="searchText" />
<!-- CustomInput.vue -->
<script setup lang="ts">
// Vue 3.4+: defineModel 宏自动处理了 props 和 emit
const model = defineModel<string>();
</script>
<template>
<input v-model="model" />
</template>
5.2.4 跨级通信: Provide / Inject
当多层嵌套(祖先 -> 孙子)时,Props 逐层传递太麻烦(Prop Drilling)。
// GrandParent.vue
import { provide } from 'vue';
// 注入一个响应式对象
provide('theme', ref('dark'));
// GrandChild.vue
import { inject } from 'vue';
// 接收数据,第二个参数是默认值
const theme = inject('theme', ref('light'));
5.2.5 属性透传: Attributes ($attrs)
如果父组件传了一些 props(如 class, style, id),但子组件没有用 defineProps 声明,这些属性会自动“透传”到子组件的根元素上。这对于封装基础 UI 组件(如 Button, Input)非常有用。
5.3 组件设计与调用模式
5.3.1 组件设计基础 (SFC)
Vue 组件通常是一个 .vue 单文件组件 (Single File Component)。
<!-- MyButton.vue -->
<script setup lang="ts">
// 1. 逻辑层:接收输入,处理状态
defineProps<{ type?: 'primary' | 'secondary' }>();
</script>
<template>
<!-- 2. 视图层:决定长什么样 -->
<button :class="['btn', type]">
<!-- 3. 插槽:允许父组件注入内容 -->
<slot>Default Text</slot>
</button>
</template>
<style scoped>
/* 3. 样式层:scoped 确保样式只影响当前组件 */
.btn { border-radius: 4px; }
</style>
5.3.2 组件调用
<script setup lang="ts">
// 1. 导入组件 (setup 语法糖下无需注册)
import MyButton from './MyButton.vue';
</script>
<template>
<!-- 2. 在模板中使用 -->
<MyButton type="primary">
Submit <strong>Now</strong> <!-- 插入到 slot 中 -->
</MyButton>
</template>
5.3.3 插槽系统 (Slots)
插槽是组件复用性的关键。
-
默认插槽:
<slot></slot> -
具名插槽: 多个插槽位置。
<!-- Layout.vue --> <header><slot name="header"></slot></header> <main><slot></slot></main><!-- 调用 --> <Layout> <template #header><h1>Title</h1></template> <p>Main content</p> </Layout>
5.4 Composition API 核心原理
5.4.1 脚本结构 <script setup>
这是 Vue 3 的标准写法,语法更加简洁,变量直接暴露给模板,不需要 return。
5.4.2 核心响应式 API:ref vs reactive
| 特性 | ref | reactive |
|---|---|---|
| 适用数据 | 基础类型 (String, Number) 及对象 | 仅对象 (Object, Array, Map) |
| 访问方式 | 必须通过 .value (如 count.value) |
直接访问 (如 state.count) |
| 解构 | 安全,不丢失响应性 | 危险,解构后会丢失响应性 |
| 推荐指数 | ⭐⭐⭐⭐⭐ (官方推荐,心智负担小) | ⭐⭐⭐ (特定场景使用) |
5.4.3 计算属性 Computed
用于根据现有数据派生出新数据,且具有缓存特性。只有当依赖变化时才会重新计算。
const count = ref(2);
const double = computed(() => count.value * 2);
5.5 Vue 3.4/3.5 新特性与语法糖
(此处保留原有 defineModel 和响应式解构内容,见上文双向绑定章节)
6. 组件化工程与高级 UI 模式
6.1 递归组件与树形结构
<script setup lang="ts">
defineProps<{ node: TreeNodeData }>();
</script>
<template>
<div class="node">
<span>{{ node.label }}</span>
<div v-if="node.children" class="children">
<!-- 递归调用自身 -->
<TreeNode v-for="child in node.children" :key="child.id" :node="child" />
</div>
</div>
</template>
6.2 异步组件与 Suspense
const HeavyChart = defineAsyncComponent({
loader: () => import('./HeavyChart.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
});
6.3 传送门 Teleport
应用场景: Modal、Toast、Tooltip 等需要脱离父组件 overflow: hidden 限制的 UI 元素。
🏢 生产实战案例:全局弹窗管理器
在复杂的 SaaS 系统中,弹窗层级管理是一个痛点。
方案:
-
在
App.vue根节点外放置<div id="modal-container"></div>。 -
封装
<BaseModal>组件,内部使用<Teleport to="#modal-container">。 -
结合 Pinia
useModalStore管理弹窗队列。
优势: 无论在多深的组件树中调用弹窗,它永远渲染在 Body 直子级,彻底解决了 z-index 冲突问题。
7. 状态管理工程化:Pinia 深度实践
7.1 为什么我们需要 Pinia?
在简单的应用中,数据通常遵循“单向数据流”:父组件通过 Props 把数据传给子组件。
痛点:Prop Drilling (属性透传)
当你的组件层级很深时(例如:App -> Layout -> Header -> UserProfile),如果 UserProfile 需要访问用户的登录信息,你必须把这个数据一层层往下传。中间的 Layout 和 Header 根本不需要这个数据,但被迫充当“搬运工”。
Pinia 的作用:
Pinia 就像一个挂在云端的“全局数据库”。
-
Store (仓库):存放数据的地方。
-
任何组件,无论在哪里,都可以直接从 Store 里取数据,或者修改数据。
-
它解决了跨组件通信难、数据流混乱的问题。
Pinia vs Vuex:
Pinia 是 Vuex 的进化版,是 Vue 官方现在唯一推荐的库。它更轻量(体积极小)、去掉了复杂的 Mutation 概念、且对 TypeScript 支持极其完美。
7.2 Pinia 基础概念与语法
在深入工程化模式前,先看懂 Pinia 的三个核心概念:
-
State (状态):就是数据,等同于组件里的
data或ref。 -
Getters (计算属性):基于 State 算出来的数据,等同于组件里的
computed。 -
Actions (动作):修改 State 的方法,支持异步(API请求),等同于组件里的
methods。
7.3 Setup Stores 模式 (推荐)
虽然 Pinia 支持类似 Vuex 的写法,但在企业级应用中,强烈推荐使用 Setup Stores。因为这种写法和我们平时写 Vue 组件(Composition API)完全一致,心智负担最小,且最灵活。
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useLocalStorage } from '@vueuse/core';
// 第一个参数 'counter' 是唯一 ID
export const useCounterStore = defineStore('counter', () => {
// 1. State: 直接定义 ref 变量
// 这里的 useLocalStorage 是一个 Composable,Pinia 完美支持组合式函数
const count = useLocalStorage('count', 0);
// 2. Getters: 使用 computed
const doubleCount = computed(() => count.value * 2);
// 3. Actions: 直接定义 function
function increment() {
count.value++;
}
// 异步 Action 示例
async function login() {
// await api.login()...
}
// 必须 return 出去,外部才能使用
return { count, doubleCount, increment, login };
});
在组件中使用:
<script setup>
import { useCounterStore } from '@/stores/counter';
const counter = useCounterStore();
// 直接读写,Pinia 会自动保持响应式
console.log(counter.count);
counter.increment(); // 调用 action
</script>
7.4 状态持久化策略
最佳实践配置:
{
persist: {
storage: localStorage,
paths: ['token', 'userPreferences'], // 仅持久化关键字段
debug: process.env.NODE_ENV === 'development',
}
}
📊 架构流程图解:Pinia 状态流转
8. 路由系统与中间件管道 (Router & Middleware)
❓ 为什么需要路由中间件?
Vue Router 默认的导航守卫 beforeEach 如果堆积了登录校验、权限判断、页面埋点、标题修改等所有逻辑,会变成一个巨大的 if-else 迷宫,难以维护。中间件模式将每个逻辑拆分为独立的函数。
8.1 中间件管道模式 (Middleware Pipeline)
借鉴后端框架的思想,将路由守卫拆分为独立的、可复用的中间件。
// middleware/pipeline.ts
export default function middlewarePipeline(context, middleware, index) {
const nextMiddleware = middleware[index];
if (!nextMiddleware) return context.next;
return () => {
const nextPipeline = middlewarePipeline(context, middleware, index + 1);
nextMiddleware({ ...context, next: nextPipeline });
};
}
// router/index.ts 应用
router.beforeEach((to, from, next) => {
if (!to.meta.middleware) return next();
const middleware = to.meta.middleware;
const context = { to, from, next, store: useAuthStore() };
return middleware[0]({
...context,
next: middlewarePipeline(context, middleware, 1)
});
});
🏢 生产实战案例:RBAC 权限控制
场景: 这是一个 CRM 系统,拥有“超级管理员”、“销售”、“客服”三种角色。
配置:
const routes = [
{
path: '/admin',
component: AdminDashboard,
meta: {
// 数组顺序决定执行顺序:先验登录,再验角色
middleware: [auth, checkRole(['admin'])]
}
}
];
流程: 路由跳转 -> auth 中间件检查 Token -> checkRole 中间件检查 userStore.role -> 通过则 next(),否则重定向到 403 页面。
9. 网络层封装与并发控制
❓ 为什么要封装 Axios?
直接在组件里写 axios.get('/api/user') 会导致 API URL 散落在各处,难以修改。更重要的是,我们需要统一处理 Token 过期自动刷新、全局错误提示(如 500 报错)以及请求防抖。
9.1 Axios 深度封装与 9.2 无感刷新 Token
解决方案:请求队列模式
// api/interceptor.ts
let isRefreshing = false;
let failedQueue: any[] = [];
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach(prom => {
if (error) prom.reject(error);
else prom.resolve(token);
});
failedQueue = [];
};
axiosInstance.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
// 加入队列,等待刷新完成
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(token => {
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return axiosInstance(originalRequest);
});
}
originalRequest._retry = true;
isRefreshing = true;
try {
const { token } = await refreshTokenApi();
processQueue(null, token); // 唤醒所有挂起的请求
return axiosInstance(originalRequest);
} catch (err) {
processQueue(err, null);
return Promise.reject(err);
} finally {
isRefreshing = false;
}
}
return Promise.reject(error);
}
);
📊 架构流程图解:Token 刷新风暴处理
10. 生产环境性能优化策略
10.1 手动分包 (Manual Chunks)
Vite 策略:
-
vendor-core: Vue, Pinia, Router (基础框架)
-
vendor-ui: Element Plus / Ant Design (UI 库,体积大,更新少)
-
vendor-charts: ECharts (可视化库,懒加载)
10.2 虚拟滚动 (Virtual Scrolling)
使用 TanStack Virtual 处理长列表。它不提供 UI 组件,只提供逻辑 Hook,允许开发者完全自定义 DOM 结构。
🏢 生产实战案例:日志监控平台
问题: 运维后台需要实时展示服务器传来的日志,数据量轻松突破 10 万行,直接渲染导致浏览器崩溃。
优化:
-
引入
@tanstack/vue-virtual。 -
设置
estimateSize估算行高。 -
只渲染视口内可见的 ~20 行 DOM。
结果: 无论日志积累到多少条,DOM 节点数恒定,内存占用平稳,滚动丝滑流畅。
11. 测试与可靠性 (Vitest)
11.1 Vitest 的优势
极速、与 Vite 共享配置、源码级调试。
11.2 组件测试实践
推荐使用 Testing Library 进行行为驱动测试(Testing Implementation Details is harmful)。
import { render, fireEvent } from '@testing-library/vue';
import Counter from './Counter.vue';
test('increments value on click', async () => {
const { getByText } = render(Counter);
const button = getByText('Increment');
await fireEvent.click(button);
// 断言用户可见的 DOM 变化,而不是组件内部的 count.value
getByText('Count: 1');
});
12. 结语
构建一个现代化的 Vue 3 应用,不仅需要掌握 ref 和 computed 的用法,更需要具备宏观的架构视野。从 FSD 架构的设计,到 Vite 的极致优化,再到 Pinia 和 Router 的高级模式应用,每一个环节都体现了前端工程化的深度。
2025 年的前端开发,是对规范、性能和体验的极致追求。希望本指南能成为你打造企业级 Vue 应用的坚实基石。
更多推荐
所有评论(0)