前言(扩展)
Vue.js自2014年发布以来,凭借"渐进式框架"的设计哲学,从一个轻量级视图库成长为覆盖全场景的前端解决方案。无论是个人开发者快速搭建原型,还是企业级团队开发复杂系统,Vue都能提供灵活的技术路径。本书的核心目标是**"让读者从'会用Vue'到'精通Vue工程化'"**,因此内容设计上遵循"基础→进阶→实战→扩展"的递进逻辑,每章均包含:
- 核心概念解析(结合原理图解,避免死记硬背)
- 完整代码示例(可直接复制运行的实战代码)
- 避坑指南(总结开发中常见的10+类问题及解决方案)
- 工程实践(从单文件组件到企业级架构的设计思路)
无论你是希望提升技能的前端开发者,还是需要构建完整项目的团队技术负责人,本书都能为你提供可落地的经验参考。
第一部分 基础篇:Vue核心能力构建
第1章 Vue 3快速上手:从0到1搭建第一个应用(详细)
1.1 Vue 3新特性深度解析(对比Vue 2)
Vue 3的升级不是简单的"修修补补",而是对核心架构的重构,以下是需要重点掌握的四大变革:
特性 | Vue 2 | Vue 3 | 核心价值 |
---|---|---|---|
响应式系统 | Object.defineProperty |
Proxy |
支持嵌套对象/数组的深层响应、更少的性能损耗 |
组合式API | 选项式API(data /methods ) |
setup /<script setup> |
逻辑复用更灵活、TypeScript友好 |
编译优化 | 全量编译 | 静态标记(PatchFlag) | 运行时性能提升(渲染速度+30%+) |
TypeScript支持 | 需手动配置 | 原生支持(defineComponent ) |
大型项目类型安全更有保障 |
示例:Proxy响应式原理简析
Vue 3通过Proxy
拦截对象的所有操作(get
/set
/deleteProperty
),相比Object.defineProperty
只能监听已有属性,Proxy
可以动态追踪新增/删除的属性,因此无需手动调用Vue.set
或Vue.delete
。
javascript
// Vue 3响应式核心代码简化版(源码参考:@vue/reactivity)
function reactive(target) {
return new Proxy(target, {
get(target, key) {
track(target, key); // 收集依赖(用于触发更新)
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key); // 触发依赖(更新视图)
return result;
}
});
}
1.2 开发环境搭建(保姆级步骤)
前置条件:需安装Node.js 16+(推荐v18),建议使用pnpm作为包管理器(更快、更节省磁盘空间)。
步骤1:创建Vue项目
使用Vue官方推荐的create-vue
工具(基于Vite):
bash
npm create vue@latest my-vue-app
根据提示选择配置(推荐勾选:TypeScript、Pinia、Vue Router、ESLint),最终生成的项目结构如下:
csharp
my-vue-app/
├── public/ # 静态资源(无需打包)
├── src/
│ ├── assets/ # 需打包的静态资源(图片、字体)
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia状态管理
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── vite.config.ts # Vite配置
├── tsconfig.json # TypeScript配置
└── package.json # 依赖管理
步骤2:启动项目
bash
cd my-vue-app
pnpm install # 安装依赖
pnpm dev # 启动开发服务器(默认访问http://localhost:5173)
1.3 第一个Vue应用:待办事项列表(含完整代码)
本实践将实现一个支持添加、删除、状态切换的待办事项列表,覆盖Vue 3核心语法。
步骤1:创建组件TodoList.vue
vue
<template>
<div class="todo-container">
<h1>待办事项</h1>
<!-- 输入框+添加按钮 -->
<div class="input-group">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="输入待办事项..."
/>
<button @click="addTodo">添加</button>
</div>
<!-- 待办列表 -->
<ul class="todo-list">
<li v-for="(todo, index) in todos" :key="index" :class="{ completed: todo.completed }">
<input type="checkbox" v-model="todo.completed" />
<span>{{ todo.text }}</span>
<button @click="removeTodo(index)">删除</button>
</li>
</ul>
<!-- 统计信息 -->
<p v-if="todos.length > 0">剩余未完成:{{ remainingCount }} / {{ todos.length }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
// 响应式数据:待办列表、输入框内容
const todos = ref([]);
const newTodo = ref('');
// 计算属性:剩余未完成数量
const remainingCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length;
});
// 方法:添加待办
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
text: newTodo.value.trim(),
completed: false
});
newTodo.value = ''; // 清空输入框
}
};
// 方法:删除待办
const removeTodo = (index) => {
todos.value.splice(index, 1);
};
</script>
<style scoped>
.todo-container {
max-width: 500px;
margin: 0 auto;
padding: 20px;
}
.input-group {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.completed span {
color: #999;
text-decoration: line-through;
}
</style>
步骤2:在App.vue
中使用组件
vue
<template>
<TodoList />
</template>
<script setup>
import TodoList from './components/TodoList.vue';
</script>
关键知识点总结:
<script setup>
:Vue 3的组合式API语法糖,无需显式导出组件选项,直接编写逻辑。ref
:创建基础类型(字符串、数字、布尔值)的响应式变量,访问时需用.value
。computed
:创建计算属性,自动缓存结果(仅依赖变化时重新计算)。v-for
:列表渲染,需为每个项提供唯一的:key
(推荐使用index
仅当列表顺序固定)。
第2章 组合式API:更灵活的逻辑组织方式(详细)
2.1 为什么需要组合式API?(对比选项式API的痛点)
选项式API(Options API)是Vue 2的核心设计,通过data
、methods
、computed
等选项组织代码。但在中大型项目中,其局限性逐渐显现:
- 逻辑分散:一个功能可能涉及多个选项(如数据、方法、计算属性),导致代码跳跃阅读。
- 复用困难 :多个组件共享逻辑需通过
mixins
,但mixins
存在命名冲突、来源不明确等问题。 - TypeScript支持弱:选项式API的类型推断需要额外配置,大型项目维护成本高。
组合式API(Composition API)通过setup
函数或<script setup>
语法糖,将逻辑按功能聚合(而非按选项拆分),完美解决了上述问题。
2.2 核心API详解(附实战案例)
2.2.1 setup
函数与<script setup>
setup
函数是组合式API的入口,执行时机在组件初始化之前(早于data
、computed
等选项)。而<script setup>
是其语法糖,更简洁且支持顶层await
。
示例:用setup
函数实现计数器
vue
<script>
export default {
setup() {
const count = ref(0);
const increment = () => { count.value++; };
return { count, increment }; // 暴露给模板
}
};
</script>
<!-- 等价的<script setup>写法 -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => { count.value++; };
</script>
2.2.2 生命周期钩子
组合式API提供了更细粒度的生命周期钩子(替代选项式API的mounted
、updated
等),需从vue
中导入。
生命周期钩子 | 触发时机 | 替代选项式API |
---|---|---|
onMounted |
组件挂载到DOM后 | mounted |
onUpdated |
组件更新后 | updated |
onUnmounted |
组件卸载前 | beforeUnmount |
onBeforeMount |
组件挂载前 | beforeMount |
实战:在组件挂载后获取DOM元素
vue
<script setup>
import { ref, onMounted } from 'vue';
const inputRef = ref(null);
onMounted(() => {
// 此时可安全访问DOM
inputRef.value.focus();
});
</script>
<template>
<input ref="inputRef" placeholder="自动聚焦" />
</template>
2.2.3 依赖注入(provide
/inject
)
跨层级组件通信时,传统的props
逐层传递("prop drilling")会导致代码冗余。provide
(提供)和inject
(注入)允许祖先组件向任意后代组件传递数据,无需经过中间组件。
实战:主题模式切换(全局状态传递)
vue
<!-- 祖先组件:App.vue -->
<script setup>
import { provide, ref } from 'vue';
const isDarkMode = ref(false);
// 提供一个响应式值(key为'theme')
provide('theme', isDarkMode);
</script>
<!-- 深层子组件:Header.vue -->
<script setup>
import { inject } from 'vue';
// 注入祖先提供的'theme'
const isDarkMode = inject('theme');
</script>
2.2.4 自定义Hook:逻辑复用的最佳实践
自定义Hook是组合式API的核心优势之一,它允许将通用逻辑(如表单校验、API请求)封装为可复用的函数,避免代码重复。
实战:封装"表单校验"Hook
需求:复用邮箱、手机号的校验规则,支持实时校验和错误提示。
javascript
// hooks/useFormValidation.js
import { ref, computed } from 'vue';
export function useFormValidation(initialValue, rules) {
const value = ref(initialValue);
const error = ref('');
// 校验函数
const validate = () => {
let isValid = true;
for (const rule of rules) {
if (rule.required && !value.value) {
error.value = rule.message || '此字段必填';
isValid = false;
break;
}
if (rule.pattern && !rule.pattern.test(value.value)) {
error.value = rule.message || '格式不正确';
isValid = false;
break;
}
}
return isValid;
};
return {
value,
error,
validate,
// 计算属性:是否校验通过
is_valid: computed(() => !error.value)
};
}
使用示例(表单组件)
vue
<script setup>
import { useFormValidation } from './hooks/useFormValidation';
// 校验规则:邮箱必填且符合正则
const emailRules = [
{ required: true, message: '请输入邮箱' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式错误' }
];
// 使用自定义Hook
const email = useFormValidation('', emailRules);
// 提交表单
const submitForm = () => {
if (email.validate()) {
console.log('提交成功,邮箱:', email.value);
}
};
</script>
<template>
<form @submit.prevent="submitForm">
<input v-model="email.value" placeholder="输入邮箱" />
<p v-if="email.error" style="color: red">{{ email.error }}</p>
<button type="submit">提交</button>
</form>
</template>
2.3 本章小结
组合式API的核心是**"逻辑聚合"**,通过将相关逻辑(如数据、方法、校验)封装在一起,大幅提升代码的可读性和可维护性。下一章我们将基于这些能力,深入探讨组件化开发的核心------组件通信与设计原则。
第3章 组件化开发:从原子组件到复杂页面(详细)
3.1 组件设计原则(附反例分析)
优秀的组件设计应遵循以下原则,否则可能导致代码冗余、维护困难:
原则 | 说明 | 反例 |
---|---|---|
单一职责 | 一个组件只负责一个功能点 | 一个"用户信息卡片"组件同时包含头像、昵称、订单列表、编辑按钮 |
高内聚低耦合 | 组件内部逻辑自包含,对外暴露清晰接口 | 组件内部直接操作DOM,依赖外部样式 |
可复用性 | 通过Props/Events抽象通用能力 | 为每个页面单独编写相似的列表组件 |
反例分析:冗余的"商品列表"组件
某团队早期为不同页面(首页、分类页、购物车页)分别编写了商品列表组件,每个组件的结构相似但筛选逻辑不同。后期维护时,修改一处需同步修改三个组件,耗时耗力。优化方案:抽象出通用ProductList
组件,通过filter
Prop接收不同的筛选规则,通过onItemClick
Event暴露点击事件。
3.2 组件通信全攻略(附场景化解决方案)
3.2.1 父子组件通信(最常用场景)
-
父→子:Props
父组件通过
props
向子组件传递数据,子组件需声明props
的类型、默认值等(TypeScript推荐使用defineProps
)。vue<!-- 父组件 --> <template> <ChildComponent :title="pageTitle" :items="listData" /> </template> <!-- 子组件 --> <script setup> // TypeScript方式声明Props(推荐) const props = defineProps<{ title: string; items?: string[]; // 可选,默认值为空数组 }>(); // 简写方式(非TS) // const props = defineProps(['title', 'items']); </script>
-
子→父:
$emit
子组件通过
defineEmits
声明事件,父组件通过@event
监听。vue<!-- 子组件 --> <script setup> const emit = defineEmits<{ (e: 'select', id: number): void; }>(); const handleSelect = (id) => { emit('select', id); // 触发事件 }; </script> <!-- 父组件 --> <template> <ChildComponent @select="onItemSelected" /> </template>
3.2.2 兄弟组件通信(无直接层级关系)
-
方案1:通过共同父组件转发 (推荐)
兄弟A触发事件→父组件接收并更新状态→父组件传递状态给兄弟B。
-
方案2:事件总线(Event Bus) (慎用,可能导致状态混乱)
创建一个全局的Vue实例作为事件中心,组件通过
$on
/$emit
通信。javascript// eventBus.js import { createApp } from 'vue'; export const eventBus = createApp({}); // 组件A(发送事件) import { eventBus } from './eventBus'; eventBus.config.globalProperties.$emit('sibling-event', data); // 组件B(接收事件) import { eventBus } from './eventBus'; eventBus.config.globalProperties.$on('sibling-event', (data) => { // 处理数据 });
3.2.3 跨层级组件通信(祖先→后代)
-
provide
/inject
(推荐)祖先组件通过
provide
提供数据,后代组件通过inject
注入,支持响应式。vue<!-- 祖先组件 --> <script setup> import { provide, ref } from 'vue'; const theme = ref('light'); provide('theme', theme); // 提供响应式值 </script> <!-- 深层子组件(任意层级) --> <script setup> import { inject } from 'vue'; const theme = inject('theme'); // 注入数据 </script>
3.2.4 插槽(Slot):组件的"扩展点"
插槽允许父组件向子组件传递内容(HTML、组件),是实现"通用容器"组件的关键。
- 默认插槽 :子组件用
<slot>
声明,父组件直接填充内容。 - 具名插槽 :子组件用
<slot name="header">
声明,父组件用v-slot:header
指定。 - 作用域插槽:子组件向父组件传递数据,父组件可自定义渲染逻辑(常用于列表组件)。
实战:通用卡片组件(含作用域插槽)
vue
<!-- Card.vue -->
<script setup>
import { defineProps, defineSlots } from 'vue';
const props = defineProps<{
title: string;
}>();
</script>
<template>
<div class="card">
<h3>{{ title }}</h3>
<!-- 作用域插槽:向父组件传递item数据 -->
<slot :item="props.item"></slot>
</div>
</template>
vue
<!-- 父组件使用 -->
<Card :title="商品详情" :item="currentProduct">
<!-- 自定义插槽内容,可访问子组件传递的item -->
<template #default="{ item }">
<p>价格:{{ item.price }}</p>
<p>库存:{{ item.stock }}</p>
</template>
</Card>
3.3 实战:开发通用弹窗组件(附完整代码)
本实践将开发一个支持自定义内容、动画、遮罩层关闭的弹窗组件,覆盖组件设计的多个核心知识点。
需求分析:
- 支持通过
v-model
控制显示/隐藏 - 自定义标题、内容、底部按钮
- 点击遮罩层关闭(可配置是否启用)
- 入场/离场动画(淡入淡出)
步骤1:定义组件Modal.vue
vue
<template>
<Transition name="modal-fade">
<div v-if="modelValue" class="modal-overlay" @click.self="handleClose">
<div class="modal-container">
<div class="modal-header">
<h3>{{ title }}</h3>
<button v-if="closable" @click="handleClose">×</button>
</div>
<div class="modal-body">
<slot></slot> <!-- 默认插槽:主要内容 -->
</div>
<div class="modal-footer" v-if="showFooter">
<slot name="footer"> <!-- 具名插槽:底部按钮 -->
<button @click="handleClose">取消</button>
<button type="primary" @click="handleConfirm">确认</button>
</slot>
</div>
</div>
</div>
</Transition>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
// 定义Props(TypeScript)
const props = defineProps<{
modelValue: boolean; // 控制显示(v-model绑定)
title?: string; // 标题(可选)
closable?: boolean; // 是否显示关闭按钮(默认true)
showFooter?: boolean; // 是否显示底部(默认true)
closeOnClickOverlay?: boolean; // 点击遮罩关闭(默认true)
}>();
// 定义Events
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void; // 更新v-model
(e: 'close'): void; // 关闭事件
(e: 'confirm'): void; // 确认事件
}>();
// 默认值处理
const handleClose = () => {
if (props.closable !== false) {
emit('update:modelValue', false);
emit('close');
}
};
const handleConfirm = () => {
emit('confirm');
handleClose();
};
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-container {
background: white;
border-radius: 8px;
width: 500px;
max-width: 90%;
}
.modal-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 16px;
max-height: 60vh;
overflow-y: auto;
}
.modal-footer {
padding: 16px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
/* 动画效果 */
.modal-fade-enter-active,
.modal-fade-leave-active {
transition: opacity 0.3s;
}
.modal-fade-enter-from,
.modal-fade-leave-to {
opacity: 0;
}
</style>
步骤2:在父组件中使用
vue
<script setup>
import { ref } from 'vue';
import Modal from './Modal.vue';
const isModalOpen = ref(false);
const openModal = () => {
isModalOpen.value = true;
};
const handleModalClose = () => {
console.log('弹窗已关闭');
};
const handleConfirm = () => {
console.log('用户点击了确认');
};
</script>
<template>
<button @click="openModal">打开弹窗</button>
<Modal
v-model="isModalOpen"
title="提示"
:closable="true"
@close="handleModalClose"
@confirm="handleConfirm"
>
<p>这是一段自定义内容</p>
<!-- 自定义底部按钮 -->
<template #footer>
<button @click="isModalOpen = false">稍后处理</button>
<button type="primary" @click="handleConfirm">立即确认</button>
</template>
</Modal>
</template>
关键知识点总结:
v-model
在组合式API中的实现:通过modelValue
Prop和update:modelValue
Event。- 作用域插槽:子组件通过
<slot :item="data">
传递数据,父组件用#default="{ item }"
接收。 - 过渡动画:使用Vue的
<Transition>
组件,通过CSS类名定义入场/离场效果。
第二部分 进阶篇:状态管理与工程化基础
第4章 Pinia:Vue 3的官方状态管理方案(详细)
4.1 为什么选择Pinia?(对比Vuex 4)
Vuex是Vue的经典状态管理库,但随着Vue 3的普及,其局限性逐渐凸显:
- 代码冗余 :需编写
state
、mutations
、actions
等多个对象,结构繁琐。 - TypeScript支持弱:类型推断需手动声明,大型项目易出错。
- 模块化不便 :模块间依赖需通过
namespaced
解决,不够直观。
Pinia作为Vuex的替代方案,针对上述问题做了优化,成为Vue 3的官方推荐状态管理库。
特性 | Vuex 4 | Pinia |
---|---|---|
API简洁性 | 需定义state /mutations /actions |
仅需defineStore 定义Store |
TypeScript支持 | 需额外配置 | 原生支持,自动类型推断 |
模块化管理 | 依赖namespaced |
天然模块化,按目录组织 |
组合式API风格 | 不支持 | 支持(与setup 无缝集成) |
4.2 核心概念与实战(附企业级案例)
4.2.1 Store的定义与使用
Pinia的核心是Store
(存储),通过defineStore
函数创建,每个Store可包含state
(状态)、getters
(计算属性)、actions
(方法)。
示例:定义用户Store
javascript
// stores/userStore.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
// 状态(响应式数据)
state: () => ({
id: null,
name: '',
avatar: '',
token: localStorage.getItem('token') || ''
}),
// Getters(计算属性,可访问state/actions)
getters: {
isLoggedIn: (state) => !!state.token,
fullName: (state) => `${state.name}(ID: ${state.id})`
},
// Actions(方法,可同步/异步)
actions: {
// 登录(异步)
async login(username, password) {
try {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
const data = await response.json();
this.id = data.id;
this.name = data.name;
this.avatar = data.avatar;
this.token = data.token;
localStorage.setItem('token', data.token);
} catch (error) {
console.error('登录失败', error);
}
},
// 登出(同步)
logout() {
this.id = null;
this.name = '';
this.avatar = '';
this.token = '';
localStorage.removeItem('token');
}
}
});
使用Store:
vue
<script setup>
import { useUserStore } from './stores/userStore';
const userStore = useUserStore();
// 访问状态
console.log(userStore.name); // 输出用户名
// 访问Getter
console.log(userStore.isLoggedIn); // 是否已登录
// 调用Action
userStore.login('admin', '123456');
</script>
4.2.2 组合式API风格与选项式API风格的对比
Pinia同时支持两种风格,但组合式API更推荐(与Vue 3的<script setup>
更契合)。
组合式API风格(推荐):
javascript
// stores/cartStore.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
// 状态(使用ref)
const items = ref([]);
// Getters(使用computed)
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0);
});
// Actions(方法)
const addToCart = (product, quantity) => {
const existingItem = items.value.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
items.value.push({ ...product, quantity });
}
};
return { items, totalPrice, addToCart }; // 暴露给组件
});
选项式API风格(传统):
javascript
// stores/productStore.js
import { defineStore } from 'pinia';
export const useProductStore = defineStore('product', {
state: () => ({
products: []
}),
getters: {
expensiveProducts: (state) => state.products.filter(p => p.price > 1000)
},
actions: {
async fetchProducts() {
this.products = await fetch('/api/products').then(res => res.json());
}
}
});
4.2.3 多Store模块化管理
大型项目中,可将Store按功能拆分为多个文件(如userStore
、cartStore
、productStore
),并通过storeToRefs
解构保持响应式。
示例:在组件中使用多个Store
vue
<script setup>
import { useUserStore } from './stores/userStore';
import { useCartStore } from './stores/cartStore';
import { storeToRefs } from 'pinia';
const userStore = useUserStore();
const cartStore = useCartStore();
// 解构State(保持响应式)
const { name } = storeToRefs(userStore);
const { items } = storeToRefs(cartStore);
// 直接访问Getters/Actions
console.log(cartStore.totalPrice);
cartStore.addToCart({ id: 1, name: '商品A', price: 99 }, 2);
</script>
4.3 实战:为待办事项应用添加全局状态管理
本实践将之前的待办事项列表从本地状态升级为Pinia全局状态,实现跨组件共享(如侧边栏显示待办总数)。
步骤1:定义todoStore
javascript
// stores/todoStore.js
import { defineStore } from 'pinia';
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: []
}),
getters: {
remainingCount: (state) => state.todos.filter(todo => !todo.completed).length,
completedCount: (state) => state.todos.filter(todo => todo.completed).length
},
actions: {
addTodo(text) {
this.todos.push({
text,
completed: false,
id: Date.now() // 唯一ID
});
},
removeTodo(id) {
this.todos = this.todos.filter(todo => todo.id !== id);
},
toggleTodo(id) {
const todo = this.todos.find(todo => todo.id === id);
if (todo) todo.completed = !todo.completed;
}
}
});
步骤2:在侧边栏组件中显示统计信息
vue
<!-- Sidebar.vue -->
<script setup>
import { useTodoStore } from '../stores/todoStore';
import { storeToRefs } from 'pinia';
const todoStore = useTodoStore();
const { remainingCount, completedCount } = storeToRefs(todoStore);
</script>
<template>
<div class="sidebar">
<h3>待办统计</h3>
<p>剩余:{{ remainingCount }}</p>
<p>已完成:{{ completedCount }}</p>
</div>
</template>
步骤3:在待办列表组件中使用全局Store
vue
<!-- TodoList.vue -->
<script setup>
import { useTodoStore } from '../stores/todoStore';
// 直接使用全局Store(无需通过props传递)
const todoStore = useTodoStore();
const newTodo = ref('');
const addTodo = () => {
if (newTodo.value.trim()) {
todoStore.addTodo(newTodo.value.trim());
newTodo.value = '';
}
};
const removeTodo = (id) => {
todoStore.removeTodo(id);
};
</script>
关键知识点总结:
- Pinia的
state
是响应式的,直接修改即可触发视图更新(无需Vue.set
)。 getters
类似于计算属性,可依赖其他state
或getters
,支持缓存。actions
用于封装复杂逻辑(如异步请求),可通过this
访问Store实例。
第三部分 实战篇:完整项目开发与优化
第7章 企业级项目实战:电商后台管理系统(详细)
7.1 需求分析与技术选型(附原型图说明)
项目背景:某电商公司需要开发一个后台管理系统,用于商品管理、订单处理、用户数据统计等核心业务。
核心功能模块:
- 商品管理:商品列表、添加/编辑商品、上下架
- 订单管理:订单列表、详情查看、发货/退款处理
- 用户管理:用户列表、权限设置、数据导出
- 数据看板:销售额统计、订单趋势、热门商品
技术选型:
- 前端框架:Vue 3 + TypeScript(类型安全、大型项目友好)
- UI组件库:Element Plus(丰富的后台组件,如表格、表单、弹窗)
- 状态管理:Pinia(替代Vuex,更简洁的API)
- 路由:Vue Router 4(动态路由、权限控制)
- 图表:ECharts(数据可视化)
- HTTP客户端:Axios(拦截器、请求封装)
- 构建工具:Vite(快速的开发体验)
7.2 项目搭建与架构设计(附目录结构规范)
7.2.1 初始化项目
使用create-vue
创建项目,勾选以下配置:
- TypeScript
- Pinia
- Vue Router
- ESLint + Prettier(代码规范)
- ESLint-plugin-vue(Vue语法检查)
bash
npm create vue@latest ecom-admin
# 按提示选择配置后,进入项目目录
cd ecom-admin
pnpm install
# 安装Element Plus
pnpm add element-plus
# 安装Axios
pnpm add axios
7.2.2 目录结构规范(关键目录说明)
csharp
ecom-admin/
├── public/ # 静态资源(logo、favicon)
├── src/
│ ├── api/ # API请求封装(按模块划分)
│ │ ├── product.ts # 商品相关API
│ │ ├── order.ts # 订单相关API
│ │ └── user.ts # 用户相关API
│ ├── assets/ # 静态资源(图片、样式)
│ ├── components/ # 公共组件(全局注册)
│ │ ├── Layout/ # 布局组件(侧边栏、头部)
│ │ ├── Table/ # 增强版表格组件
│ │ └── Form/ # 增强版表单组件
│ ├── hooks/ # 自定义Hook(逻辑复用)
│ │ ├── useAuth.ts # 权限校验Hook
│ │ └── useRequest.ts # 请求封装Hook
│ ├── router/ # 路由配置(动态路由、权限控制)
│ ├── stores/ # Pinia Store(按模块划分)
│ │ ├── app.ts # 全局状态(主题、加载状态)
│ │ ├── product.ts # 商品状态
│ │ └── user.ts # 用户状态
│ ├── utils/ # 工具函数(日期格式化、权限判断)
│ ├── views/ # 页面组件(按模块划分)
│ │ ├── product/ # 商品管理页面
│ │ ├── order/ # 订单管理页面
│ │ └── dashboard/ # 数据看板页面
│ ├── App.vue # 根组件(布局容器)
│ └── main.ts # 入口文件(初始化全局样式、插件)
├── .eslintrc.js # ESLint配置
├── vite.config.ts # Vite配置(别名、插件)
├── tsconfig.json # TypeScript配置
└── package.json # 依赖管理
7.2.3 全局配置(主题、请求拦截、错误处理)
主题定制 :Element Plus支持自定义主题,通过vite-plugin-element-plus
自动导入样式。
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import ElementPlus from 'unplugin-element-plus/vite';
export default defineConfig({
plugins: [
vue(),
ElementPlus({
// 自定义主题色(修改SCSS变量)
useSource: true,
// 按需导入(减小打包体积)
importStyle: 'scss'
})
]
});
Axios请求封装:统一处理请求头、错误提示、加载状态。
typescript
// utils/request.ts
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus';
import { useAppStore } from '../stores/app';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL, // 从环境变量获取
timeout: 10000
});
// 请求拦截器:添加Token
service.interceptors.request.use((config: AxiosRequestConfig) => {
const appStore = useAppStore();
if (appStore.token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${appStore.token}`
};
}
return config;
});
// 响应拦截器:处理错误码
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data;
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败');
// 401:未登录/Token过期
if (res.code === 401) {
// 跳转到登录页
}
return Promise.reject(new Error(res.message));
}
return res.data;
},
(error) => {
ElMessage.error(error.message || '网络异常');
return Promise.reject(error);
}
);
export default service;
7.3 核心功能开发(附关键代码)
7.3.1 商品列表页(分页、筛选、排序)
需求:展示商品列表,支持按名称搜索、价格区间筛选、销量排序,分页加载。
实现步骤:
-
API接口定义 (
src/api/product.ts
):typescriptimport request from '../utils/request'; export interface Product { id: number; name: string; price: number; stock: number; sales: number; status: 0 | 1; // 0-下架,1-上架 } export interface QueryParams { page: number; pageSize: number; keyword?: string; minPrice?: number; maxPrice?: number; sortBy?: 'sales' | 'price'; sortOrder?: 'asc' | 'desc'; } export function getProductList(params: QueryParams) { return request({ url: '/api/products', method: 'GET', params }); }
-
页面组件开发 (
src/views/product/List.vue
):vue<script setup> import { ref, reactive, onMounted } from 'vue'; import { getProductList } from '@/api/product'; import { ElTable, ElPagination, ElInput, ElSelect, ElOption } from 'element-plus'; // 状态 const tableData = ref<Product[]>([]); const loading = ref(false); const total = ref(0); // 查询参数 const queryParams = reactive<QueryParams>({ page: 1, pageSize: 10, sortBy: 'sales', sortOrder: 'desc' }); // 搜索关键字 const keyword = ref(''); // 加载数据 const loadData = async () => { loading.value = true; try { const res = await getProductList({ ...queryParams, keyword: keyword.value }); tableData.value = res.list; total.value = res.total; } catch (error) { console.error('加载失败', error); } finally { loading.value = false; } }; // 页码变化 const handlePageChange = (page: number) => { queryParams.page = page; loadData(); }; // 每页数量变化 const handleSizeChange = (size: number) => { queryParams.pageSize = size; queryParams.page = 1; loadData(); }; // 搜索 const handleSearch = () => { queryParams.page = 1; loadData(); }; onMounted(loadData); </script> <template> <div class="product-list"> <div class="search-bar"> <ElInput v-model="keyword" placeholder="输入商品名称搜索" style="width: 300px" clearable /> <ElButton type="primary" @click="handleSearch">搜索</ElButton> </div> <ElTable :data="tableData" border v-loading="loading"> <ElTableColumn prop="name" label="商品名称" /> <ElTableColumn prop="price" label="价格" width="100" /> <ElTableColumn prop="stock" label="库存" width="100" /> <ElTableColumn prop="sales" label="销量" width="100" /> <ElTableColumn prop="status" label="状态" width="100"> <template #default="{ row }"> <ElTag :type="row.status === 1 ? 'success' : 'info'"> {{ row.status === 1 ? '上架' : '下架' }} </ElTag> </template> </ElTableColumn> </ElTable> <ElPagination :current-page="queryParams.page" :page-size="queryParams.pageSize" :total="total" layout="total, sizes, prev, pager, next" @size-change="handleSizeChange" @current-change="handlePageChange" /> </div> </template>
7.3.2 订单详情页(动态路由传参、富文本编辑器)
需求:点击订单列表的"查看详情"跳转到详情页,展示订单的收货地址、商品明细、物流信息,并支持编辑备注(使用富文本编辑器)。
实现步骤:
-
动态路由配置 (
src/router/index.ts
):typescriptimport { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ history: createWebHistory(), routes: [ // 其他路由... { path: '/order/:orderId', name: 'OrderDetail', component: () => import('@/views/order/Detail.vue'), meta: { title: '订单详情', requiresAuth: true } } ] }); export default router;
-
富文本编辑器集成 (使用
@wangeditor/editor-for-vue
):bashpnpm add @wangeditor/editor-for-vue
-
订单详情页组件 (
src/views/order/Detail.vue
):vue<script setup> import { ref, onMounted } from 'vue'; import { useRoute } from 'vue-router'; import { ElMessage } from 'element-plus'; import { getOrderBy
7.3.3 订单详情页(动态路由传参、富文本编辑器集成)(续)
typescript
// 续:订单详情页组件
import { getOrderByID, updateOrderRemark } from '@/api/order';
import { Editor, createToolbar } from '@wangeditor/editor-for-vue';
const route = useRoute();
const orderId = route.params.orderId as string;
// 订单详情数据
const orderDetail = ref({
id: '',
orderNo: '',
createTime: '',
totalAmount: 0,
status: '',
address: {} as Address,
items: [] as OrderItem[],
remark: ''
});
// 加载订单详情
const loadOrderDetail = async () => {
try {
const res = await getOrderByID(orderId);
orderDetail.value = res.data;
} catch (error) {
ElMessage.error('获取订单详情失败');
}
};
// 编辑备注
const handleRemarkChange = (editor: any) => {
orderDetail.value.remark = editor.getHtml();
};
// 保存备注
const saveRemark = async () => {
try {
await updateOrderRemark(orderId, { remark: orderDetail.value.remark });
ElMessage.success('备注保存成功');
} catch (error) {
ElMessage.error('保存失败');
}
};
onMounted(loadOrderDetail);
</script>
<template>
<div class="order-detail">
<h2>订单详情:{{ orderDetail.orderNo }}</h2>
<!-- 基础信息 -->
<ElCard class="section">
<template #header>基础信息</template>
<ElDescriptions :column="2">
<ElDescriptionsItem label="下单时间">
{{ formatDate(orderDetail.createTime) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="订单状态">
<ElTag :type="getStatusType(orderDetail.status)">
{{ getStatusText(orderDetail.status) }}
</ElTag>
</ElDescriptionsItem>
<ElDescriptionsItem label="支付金额">
¥{{ orderDetail.totalAmount.toFixed(2) }}
</ElDescriptionsItem>
<ElDescriptionsItem label="收货地址">
{{ formatAddress(orderDetail.address) }}
</ElDescriptionsItem>
</ElDescriptions>
</ElCard>
<!-- 商品明细 -->
<ElCard class="section">
<template #header>商品明细</template>
<ElTable :data="orderDetail.items" border>
<ElTableColumn prop="productName" label="商品名称" />
<ElTableColumn prop="price" label="单价" width="100">
¥{{ props.row.price.toFixed(2) }}
</ElTableColumn>
<ElTableColumn prop="quantity" label="数量" width="100" />
<ElTableColumn label="小计" width="120">
¥{{ (props.row.price * props.row.quantity).toFixed(2) }}
</ElTableColumn>
</ElTable>
</ElCard>
<!-- 备注编辑 -->
<ElCard class="section">
<template #header>备注信息</template>
<div class="editor-container">
<Editor
v-model="orderDetail.remark"
:defaultConfig="editorConfig"
mode="default"
@onCreated="onEditorCreated"
/>
<ElButton type="primary" @click="saveRemark">保存备注</ElButton>
</div>
</ElCard>
</div>
</template>
<style scoped>
.section {
margin-bottom: 20px;
}
.editor-container {
margin-top: 10px;
}
</style>
关键技术点解析:
- 动态路由传参 :通过
route.params.orderId
获取动态参数,结合useRoute
钩子实现页面跳转时的数据传递。 - 富文本编辑器集成 :使用
@wangeditor/editor-for-vue
组件,通过v-model
绑定内容,onCreated
生命周期初始化编辑器。 - 响应式数据更新 :订单详情数据通过
ref
声明,确保视图随数据变化自动更新。
7.3.4 数据看板(ECharts集成、WebSocket实时更新)
需求:展示销售额趋势、订单量统计、热门商品排行,支持实时刷新(每30秒更新一次)。
实现步骤:
-
ECharts集成 (安装
echarts
和vue-echarts
):bashpnpm add echarts vue-echarts
-
数据看板组件 (
src/views/dashboard/Index.vue
):vue<script setup> import { ref, onMounted, onUnmounted } from 'vue'; import { useDark } from '@vueuse/core'; import { getSalesTrend, getOrderStats, getHotProducts } from '@/api/dashboard'; import { useWebSocket } from '@/hooks/useWebSocket'; // 自定义WebSocket Hook // 图表实例 const salesChart = ref<echarts.ECharts>(); const orderChart = ref<echarts.ECharts>(); const hotProductsChart = ref<echarts.ECharts>(); // 数据 const salesTrend = ref([]); const orderStats = ref({ today: 0, yesterday: 0 }); const hotProducts = ref([]); // WebSocket连接(实时更新) const { message } = useWebSocket('ws://your-api.com/dashboard/realtime'); // 初始化图表 const initCharts = () => { // 销售额趋势图 salesChart.value = echarts.init(document.getElementById('sales-trend')); salesChart.value.setOption({ title: { text: '销售额趋势' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: salesTrend.value.map(item => item.date) }, yAxis: { type: 'value' }, series: [{ name: '销售额', type: 'line', data: salesTrend.value.map(item => item.amount) }] }); // 其他图表初始化类似... }; // 获取数据 const loadData = async () => { const [salesRes, orderRes, productsRes] = await Promise.all([ getSalesTrend(), getOrderStats(), getHotProducts() ]); salesTrend.value = salesRes.data; orderStats.value = orderRes.data; hotProducts.value = productsRes.data; updateCharts(); }; // 更新图表数据 const updateCharts = () => { if (salesChart.value) { salesChart.value.setOption({ xAxis: { data: salesTrend.value.map(item => item.date) }, series: [{ data: salesTrend.value.map(item => item.amount) }] }); } // 其他图表更新类似... }; // WebSocket消息处理 message.value?.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'sales') { salesTrend.value.unshift(data.item); updateCharts(); } }; onMounted(() => { loadData(); initCharts(); // 定时刷新(30秒) const timer = setInterval(loadData, 30000); onUnmounted(() => clearInterval(timer)); }); </script> <template> <div class="dashboard"> <h2>数据看板</h2> <!-- 核心指标 --> <ElRow :gutter="20"> <ElCol :span="8"> <ElCard> <template #header>今日销售额</template> <div class="stat-value">¥{{ orderStats.today.toFixed(2) }}</div> </ElCard> </ElCol> <ElCol :span="8"> <ElCard> <template #header>今日订单量</template> <div class="stat-value">{{ orderStats.today }}</div> </ElCard> </ElCol> <ElCol :span="8"> <ElCard> <template #header>热门商品</template> <div class="stat-value">{{ hotProducts[0]?.name }}</div> </ElCard> </ElCol> </ElRow> <!-- 图表区域 --> <ElRow :gutter="20" style="margin-top: 20px"> <ElCol :span="12"> <div id="sales-trend" style="width: 100%; height: 400px"></div> </ElCol> <ElCol :span="12"> <div id="order-stats" style="width: 100%; height: 400px"></div> </ElCol> </ElRow> </div> </template> <style scoped> .stat-value { font-size: 24px; font-weight: bold; color: var(--el-color-primary); } </style>
关键技术点解析:
- ECharts响应式 :通过
vue-echarts
组件绑定数据,结合resize
事件监听窗口变化,自动调整图表尺寸。 - WebSocket实时更新 :自定义
useWebSocket
Hook封装连接逻辑,通过onmessage
事件处理实时数据推送。 - 性能优化 :使用
Promise.all
并行请求数据,减少加载时间;定时器在组件卸载时清除,避免内存泄漏。
第8章 性能优化:从加载到运行的全链路优化(详细)
8.1 首屏加载优化(附Lighthouse优化前后对比)
8.1.1 代码分割与懒加载
Vue 3配合Vite默认支持ES模块的动态导入,可通过import()
实现组件、路由、第三方库的懒加载。
实践:路由懒加载
typescript
// router/index.ts
const routes = [
{
path: '/product',
name: 'Product',
component: () => import('@/views/product/List.vue'), // 动态导入
children: [
{
path: 'detail/:id',
name: 'ProductDetail',
component: () => import('@/views/product/Detail.vue') // 嵌套路由懒加载
}
]
}
];
第三方库按需引入 :
使用unplugin-auto-import
自动导入Vue API(如ref
、computed
),减少手动导入的代码量;
使用unplugin-vue-components
自动导入Element Plus组件(如ElButton
、ElTable
),避免全局注册的冗余。
typescript
// vite.config.ts
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default defineConfig({
plugins: [
// ...其他插件
AutoImport({
imports: ['vue', 'vue-router'],
dts: 'src/auto-imports.d.ts'
}),
Components({
resolvers: [ElementPlusResolver()],
dirs: ['src/components'], // 扫描本地组件
dts: 'src/components.d.ts'
})
]
});
8.1.2 资源优化
- 图片压缩与处理 :使用
image-webpack-loader
压缩图片,vite-plugin-imagemin
自动优化; - 字体子集化:仅保留项目中使用的字符(如中文常用字),减少字体文件体积;
- CDN加速 :将静态资源(Vue、Element Plus、ECharts)托管到CDN,通过
vite.config.ts
配置base
路径。
typescript
// vite.config.ts
export default defineConfig({
base: 'https://cdn.example.com/', // CDN基础路径
build: {
assetsInlineLimit: 0, // 强制外部化资源
rollupOptions: {
output: {
assetFileNames: '[name]-[hash].[ext]' // 自定义资源文件名
}
}
}
});
8.1.3 Lighthouse优化实践
通过Chrome DevTools的Lighthouse工具检测,针对以下指标优化:
指标 | 目标值 | 优化方法 |
---|---|---|
首次内容渲染(FCP) | <1.5s | 代码分割、CDN加速、预加载关键资源 |
最大内容渲染(LCP) | <2.5s | 图片懒加载、优化图片尺寸、服务端渲染 |
总阻塞时间(TBT) | <300ms | 减少长任务、优化JavaScript执行 |
累积布局偏移(CLS) | <0.1 | 为动态内容预留空间、避免无占位符的图片 |
8.2 运行时性能优化
8.2.1 响应式优化
Vue 3的响应式系统虽高效,但不当使用仍可能导致性能问题:
-
避免过度响应式 :对于不需要响应式的大对象(如配置项),使用
shallowRef
或markRaw
标记。typescriptconst config = shallowRef({ theme: 'light', layout: 'grid' }); // 仅第一层响应式 const rawUser = markRaw(user); // 标记为原始对象,不跟踪依赖
-
减少依赖收集 :在
computed
或watch
中,避免引用不必要的大对象。
8.2.2 虚拟滚动
对于大数据量列表(如1000+条数据),使用vue-virtual-scroller
组件仅渲染可见区域的DOM节点。
实践:集成虚拟滚动
bash
pnpm add vue-virtual-scroller
vue
<template>
<RecycleScroller
class="list-container"
:items="items"
:item-size="50"
key-field="id"
>
<template #default="{ item }">
<div class="list-item">{{ item.name }}</div>
</template>
</RecycleScroller>
</template>
<script setup>
import { ref } from 'vue';
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `商品${i}` })));
</script>
8.2.3 防抖与节流
高频事件(如搜索框输入、窗口缩放)需使用防抖(Debounce)或节流(Throttle)减少函数执行次数。
实践:自定义防抖Hook
typescript
// hooks/useDebounce.ts
import { ref, onUnmounted } from 'vue';
export function useDebounce(fn: Function, delay = 500) {
let timer: NodeJS.Timeout;
const debouncedFn = (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
onUnmounted(() => clearTimeout(timer));
return debouncedFn;
}
// 使用示例
const searchInput = ref('');
const search = useDebounce(async (keyword) => {
const res = await getProductList({ keyword });
// 更新数据...
}, 500);
// 模板中绑定
<input v-model="searchInput" @input="search(searchInput)" />
第9章 工程化进阶:测试与部署(详细)
9.1 单元测试与E2E测试
9.1.1 单元测试(Vitest配置)
Vitest是Vue官方推荐的测试框架,支持与Vite无缝集成,兼容Jest语法。
配置步骤:
-
安装依赖:
bashpnpm add vitest @vue/test-utils jsdom --save-dev
-
配置
vitest.config.ts
:typescriptimport { defineConfig } from 'vitest/config'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', // 模拟浏览器环境 coverage: { provider: 'v8', // 代码覆盖率 reporter: ['text', 'html'] } } });
-
编写测试用例(
src/components/TodoList.spec.ts
):typescriptimport { describe, it, expect, vi } from 'vitest'; import { mount } from '@vue/test-utils'; import TodoList from '../TodoList.vue'; describe('TodoList', () => { it('添加待办事项', async () => { const wrapper = mount(TodoList); const input = wrapper.find('input'); const button = wrapper.find('button'); // 模拟输入和点击 await input.setValue('学习Vue 3'); await button.trigger('click'); // 断言列表包含新项 expect(wrapper.findAll('li')).toHaveLength(1); expect(wrapper.text()).toContain('学习Vue 3'); }); });
9.1.2 E2E测试(Cypress集成)
Cypress是功能强大的E2E测试工具,支持模拟用户操作、验证页面行为。
配置步骤:
-
安装依赖:
bashpnpm add cypress --save-dev
-
初始化Cypress:
bashnpx cypress open
-
编写测试用例(
cypress/e2e/dashboard.cy.ts
):typescriptdescribe('数据看板', () => { it('显示今日销售额', () => { // 访问页面 cy.visit('/dashboard'); // 等待数据加载 cy.get('[data-testid="sales-value"]').should('contain', '¥'); // 验证数值大于0 cy.get('[data-testid="sales-value"]').invoke('text') .then(text => { const amount = parseFloat(text.replace('¥', '')); expect(amount).to.be.greaterThan(0); }); }); });
-
配置CI/CD自动运行测试(GitHub Actions):
yaml# .github/workflows/test.yml name: Test on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: { node-version: 20 } - run: npm ci - run: npm run test:unit # Vitest单元测试 - run: npm run test:e2e # Cypress E2E测试(需启动服务)
9.2 生产环境部署
9.2.1 Docker容器化
将Vue项目打包为Docker镜像,便于部署到云服务器(如阿里云、AWS)。
Dockerfile示例:
dockerfile
# 基础镜像(Node.js 18)
FROM node:18-alpine as builder
# 设置工作目录
WORKDIR /app
# 复制依赖文件并安装
COPY package*.json ./
RUN npm ci --omit=dev
# 复制项目代码并构建
COPY . .
RUN npm run build
# 生产镜像(Nginx)
FROM nginx:alpine
# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 配置Nginx
COPY nginx.conf /etc/nginx/conf.d/default.conf
# 启动Nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Nginx配置(nginx.conf
):
conf
server {
listen 80;
server_name your-domain.com;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html; # 支持前端路由
}
# 反向代理API请求
location /api {
proxy_pass http://your-api-server:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
9.2.2 CI/CD流程(GitHub Actions)
通过GitHub Actions实现代码推送后自动构建、测试、部署。
yaml
# .github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: 构建项目
run: |
npm ci
npm run build
- name: 构建Docker镜像
uses: docker/build-push-action@v5
with:
context: .
push: false # 本地测试,不推送到仓库
tags: my-vue-app:latest
- name: 部署到服务器
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
# 停止旧容器
docker stop my-vue-app || true
docker rm my-vue-app || true
# 启动新容器
docker run -d -p 80:80 --name my-vue-app my-vue-app:latest
第10章 开发者软技能:代码规范与团队协作(详细)
10.1 代码规范(ESLint + Prettier配置)
10.1.1 ESLint配置(Vue 3 + TypeScript)
通过ESLint统一代码风格,结合Prettier格式化代码,避免"格式战争"。
.eslintrc.js配置:
javascript
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended' // 与Prettier集成
],
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
rules: {
'vue/multi-word-component-names': 'off', // 关闭组件名多单词限制
'@typescript-eslint/no-explicit-any': 'warn', // 禁止any类型
'prettier/prettier': 'error' // Prettier格式化错误视为ESLint错误
}
};
10.1.2 Prettier配置(.prettierrc)
json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"endOfLine": "lf"
}
10.1.3 保存自动格式化(VS Code配置)
在.vscode/settings.json
中添加:
json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
10.2 文档与协作
10.2.1 组件库文档(Storybook)
使用Storybook为公共组件编写交互式文档,方便团队成员查看组件用法。
安装与配置:
bash
pnpm add storybook @storybook/vue3 @storybook/addon-essentials --save-dev
创建故事文件 (src/components/Button.stories.mdx
):
markdown
import { Button } from './Button.vue';
# Button 组件
基础按钮组件,支持主色、次色、危险色样式。
## 示例
<Canvas>
<Story name="基础用法">
<Button type="primary">主要按钮</Button>
<Button type="default">默认按钮</Button>
<Button type="danger">危险按钮</Button>
</Story>
</Canvas>
## Props
| Prop | 类型 | 默认值 | 说明 |
|-------|--------|--------|------------|
| type | string | 'default' | 按钮类型:primary/default/danger |
| size | string | 'medium' | 按钮尺寸:small/medium/large |
| disabled | boolean | false | 是否禁用 |
启动Storybook:
bash
pnpm storybook
10.2.2 接口文档(Swagger)
后端与前端协作时,使用Swagger生成API文档,明确请求参数、响应格式。
后端配置示例(Node.js + Express):
javascript
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '电商后台管理系统API',
version: '1.0.0'
}
},
apis: ['./routes/*.ts'] // 扫描路由文件
};
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
前端访问文档 :
通过http://your-api.com/api-docs
访问交互式API文档,查看接口详情并测试请求。
10.3 经验分享:常见踩坑总结
10.3.1 响应式丢失问题
现象 :修改数组或对象的属性时,视图未更新。
原因 :Vue 3的响应式系统基于Proxy,但对数组的索引赋值、对象新增属性等情况无法自动追踪。
解决方案:
- 数组:使用
splice
方法或替换整个数组(this.items = [...newItems]
)。 - 对象:使用
Vue.set
(Vue 2)或set
函数(Vue 3,需从@vue/reactivity
导入)。
typescript
// Vue 3中正确修改对象属性
import { set } from '@vue/reactivity';
const obj = ref({ name: '张三' });
set(obj.value, 'age', 20); // 触发响应式更新
10.3.2 内存泄漏问题
现象 :组件卸载后,定时器或事件监听器仍执行,导致页面卡顿。
原因 :未在组件卸载时清理副作用(如setTimeout
、addEventListener
)。
解决方案:
- 使用
onUnmounted
钩子清理定时器。 - 使用
once
选项监听一次性事件。
typescript
// 组件中清理定时器
import { onUnmounted } from 'vue';
const timer = setInterval(() => {
console.log('定时器执行');
}, 1000);
onUnmounted(() => {
clearInterval(timer);
});
10.3.3 跨域问题
现象 :前端请求后端API时,浏览器报"Access to XMLHttpRequest at 'api.com' from origin 'http://localhost:5173' has been blocked by CORS policy"。
原因 :后端未配置CORS(跨域资源共享)头。
解决方案:
- 后端添加CORS头(如Express的
cors
中间件)。 - 前端开发环境配置代理(Vite的
vite.config.ts
):
typescript
// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://your-api.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});
附录
附录A:Vue 3官方文档速查表
附录B:常用工具库推荐
类别 | 工具库 | 说明 |
---|---|---|
UI组件库 | Element Plus | 后台管理系统首选,支持Vue 3 |
HTTP客户端 | Axios | 支持拦截器、请求取消 |
图表库 | ECharts | 功能强大的数据可视化库 |
状态管理 | Pinia | Vue 3官方推荐,替代Vuex |
表单校验 | VeeValidate | 灵活的表单验证解决方案 |
附录C:学习资源推荐
- 书籍:《Vue.js设计与实现》(霍春阳)、《Vue 3从入门到精通》(明日科技)
- 视频 :Vue官方教程(vuejs.org/zh/tutorial... 3核心源码解析》
- 社区 :Vue GitHub仓库(github.com/vuejs/core)... Overflow(标签:vue.js)
后记
Vue.js的魅力在于其"渐进式"的设计哲学------你可以从一个简单的单页面应用开始,逐步引入路由、状态管理、工程化工具,最终构建出复杂的企业级系统。本书的结束并非学习的终点,而是你探索Vue世界的起点。希望你能保持对技术的热情,在实践中不断积累经验,成为一名优秀的前端开发者。
愿你在Vue的星空中,找到属于自己的那颗最亮的星! 🌟