Vue相关
Vue.js 框架 完整系统学习大纲
一、Vue.js 概述
1.1 Vue.js 发展历史
- 2013年:尤雨溪在 Google 工作时创建 Vue.js
- 2014年2月:正式发布 Vue.js 0.x
- 2015年10月:Vue.js 1.0 发布
- 2016年10月:Vue.js 2.0 发布,引入虚拟 DOM
- 2019年9月:Vue.js 3.0 Beta 版发布
- 2020年9月:Vue.js 3.0 正式版发布
- 2022年1月:Vue.js 3 成为新的默认版本
- 2023年:Vue 3.3+ 发布,TypeScript 支持大幅增强
1.2 Vue.js 核心特性
- 响应式系统:基于 ES6 Proxy 的数据绑定
- 组件化:可复用、可组合的 UI 组件
- 虚拟 DOM:高效的 DOM 更新机制
- 指令系统:v-if、v-for、v-bind 等
- 模板语法:声明式渲染
- 组合式 API:Vue 3 新特性,更好的逻辑复用
- 渐进式框架:可按需引入功能
1.3 Vue.js 生态系统
- 核心库:Vue.js
- 路由:Vue Router
- 状态管理:Pinia(推荐)、Vuex
- 构建工具:Vite、Vue CLI
- UI 框架:Element Plus、Ant Design Vue、Vuetify
- 服务端渲染:Nuxt.js
- 移动端:Ionic Vue
- 桌面端:Electron + Vue
- 测试工具:Vue Test Utils、Vitest
1.4 Vue 3 与 Vue 2 对比
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy |
| 组件实例 | 单个 this 实例 | 多个 setup 上下文 |
| 代码组织 | Options API | Composition API + Options API |
| TypeScript 支持 | 一般 | 优秀 |
| 包大小 | 较大 | 更小(Tree-shaking) |
| 性能 | 良好 | 更好(编译优化) |
| 生命周期 | beforeCreate, created 等 | 新增 setup, onBeforeMount 等 |
1.5 第一个 Vue 应用
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
<div id="app">
<h1> {{ message }} </h1>
<p>计数: {{ count }} </p>
<button @click="increment">增加</button>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const message = ref('Hello Vue 3!');
const count = ref(0);
const increment = () => {
count.value++;
};
return {
message,
count,
increment
};
}
}).mount('#app');
</script>
</body>
</html>
二、Vue 基础
2.1 Vue 实例与应用
// 创建 Vue 应用
import { createApp } from 'vue';
// 根组件
const App = {
data() {
return {
message: 'Hello Vue!'
};
}
};
// 创建应用实例
const app = createApp(App);
// 全局配置
app.config.errorHandler = (err, vm, info) => {
console.error('Vue 错误:', err, info);
};
// 全局属性
app.config.globalProperties.$myGlobal = '全局属性';
// 全局组件
app.component('MyComponent', {
template: '<div>全局组件</div>'
});
// 全局指令
app.directive('focus', {
mounted(el) {
el.focus();
}
});
// 挂载到 DOM
app.mount('#app');
// 应用实例方法
app.component(); // 注册组件
app.directive(); // 注册指令
app.use(); // 使用插件
app.provide(); // 提供依赖
app.mount(); // 挂载应用
app.unmount(); // 卸载应用
app.version; // Vue 版本
2.2 模板语法
<template>
<!-- 1. 插值表达式 -->
<p> {{ message }} </p>
<p> {{ number + 1 }} </p>
<p> {{ ok ? 'YES' : 'NO' }} </p>
<p> {{ message.split('').reverse().join('') }} </p>
<!-- 2. 原始 HTML -->
<p>Using mustaches: {{ rawHtml }} </p>
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
<!-- 3. 属性绑定 -->
<div v-bind:id="dynamicId"></div>
<div :id="dynamicId"></div> <!-- 简写 -->
<button :disabled="isButtonDisabled">按钮</button>
<!-- 4. JavaScript 表达式 -->
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<!-- 5. 动态参数 -->
<a v-bind:[attributeName]="url">链接</a>
<a :[attributeName]="url">链接</a> <!-- 简写 -->
<!-- 6. 修饰符 -->
<form @submit.prevent="onSubmit">表单</form>
</template>
2.3 计算属性
// Options API
export default {
data() {
return {
firstName: '张',
lastName: '三',
items: [
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 },
{ id: 3, name: '橙子', price: 4 }
]
};
},
computed: {
// 基本计算属性
fullName() {
return this.firstName + ' ' + this.lastName;
},
// 带 getter 和 setter
reversedMessage: {
get() {
return this.message.split('').reverse().join('');
},
set(newValue) {
this.message = newValue.split('').reverse().join('');
}
},
// 依赖其他计算属性
computedFullName() {
return this.fullName.toUpperCase();
},
// 计算属性缓存特性
totalPrice() {
console.log('计算总价');
return this.items.reduce((sum, item) => sum + item.price, 0);
},
// 计算属性 vs 方法
expensiveItems() {
return this.items.filter(item => item.price > 3);
}
},
methods: {
getTotalPrice() {
console.log('调用方法');
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
};
// Composition API
import { computed, ref, reactive } from 'vue';
export default {
setup() {
const firstName = ref('张');
const lastName = ref('三');
// 计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 可写计算属性
const fullName2 = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(newValue) {
[firstName.value, lastName.value] = newValue.split(' ');
}
});
return {
firstName,
lastName,
fullName,
fullName2
};
}
};
2.4 侦听器
// Options API
export default {
data() {
return {
question: '',
answer: '请提出问题',
user: {
name: '张三',
age: 25
},
deepObject: {
nested: {
value: 1
}
}
};
},
watch: {
// 基本侦听
question(newQuestion, oldQuestion) {
if (newQuestion.includes('?')) {
this.getAnswer();
}
},
// 立即执行
question: {
handler(newQuestion, oldQuestion) {
console.log('问题变化:', newQuestion);
},
immediate: true
},
// 深度侦听
user: {
handler(newVal, oldVal) {
console.log('用户信息变化');
},
deep: true
},
// 侦听嵌套属性
'deepObject.nested.value': function(newVal, oldVal) {
console.log('嵌套值变化:', newVal);
},
// 侦听计算属性
fullName(newVal, oldVal) {
console.log('全名变化:', newVal);
}
},
methods: {
async getAnswer() {
this.answer = '思考中...';
try {
const res = await fetch('https://yesno.wtf/api');
const data = await res.json();
this.answer = data.answer;
} catch (error) {
this.answer = '错误: ' + error.message;
}
}
}
};
// Composition API
import { watch, watchEffect, ref, reactive } from 'vue';
export default {
setup() {
const question = ref('');
const answer = ref('请提出问题');
const user = reactive({
name: '张三',
age: 25
});
// watch 函数
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
answer.value = '思考中...';
try {
const res = await fetch('https://yesno.wtf/api');
const data = await res.json();
answer.value = data.answer;
} catch (error) {
answer.value = '错误: ' + error.message;
}
}
});
// 侦听多个源
watch(
[question, answer],
([newQuestion, newAnswer], [oldQuestion, oldAnswer]) => {
console.log('问题:', newQuestion, '答案:', newAnswer);
}
);
// 侦听响应式对象
watch(
() => user.name,
(newName, oldName) => {
console.log('用户名变化:', newName);
}
);
// 深度侦听
watch(
user,
(newVal, oldVal) => {
console.log('用户信息变化');
},
{ deep: true }
);
// watchEffect - 自动追踪依赖
watchEffect(() => {
console.log('问题变化:', question.value);
// 这里会自动追踪 question.value 的依赖
});
return {
question,
answer,
user
};
}
};
2.5 样式与 Class 绑定
<template>
<!-- Class 绑定 -->
<!-- 1. 对象语法 -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 2. 数组语法 -->
<div :class="[activeClass, errorClass]"></div>
<!-- 3. 数组和对象混合 -->
<div :class="[{ active: isActive }, errorClass]"></div>
<!-- 4. 在组件上使用 -->
<my-component :class="{ active: isActive }"></my-component>
<!-- Style 绑定 -->
<!-- 1. 对象语法 -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- 2. 数组语法(多个样式对象) -->
<div :style="[baseStyles, overridingStyles]"></div>
<!-- 3. 自动前缀 -->
<div :style="{ display: 'flex' }"></div>
<!-- 4. 多重值 -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
<!-- 5. CSS 变量 -->
<div :style="{
'--primary-color': primaryColor,
'--secondary-color': secondaryColor
}">
使用 CSS 变量
</div>
</template>
export default {
data() {
return {
isActive: true,
hasError: false,
activeClass: 'active',
errorClass: 'text-danger',
activeColor: 'red',
fontSize: 30,
primaryColor: '#42b983',
secondaryColor: '#35495e',
baseStyles: {
color: 'red',
fontSize: '20px'
},
overridingStyles: {
fontWeight: 'bold'
}
};
}
};
2.6 条件渲染
<template>
<!-- v-if, v-else-if, v-else -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>C</div>
<!-- 在 template 上使用 v-if -->
<template v-if="show">
<h1>标题</h1>
<p>段落</p>
</template>
<!-- v-show -->
<h1 v-show="ok">你好!</h1>
<!-- v-if vs v-show -->
<div v-if="Math.random() > 0.5">v-if: 条件渲染(切换开销高)</div>
<div v-show="Math.random() > 0.5">v-show: 条件显示(初始渲染开销高)</div>
<!-- 通过 key 管理可复用元素 -->
<template v-if="loginType === 'username'">
<label>用户名</label>
<input placeholder="输入用户名" key="username-input">
</template>
<template v-else>
<label>邮箱</label>
<input placeholder="输入邮箱" key="email-input">
</template>
</template>
2.7 列表渲染
<template>
<!-- 数组渲染 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} : {{ item.message }}
</li>
</ul>
<!-- 对象渲染 -->
<ul>
<li v-for="(value, key, index) in myObject" :key="key">
{{ index }} . {{ key }} : {{ value }}
</li>
</ul>
<!-- 范围渲染 -->
<div>
<span v-for="n in 10" :key="n"> {{ n }} </span>
</div>
<!-- 在 template 上使用 v-for -->
<ul>
<template v-for="item in items" :key="item.id">
<li> {{ item.message }} </li>
<li class="divider" role="presentation"></li>
</template>
</ul>
<!-- 组件上使用 v-for -->
<my-component
v-for="(item, index) in items"
:key="item.id"
:item="item"
:index="index"
/>
<!-- 显示过滤/排序结果 -->
<ul>
<!-- 在计算属性中处理 -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
<!-- 在方法中处理 -->
<li v-for="n in even(numbers)" :key="n"> {{ n }} </li>
<!-- 嵌套 v-for -->
<template v-for="list in lists" :key="list.id">
<h3> {{ list.title }} </h3>
<ul>
<li v-for="item in list.items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
</ul>
</template>
export default {
data() {
return {
items: [
{ id: 1, message: 'Foo' },
{ id: 2, message: 'Bar' }
],
myObject: {
title: '标题',
author: '作者',
publishedAt: '2016-04-10'
},
numbers: [1, 2, 3, 4, 5],
lists: [
{
id: 1,
title: '购物清单',
items: [
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' }
]
}
],
users: [
{ id: 1, name: '张三', active: true },
{ id: 2, name: '李四', active: false }
]
};
},
computed: {
activeUsers() {
return this.users.filter(user => user.active);
}
},
methods: {
even(numbers) {
return numbers.filter(number => number % 2 === 0);
}
}
};
2.8 事件处理
<template>
<!-- 内联处理器 -->
<button @click="count++">增加 {{ count }} </button>
<!-- 方法处理器 -->
<button @click="greet">问候</button>
<!-- 内联处理器中访问原生 DOM 事件 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
提交
</button>
<!-- 多事件处理器 -->
<button @click="one($event), two($event)">
点击
</button>
<!-- 事件修饰符 -->
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit"></form>
<!-- 阻止事件冒泡 -->
<div @click.stop="doThis"></div>
<!-- 阻止默认行为,不阻止冒泡 -->
<form @submit.prevent></form>
<!-- 串联修饰符 -->
<a @click.stop.prevent="doThat"></a>
<!-- 添加事件监听器时使用事件捕获模式 -->
<div @click.capture="doThis"></div>
<!-- 只当事件在该元素本身触发时触发回调 -->
<div @click.self="doThat"></div>
<!-- 点击事件最多触发一次 -->
<button @click.once="doThis"></button>
<!-- 滚动事件的默认行为会立即触发,包含 `event.preventDefault()` 的情况 -->
<div @scroll.passive="onScroll"></div>
<!-- 按键修饰符 -->
<input @keyup.enter="submit">
<input @keyup.13="submit"> <!-- 键码 -->
<!-- 按键别名 -->
<!-- .enter .tab .delete .esc .space .up .down .left .right -->
<input @keyup.page-down="onPageDown">
<!-- 系统修饰键 -->
<!-- .ctrl .alt .shift .meta -->
<div @click.ctrl="doSomething">Ctrl + 点击</div>
<!-- 精确修饰符 -->
<button @click.ctrl.exact="onCtrlClick">有且只有 Ctrl 被按下</button>
<button @click.exact="onClick">没有任何系统修饰符被按下</button>
<!-- 鼠标按钮修饰符 -->
<!-- .left .right .middle -->
<button @click.right="onRightClick">右键点击</button>
</template>
export default {
data() {
return {
count: 0,
name: 'Vue.js'
};
},
methods: {
greet(event) {
alert('Hello ' + this.name + '!');
if (event) {
alert(event.target.tagName);
}
},
warn(message, event) {
if (event) {
event.preventDefault();
}
alert(message);
},
doThis() {
console.log('doThis');
},
doThat() {
console.log('doThat');
},
onSubmit() {
console.log('表单提交');
},
submit() {
console.log('提交');
},
onPageDown() {
console.log('PageDown');
},
onCtrlClick() {
console.log('Ctrl + Click');
},
onClick() {
console.log('Click');
},
onRightClick() {
console.log('右键点击');
},
onScroll() {
console.log('滚动');
},
one(event) {
console.log('第一个处理器');
},
two(event) {
console.log('第二个处理器');
}
}
};
2.9 表单输入绑定
<template>
<!-- 文本 -->
<input v-model="message" placeholder="编辑我">
<p>消息是: {{ message }} </p>
<!-- 多行文本 -->
<textarea v-model="message" placeholder="多行输入"></textarea>
<!-- 复选框 -->
<!-- 单个复选框,绑定布尔值 -->
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox"> {{ checked }} </label>
<!-- 多个复选框,绑定到同一个数组 -->
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<span>选中的名字: {{ checkedNames }} </span>
<!-- 单选按钮 -->
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<span>选中的是: {{ picked }} </span>
<!-- 选择框 -->
<!-- 单选 -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>选中的: {{ selected }} </span>
<!-- 多选(绑定到数组) -->
<select v-model="multiSelected" multiple style="width: 50px;">
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<span>选中的: {{ multiSelected }} </span>
<!-- 动态选项 -->
<select v-model="selected">
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>
<span>选中的: {{ selected }} </span>
<!-- 值绑定 -->
<!-- 复选框 -->
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no"
>
<p> {{ toggle }} </p>
<!-- 单选按钮 -->
<input type="radio" v-model="pick" :value="a">
<p> {{ pick }} </p>
<!-- 选择框选项 -->
<select v-model="selected">
<!-- 内联对象字面量 -->
<option :value="{ number: 123 }">123</option>
</select>
<p> {{ selected.number }} </p>
<!-- 修饰符 -->
<!-- .lazy - 在 change 事件后同步 -->
<input v-model.lazy="msg">
<p> {{ msg }} </p>
<!-- .number - 输入值转为数字 -->
<input v-model.number="age" type="number">
<p> {{ typeof age }} </p>
<!-- .trim - 自动去除首尾空白字符 -->
<input v-model.trim="trimmed">
<p>" {{ trimmed }} "</p>
</template>
export default {
data() {
return {
message: '',
checked: false,
checkedNames: [],
picked: '',
selected: '',
multiSelected: [],
options: [
{ text: 'One', value: 'A' },
{ text: 'Two', value: 'B' },
{ text: 'Three', value: 'C' }
],
toggle: '',
pick: '',
a: 'a',
msg: '',
age: 0,
trimmed: ''
};
}
};
三、组件基础
3.1 组件注册
// 全局组件注册
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
// 1. 组件选项对象
app.component('my-component', {
template: '<div>自定义组件!</div>'
});
// 2. 单文件组件
import MyComponent from './MyComponent.vue';
app.component('MyComponent', MyComponent);
// 3. 异步组件
app.component(
'AsyncComponent',
() => import('./AsyncComponent.vue')
);
// 局部组件注册
export default {
components: {
// 1. 组件选项对象
'my-component': {
template: '<div>局部组件</div>'
},
// 2. 导入的组件
MyComponent: MyComponent,
// 3. 简写
MyComponent
}
};
3.2 Props
// 子组件
export default {
// 1. 字符串数组形式
props: ['title', 'likes', 'isPublished', 'commentIds', 'author'],
// 2. 对象形式
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
callback: Function,
contactsPromise: Promise
},
// 3. 详细配置
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数返回
default() {
return { message: 'hello' };
}
},
// 自定义验证函数
propF: {
validator(value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].includes(value);
}
},
// 自定义类型构造函数
propG: {
type: Person, // 自定义构造函数
default: () => new Person('John')
},
// 自定义类型检查
propH: {
type: [String, Number, Boolean],
required: true,
validator(value) {
return value !== '';
}
}
},
// 4. Composition API
setup(props, context) {
// props 是响应式的
console.log(props.title);
// 解构 props
const { title } = toRefs(props);
return {
title
};
}
};
<!-- 父组件使用 -->
<template>
<!-- 传递静态值 -->
<blog-post title="My journey with Vue"></blog-post>
<!-- 动态绑定 -->
<blog-post :title="post.title"></blog-post>
<!-- 传入一个数字 -->
<blog-post :likes="42"></blog-post>
<!-- 传入一个布尔值 -->
<blog-post :is-published="false"></blog-post>
<!-- 传入一个数组 -->
<blog-post :comment-ids="[234, 266, 273]"></blog-post>
<!-- 传入一个对象 -->
<blog-post :author="{ name: 'Veronica', company: 'Veridian Dynamics' }"></blog-post>
<!-- 传入一个对象的所有 property -->
<blog-post v-bind="post"></blog-post>
<!-- 等价于 -->
<blog-post
:id="post.id"
:title="post.title"
></blog-post>
<!-- 单向数据流 -->
<!-- 在子组件中不要直接修改 props -->
</template>
3.3 自定义事件
// 子组件
export default {
emits: [
// 数组语法
'enlarge-text',
'update:title'
],
emits: {
// 对象语法
'enlarge-text': null, // 没有验证
'update:title': (title) => {
if (title && typeof title === 'string') {
return true;
} else {
console.warn('Invalid title!');
return false;
}
}
},
methods: {
submitForm() {
// 触发自定义事件
this.$emit('enlarge-text');
this.$emit('update:title', '新的标题');
// 传递多个参数
this.$emit('submit', formData, extraData);
// Composition API
const { emit } = getCurrentInstance();
emit('custom-event', payload);
}
}
};
<!-- 父组件 -->
<template>
<!-- 监听事件 -->
<blog-post @enlarge-text="postFontSize += 0.1"></blog-post>
<!-- 事件处理函数接收参数 -->
<blog-post @update:title="handleUpdate"></blog-post>
<!-- 内联处理器 -->
<blog-post @update:title="title = $event"></blog-post>
<!-- 多个事件处理器 -->
<blog-post
@update:title="handleUpdate"
@delete="handleDelete"
></blog-post>
<!-- 事件修饰符 -->
<my-component @click.native="handleNativeClick"></my-component>
<!-- v-model 自定义 -->
<custom-input v-model="searchText"></custom-input>
<!-- 等价于 -->
<custom-input
:model-value="searchText"
@update:model-value="newValue => searchText = newValue"
></custom-input>
</template>
3.4 插槽
<!-- 子组件 -->
<template>
<!-- 默认插槽 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<!-- 作用域插槽 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
<!-- 具名作用域插槽 -->
<slot name="item" v-bind="{ value }"></slot>
<!-- 动态插槽名 -->
<template v-for="(_, slot) in $slots" :key="slot">
<slot :name="slot" />
</template>
</template>
<!-- 父组件 -->
<template>
<!-- 默认插槽 -->
<base-layout>
<template v-slot:header>
<h1>标题</h1>
</template>
<p>主要内容</p>
<template v-slot:footer>
<p>页脚</p>
</template>
</base-layout>
<!-- 缩写 -->
<base-layout>
<template #header>
<h1>标题</h1>
</template>
</base-layout>
<!-- 作用域插槽 -->
<todo-list>
<template v-slot:default="slotProps">
<span v-if="slotProps.item.isComplete">✓</span>
{{ slotProps.item.text }}
</template>
</todo-list>
<!-- 解构插槽 Prop -->
<todo-list>
<template v-slot="{ item }">
<span v-if="item.isComplete">✓</span>
{{ item.text }}
</template>
</todo-list>
<!-- 具名作用域插槽 -->
<scoped-slot-demo>
<template #item="{ value }">
<span> {{ value }} </span>
</template>
</scoped-slot-demo>
<!-- 动态插槽名 -->
<base-layout>
<template #[dynamicSlotName]>
...
</template>
</base-layout>
</template>
3.5 动态组件
<template>
<!-- 动态组件 -->
<component :is="currentComponent"></component>
<!-- 保持组件状态 -->
<keep-alive>
<component :is="currentComponent"></component>
</keep-alive>
<!-- 包含/排除特定组件 -->
<keep-alive :include="['Post', 'Archive']" :exclude="['Home']" :max="10">
<component :is="currentComponent"></component>
</keep-alive>
<!-- 组件切换过渡 -->
<transition name="fade" mode="out-in">
<component :is="currentComponent"></component>
</transition>
<!-- 组件名可以是 -->
<!-- 1. 注册的组件名 -->
<component :is="'my-component'"></component>
<!-- 2. 导入的组件对象 -->
<component :is="importedComponent"></component>
<!-- 3. 组件选项对象 -->
<component :is="{ template: '<div>动态组件</div>' }"></component>
</template>
import { shallowRef } from 'vue';
import Home from './Home.vue';
import About from './About.vue';
import Contact from './Contact.vue';
export default {
components: {
Home,
About,
Contact
},
data() {
return {
currentComponent: shallowRef(Home),
tabs: [
{ name: 'Home', component: Home },
{ name: 'About', component: About },
{ name: 'Contact', component: Contact }
],
currentTab: 0
};
},
methods: {
changeComponent(component) {
this.currentComponent = component;
}
}
};
3.6 异步组件
// 全局注册异步组件
import { defineAsyncComponent } from 'vue';
// 1. 基本用法
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
);
// 2. 带选项的工厂函数
const AsyncCompWithOptions = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了 timeout,并且加载组件的时间超过了设定值,也会展示报错组件
timeout: 3000,
// 定义组件是否可挂起
suspensible: false,
// 错误处理函数
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
// 重试获取错误
retry();
} else {
// 注意,retry/fail 类似于 promise 的 resolve/reject
fail();
}
}
});
// 3. 局部注册异步组件
export default {
components: {
AsyncComponent: defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
}
};
3.7 依赖注入
// 父组件提供
import { provide, reactive, readonly } from 'vue';
export default {
// Options API
provide: {
// 提供静态值
message: 'hello',
// 提供响应式数据
user: {
name: '张三',
age: 25
}
},
// 提供函数
provide() {
return {
// 使注入成为只读
theme: readonly(this.theme),
// 提供方法
updateTheme: this.updateTheme
};
},
// Composition API
setup() {
const theme = reactive({
primary: '#1890ff',
secondary: '#f0f2f5'
});
// 提供响应式数据
provide('theme', theme);
// 提供方法
provide('updateTheme', (newTheme) => {
Object.assign(theme, newTheme);
});
// 提供 Symbol 作为 key
const ThemeSymbol = Symbol('theme');
provide(ThemeSymbol, theme);
return {
theme
};
}
};
// 子组件注入
import { inject } from 'vue';
export default {
// Options API
inject: [
'message', // 数组语法
'user',
'theme'
],
inject: {
// 对象语法
localMessage: {
from: 'message', // 从哪个 property 注入
default: '默认消息' // 默认值
},
// 简写
user: {
default: () => ({ name: '游客' })
},
// 工厂函数
theme: {
default: () => ({
primary: '#1890ff',
secondary: '#f0f2f5'
})
}
},
// Composition API
setup() {
// 注入值
const message = inject('message');
// 带默认值
const theme = inject('theme', {
primary: '#1890ff',
secondary: '#f0f2f5'
});
// 响应式注入
const user = inject('user', {});
// 注入方法
const updateTheme = inject('updateTheme');
// Symbol key
const ThemeSymbol = Symbol('theme');
const theme2 = inject(ThemeSymbol);
return {
message,
theme,
user,
updateTheme
};
}
};
四、深入组件
4.1 组件通信
// 1. Props / Events (父子组件)
// 父 -> 子: Props
// 子 -> 父: Events
// 2. v-model (双向绑定)
// 自定义组件实现 v-model
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
`
};
// 3. .sync 修饰符 (Vue 2)
// 在 Vue 3 中,使用 v-model:propName 替代
// 4. $parent / $children (不推荐)
// 访问父组件实例
this.$parent.someMethod();
// 访问子组件实例
this.$children[0].someMethod();
// 5. $refs
// 父组件
<template>
<child-component ref="childRef"></child-component>
</template>
<script>
export default {
mounted() {
// 访问子组件实例
this.$refs.childRef.someMethod();
// 访问 DOM 元素
this.$refs.input.focus();
}
};
</script>
// 6. Provide / Inject (跨级组件)
// 见 3.7 节
// 7. Event Bus (不推荐,使用 Vuex/Pinia 替代)
// 创建事件总线
// eventBus.js
import mitt from 'mitt';
export const emitter = mitt();
// 发送事件
emitter.emit('event-name', data);
// 接收事件
emitter.on('event-name', (data) => {
console.log(data);
});
// 8. Vuex / Pinia (状态管理)
// 见第 6 章
// 9. 全局状态
// 创建全局状态
// globalState.js
import { reactive } from 'vue';
export const globalState = reactive({
count: 0,
increment() {
this.count++;
}
});
// 在组件中使用
import { globalState } from './globalState';
console.log(globalState.count);
globalState.increment();
4.2 组件生命周期
// Options API
export default {
data() {
return {
message: 'Hello Vue!'
};
},
// 生命周期钩子
beforeCreate() {
// 实例初始化之后,data observer 和 event/watcher 事件配置之前
console.log('beforeCreate');
},
created() {
// 实例创建完成后,data observer 和 event/watcher 已配置
console.log('created');
// 可以访问 data、computed、methods
console.log(this.message);
},
beforeMount() {
// 挂载开始之前,render 函数首次被调用
console.log('beforeMount');
},
mounted() {
// 实例被挂载后,DOM 已更新
console.log('mounted');
// 可以访问 $el
console.log(this.$el);
},
beforeUpdate() {
// 数据更新时,虚拟 DOM 重新渲染和打补丁之前
console.log('beforeUpdate');
},
updated() {
// 数据更新导致虚拟 DOM 重新渲染和打补丁之后
console.log('updated');
},
beforeUnmount() {
// 实例销毁之前
console.log('beforeUnmount');
},
unmounted() {
// 实例销毁后
console.log('unmounted');
},
// 其他钩子
activated() {
// 被 keep-alive 缓存的组件激活时调用
console.log('activated');
},
deactivated() {
// 被 keep-alive 缓存的组件失活时调用
console.log('deactivated');
},
errorCaptured(err, vm, info) {
// 捕获子孙组件的错误
console.log('errorCaptured', err, info);
return false; // 阻止错误继续向上传播
},
renderTracked(e) {
// 跟踪虚拟 DOM 重新渲染时调用
console.log('renderTracked', e);
},
renderTriggered(e) {
// 虚拟 DOM 重新渲染被触发时调用
console.log('renderTriggered', e);
}
};
// Composition API
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onActivated,
onDeactivated,
onErrorCaptured,
onRenderTracked,
onRenderTriggered
} from 'vue';
export default {
setup() {
// 组件挂载阶段
onBeforeMount(() => {
console.log('onBeforeMount');
});
onMounted(() => {
console.log('onMounted');
});
// 组件更新阶段
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
});
onUpdated(() => {
console.log('onUpdated');
});
// 组件卸载阶段
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
});
onUnmounted(() => {
console.log('onUnmounted');
});
// 组件激活/停用
onActivated(() => {
console.log('onActivated');
});
onDeactivated(() => {
console.log('onDeactivated');
});
// 错误捕获
onErrorCaptured((err, instance, info) => {
console.log('onErrorCaptured', err, info);
return false;
});
// 调试钩子
onRenderTracked((e) => {
console.log('onRenderTracked', e);
});
onRenderTriggered((e) => {
console.log('onRenderTriggered', e);
});
return {};
}
};
4.3 模板引用
<template>
<!-- 模板引用 -->
<input ref="input" />
<!-- 在 v-for 中使用 -->
<div v-for="item in list" :ref="setItemRef">
{{ item }}
</div>
<!-- 组件引用 -->
<child-component ref="child" />
<!-- 函数引用 -->
<child-component :ref="(el) => { childRef = el }" />
<!-- Composition API -->
<div ref="divRef">内容</div>
</template>
import { ref, onMounted, nextTick } from 'vue';
export default {
// Options API
mounted() {
// 访问 DOM 元素
this.$refs.input.focus();
// 访问组件实例
this.$refs.child.someMethod();
// v-for 中的引用
console.log(this.itemRefs);
},
data() {
return {
itemRefs: []
};
},
methods: {
setItemRef(el) {
if (el) {
this.itemRefs.push(el);
}
}
},
// Composition API
setup() {
const input = ref(null);
const child = ref(null);
const divRef = ref(null);
onMounted(() => {
// 访问 DOM 元素
input.value.focus();
// 访问组件实例
child.value.someMethod();
// 等待下一个 DOM 更新周期
nextTick(() => {
console.log('DOM 已更新');
});
});
return {
input,
child,
divRef
};
}
};
4.4 过渡与动画
<template>
<!-- 单元素/组件过渡 -->
<transition name="fade">
<p v-if="show">hello</p>
</transition>
<!-- 多个元素的过渡 -->
<transition name="fade" mode="out-in">
<button v-if="isEditing" key="save" @click="isEditing = false">
保存
</button>
<button v-else key="edit" @click="isEditing = true">
编辑
</button>
</transition>
<!-- 多个组件过渡 -->
<transition name="component-fade" mode="out-in">
<component :is="currentComponent"></component>
</transition>
<!-- 列表过渡 -->
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</transition-group>
<!-- 动态过渡 -->
<transition :name="transitionName">
<p v-if="show">hello</p>
</transition>
<!-- JavaScript 钩子 -->
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@enter-cancelled="enterCancelled"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
@leave-cancelled="leaveCancelled"
>
<p v-if="show">hello</p>
</transition>
</template>
export default {
data() {
return {
show: true,
isEditing: false,
currentComponent: 'component-a',
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' }
],
transitionName: 'fade'
};
},
methods: {
// JavaScript 钩子
beforeEnter(el) {
el.style.opacity = 0;
el.style.transform = 'translateY(30px)';
},
enter(el, done) {
gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.5,
onComplete: done
});
},
afterEnter(el) {
console.log('进入完成');
},
enterCancelled(el) {
console.log('进入取消');
},
beforeLeave(el) {
console.log('离开之前');
},
leave(el, done) {
gsap.to(el, {
opacity: 0,
y: 30,
duration: 0.5,
onComplete: done
});
},
afterLeave(el) {
console.log('离开完成');
},
leaveCancelled(el) {
console.log('离开取消');
}
}
};
/* CSS 过渡类名 */
/* 进入/离开 */
.fade-enter-from {
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}
.fade-enter-active {
transition: opacity 0.5s ease;
}
.fade-leave-from {
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
.fade-leave-active {
transition: opacity 0.5s ease;
}
/* 列表过渡 */
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-enter-to,
.list-leave-from {
opacity: 1;
transform: translateY(0);
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-move {
transition: transform 0.5s ease;
}
.list-leave-active {
position: absolute;
}
4.5 混入
// mixin.js
export const myMixin = {
data() {
return {
mixinData: '来自混入的数据'
};
},
created() {
console.log('混入的 created 钩子');
},
methods: {
mixinMethod() {
console.log('混入的方法');
}
}
};
// 组件中使用
import { myMixin } from './mixin.js';
// 局部混入
export default {
mixins: [myMixin],
data() {
return {
// 与混入对象的数据合并
mixinData: '组件的数据', // 组件数据优先
componentData: '组件独有数据'
};
},
created() {
console.log('组件的 created 钩子');
// 都会调用,混入的先调用
},
methods: {
// 与混入对象的方法合并
mixinMethod() {
console.log('组件的方法'); // 组件方法优先
},
componentMethod() {
console.log('组件独有方法');
}
}
};
// 全局混入
import { createApp } from 'vue';
const app = createApp(App);
app.mixin({
created() {
console.log('全局混入的 created 钩子');
}
});
4.6 自定义指令
// 全局自定义指令
import { createApp } from 'vue';
const app = createApp(App);
// 1. 函数简写
app.directive('focus', (el, binding) => {
el.focus();
});
// 2. 完整对象
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed';
const s = binding.arg || 'top';
el.style[s] = binding.value + 'px';
},
updated(el, binding) {
const s = binding.arg || 'top';
el.style[s] = binding.value + 'px';
},
beforeUnmount(el, binding) {
// 清理工作
}
});
// 局部自定义指令
export default {
directives: {
// 局部指令
focus: {
mounted(el) {
el.focus();
}
},
// 简写
color(el, binding) {
el.style.color = binding.value;
}
},
setup() {
// Composition API
const vMyDirective = {
mounted(el, binding) {
// 指令逻辑
}
};
return {
vMyDirective
};
}
};
<template>
<!-- 使用指令 -->
<input v-focus />
<!-- 带值 -->
<div v-color="'red'">红色文字</div>
<!-- 动态参数 -->
<div v-pin:top="200">固定在顶部</div>
<!-- 修饰符 -->
<div v-demo.modifier="value">修饰符示例</div>
<!-- 对象字面量 -->
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
</template>
4.7 渲染函数
// Options API
export default {
data() {
return {
message: 'Hello Vue!'
};
},
render() {
// 使用 h() 函数创建虚拟 DOM
return h(
'div', // 标签名
{ id: 'app' }, // 属性/Props
[
h('h1', null, this.message),
h('button', {
onClick: () => {
this.message = 'Clicked!';
}
}, '点击我')
]
);
}
};
// Composition API
import { h, ref } from 'vue';
export default {
setup() {
const message = ref('Hello Vue!');
return () => h('div', [
h('h1', null, message.value),
h('button', {
onClick: () => {
message.value = 'Clicked!';
}
}, '点击我')
]);
}
};
// JSX
import { defineComponent, ref } from 'vue';
export default defineComponent({
setup() {
const count = ref(0);
return () => (
<div>
<h1>Count: {count.value}</h1>
<button onClick={() => count.value++}>增加</button>
</div>
);
}
});
4.8 函数式组件
// 函数式组件
export default {
functional: true,
props: {
title: {
type: String,
required: true
}
},
// 对于函数式组件,没有 this
// context 参数包含 attrs, slots, emit, props
render(h, context) {
return h('div', context.data, [
h('h2', context.props.title),
context.children
]);
}
};
// Vue 3 函数式组件
import { h } from 'vue';
const FunctionalComponent = (props, context) => {
return h('div', context.attrs, [
h('h2', props.title),
context.slots.default()
]);
};
FunctionalComponent.props = ['title'];
五、组合式 API
5.1 setup 函数
import { ref, reactive, computed, watch, onMounted } from 'vue';
export default {
// setup 函数
setup(props, context) {
// props - 响应式的 props
console.log(props.title);
// context - 非响应式的对象
const {
attrs, // 非响应式的 attribute
slots, // 插槽
emit, // 触发事件
expose // 暴露公共属性
} = context;
// 响应式状态
const count = ref(0);
const state = reactive({
name: '张三',
age: 25
});
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 侦听器
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`);
});
// 生命周期钩子
onMounted(() => {
console.log('组件已挂载');
});
// 方法
const increment = () => {
count.value++;
};
// 暴露给模板
return {
count,
state,
doubleCount,
increment
};
// 使用 expose
// expose({
// increment
// });
// return {};
}
};
5.2 响应式 API
import {
ref,
reactive,
readonly,
toRef,
toRefs,
toRaw,
isRef,
isReactive,
isReadonly,
isProxy,
shallowRef,
shallowReactive,
shallowReadonly,
markRaw
} from 'vue';
export default {
setup() {
// ref - 响应式引用
const count = ref(0);
const objRef = ref({ name: '张三' });
// reactive - 响应式对象
const state = reactive({
name: '张三',
age: 25,
address: {
city: '北京',
street: '朝阳区'
}
});
// readonly - 只读代理
const readOnlyState = readonly(state);
// readOnlyState.name = '李四'; // 错误:只读
// toRef - 创建 ref
const nameRef = toRef(state, 'name');
// toRefs - 解构响应式对象
const { name, age } = toRefs(state);
// toRaw - 返回原始对象
const rawState = toRaw(state);
// 类型检查
console.log(isRef(count)); // true
console.log(isReactive(state)); // true
console.log(isReadonly(readOnlyState)); // true
console.log(isProxy(state)); // true
// shallowRef - 浅层 ref
const shallowObj = shallowRef({ nested: { value: 1 } });
shallowObj.value.nested.value = 2; // 不会触发响应
// shallowReactive - 浅层 reactive
const shallowState = shallowReactive({
nested: { value: 1 }
});
// shallowReadonly - 浅层 readonly
const shallowReadOnly = shallowReadonly({
nested: { value: 1 }
});
// markRaw - 标记为不可转为响应式
const rawObj = markRaw({ id: 1 });
const state2 = reactive({
raw: rawObj // raw 不会被转为响应式
});
return {
count,
state,
nameRef,
name,
age
};
}
};
5.3 组合式函数
// useMouse.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useMouse() {
const x = ref(0);
const y = ref(0);
function update(event) {
x.value = event.pageX;
y.value = event.pageY;
}
onMounted(() => window.addEventListener('mousemove', update));
onUnmounted(() => window.removeEventListener('mousemove', update));
return { x, y };
}
// useFetch.js
import { ref, isRef, unref, watchEffect } from 'vue';
export function useFetch(url) {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
async function doFetch() {
data.value = null;
error.value = null;
loading.value = true;
try {
const response = await fetch(unref(url));
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
}
if (isRef(url)) {
watchEffect(doFetch);
} else {
doFetch();
}
return { data, error, loading, retry: doFetch };
}
// 在组件中使用
import { useMouse, useFetch } from './composables';
export default {
setup() {
const { x, y } = useMouse();
const { data, error, loading } = useFetch('/api/user');
return {
x,
y,
data,
error,
loading
};
}
};
5.4 依赖注入
// 提供依赖
import { provide, inject, reactive, readonly } from 'vue';
// 创建 Symbol 作为 key
const ThemeSymbol = Symbol('theme');
export function provideTheme() {
const theme = reactive({
primary: '#1890ff',
secondary: '#f0f2f5',
mode: 'light'
});
const updateTheme = (newTheme) => {
Object.assign(theme, newTheme);
};
provide(ThemeSymbol, {
theme: readonly(theme),
updateTheme
});
return { theme, updateTheme };
}
// 注入依赖
import { inject } from 'vue';
import { ThemeSymbol } from './theme';
export function useTheme() {
const context = inject(ThemeSymbol);
if (!context) {
throw new Error('必须在 provideTheme 的组件中使用 useTheme');
}
return context;
}
5.5 异步组件
// 异步组件
import { defineAsyncComponent } from 'vue';
// 基本用法
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
);
// 带加载状态
const AsyncCompWithLoading = defineAsyncComponent({
loader: () => import('./Foo.vue'),
loadingComponent: LoadingComponent,
delay: 200,
errorComponent: ErrorComponent,
timeout: 3000
});
5.6 Suspense
<template>
<!-- Suspense 组件 -->
<Suspense>
<!-- 默认插槽显示异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- fallback 插槽显示加载状态 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
<!-- 多个异步组件 -->
<Suspense>
<template #default>
<div>
<AsyncComponent1 />
<AsyncComponent2 />
</div>
</template>
</Suspense>
<!-- 错误处理 -->
<Suspense @resolve="onResolve" @pending="onPending" @fallback="onFallback">
<AsyncComponent />
</Suspense>
</template>
// 异步组件
export default {
async setup() {
// 在 setup 中执行异步操作
const data = await fetchData();
return {
data
};
}
};
六、状态管理
6.3 Pinia 与 Vuex 对比
| 特性 | Pinia | Vuex |
|---|---|---|
| 创建时间 | 2019年,Vue 3 时代 | 2015年,Vue 2 时代 |
| API 设计 | 组合式 API 风格 | 选项式 API 风格 |
| TypeScript 支持 | 一流的 TypeScript 支持 | 需要类型定义帮助 |
| 模块化 | 自动代码分割,多个 store | 模块系统 |
| 开发工具 | Vue DevTools 支持 | Vue DevTools 支持 |
| 大小 | 约 1KB | 约 10KB |
| 学习曲线 | 更简单,概念更少 | 较复杂,概念较多 |
| 推荐使用 | Vue 3 项目首选 | 遗留项目维护 |
6.4 状态管理最佳实践
// 1. 模块化组织
// store/
// ├── index.js // 主 store
// ├── modules/
// │ ├── user.js
// │ ├── cart.js
// │ └── product.js
// └── types.js // TypeScript 类型定义
// 2. 类型安全
// types.ts
export interface User {
id: number;
name: string;
email: string;
age: number;
}
export interface Product {
id: number;
name: string;
price: number;
stock: number;
}
// 3. 数据持久化
// 使用插件实现
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';
const pinia = createPinia();
pinia.use(piniaPluginPersist);
// 4. 服务层分离
// services/userService.js
export class UserService {
async fetchUsers() {
const response = await fetch('/api/users');
return response.json();
}
async updateUser(user) {
const response = await fetch(`/api/users/${user.id}`, {
method: 'PUT',
body: JSON.stringify(user)
});
return response.json();
}
}
// 5. 状态规范化
// 避免嵌套过深,扁平化数据结构
const normalizedState = {
users: {
byId: {
'1': { id: 1, name: '张三' },
'2': { id: 2, name: '李四' }
},
allIds: [1, 2]
},
products: {
byId: {
'101': { id: 101, name: '商品1' },
'102': { id: 102, name: '商品2' }
},
allIds: [101, 102]
}
};
七、路由(Vue Router)
7.1 Vue Router 基础
// 安装和配置
import { createRouter, createWebHistory } from 'vue-router';
// 路由配置
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
requiresAuth: true,
title: '首页'
}
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
meta: {
title: '关于我们'
}
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true, // 将路由参数作为 props 传递
children: [
{
path: 'profile',
component: () => import('@/views/UserProfile.vue')
},
{
path: 'posts',
component: () => import('@/views/UserPosts.vue')
}
]
},
{
path: '/product/:id(\\d+)', // 正则表达式约束
name: 'Product',
component: () => import('@/views/Product.vue')
},
{
path: '/:pathMatch(.*)*', // 404 页面
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
];
// 创建路由实例
const router = createRouter({
history: createWebHistory(), // HTML5 模式
// history: createWebHashHistory(), // Hash 模式
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
}
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 修改页面标题
document.title = to.meta.title || '默认标题';
// 身份验证检查
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login');
} else {
next();
}
});
// 全局后置钩子
router.afterEach((to, from) => {
// 发送页面浏览统计
sendToAnalytics(to.fullPath);
});
// 导出
export default router;
7.2 路由组件中使用
// 在组件中使用路由
import { useRouter, useRoute, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router';
export default {
setup() {
const router = useRouter();
const route = useRoute();
// 编程式导航
const goToHome = () => {
router.push('/');
// 或 router.push({ name: 'Home' });
// 或 router.push({ path: '/' });
};
const goBack = () => {
router.go(-1);
};
const replaceRoute = () => {
router.replace('/about');
};
// 路由参数
const userId = computed(() => route.params.id);
const queryParam = computed(() => route.query.search);
// 路由守卫
onBeforeRouteUpdate((to, from) => {
// 当前路由改变,但是该组件被复用时调用
console.log('路由更新', to.params.id);
});
onBeforeRouteLeave((to, from) => {
// 导航离开该组件的对应路由时调用
const answer = window.confirm('确定要离开吗?');
if (!answer) return false;
});
return {
goToHome,
goBack,
replaceRoute,
userId,
queryParam
};
}
};
<!-- 模板中使用 -->
<template>
<!-- 路由链接 -->
<router-link to="/">首页</router-link>
<router-link :to="{ name: 'About' }">关于</router-link>
<router-link :to="{ path: '/user/123' }">用户123</router-link>
<router-link
:to="{
name: 'User',
params: { id: 456 },
query: { tab: 'profile' },
hash: '#section'
}"
custom
v-slot="{ href, route, navigate, isActive, isExactActive }"
>
<a
:href="href"
@click="navigate"
:class="{ active: isActive }"
>
自定义链接
</a>
</router-link>
<!-- 路由视图 -->
<router-view></router-view>
<!-- 命名视图 -->
<router-view name="header"></router-view>
<router-view></router-view>
<router-view name="footer"></router-view>
<!-- 过渡效果 -->
<router-view v-slot="{ Component }">
<transition name="fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
7.3 路由元信息
// 路由配置中使用 meta
const routes = [
{
path: '/admin',
meta: {
requiresAuth: true,
requiresAdmin: true,
breadcrumb: '管理后台',
transition: 'slide'
},
children: [
{
path: 'dashboard',
component: Dashboard,
meta: {
breadcrumb: '仪表板',
icon: 'dashboard'
}
}
]
}
];
// 在导航守卫中使用
router.beforeEach((to, from, next) => {
// 检查是否需要身份验证
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
});
} else if (to.matched.some(record => record.meta.requiresAdmin)) {
if (!auth.isAdmin()) {
next({ path: '/unauthorized' });
} else {
next();
}
} else {
next();
}
} else {
next();
}
});
// 在组件中访问
export default {
setup() {
const route = useRoute();
// 获取当前路由的 meta
const meta = computed(() => {
return route.meta;
});
// 获取匹配的所有路由记录的 meta
const matchedMetas = computed(() => {
return route.matched.map(record => record.meta);
});
return { meta, matchedMetas };
}
};
7.4 动态路由
// 添加路由
const router = createRouter({ /* ... */ });
// 动态添加路由
const adminRoutes = [
{
path: '/admin',
component: AdminLayout,
children: [
{ path: 'users', component: UserList },
{ path: 'settings', component: Settings }
]
}
];
// 添加路由
adminRoutes.forEach(route => {
router.addRoute(route);
});
// 添加嵌套路由
router.addRoute('admin', {
path: 'profile',
component: AdminProfile
});
// 删除路由
const removeRoute = router.addRoute(adminRoute);
removeRoute(); // 删除路由
// 或通过名称删除
router.removeRoute('admin');
// 检查路由是否存在
if (!router.hasRoute('admin')) {
router.addRoute(adminRoute);
}
// 获取路由列表
const routeRecords = router.getRoutes();
console.log(routeRecords);
7.5 路由懒加载
// 1. 动态 import
const Home = () => import('./views/Home.vue');
// 2. 分组(webpack 魔法注释)
const UserProfile = () => import(/* webpackChunkName: "user" */ './views/UserProfile.vue');
const UserSettings = () => import(/* webpackChunkName: "user" */ './views/UserSettings.vue');
// 3. Vue 3 的 defineAsyncComponent
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
);
// 4. 高级懒加载配置
const LazyComponent = defineAsyncComponent({
loader: () => import('./HeavyComponent.vue'),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200, // 延迟显示加载组件
timeout: 3000, // 超时时间
suspensible: false // 是否使用 Suspense
});
// 路由配置中使用
const routes = [
{
path: '/dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
}
];
八、Vue 生态和工具
8.1 构建工具
-
Vite
// vite.config.js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { resolve } from 'path'; export default defineConfig({ plugins: [vue()], // 解析 resolve: { alias: { '@': resolve(__dirname, 'src') } }, // 开发服务器 server: { port: 3000, open: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }, // 构建配置 build: { outDir: 'dist', sourcemap: true, rollupOptions: { output: { manualChunks: { vendor: ['vue', 'vue-router', 'pinia'], ui: ['element-plus'] } } } }, // 环境变量 envPrefix: 'VUE_' }); -
Vue CLI
// vue.config.js module.exports = { // 基本路径 publicPath: process.env.NODE_ENV === 'production' ? '/production-sub-path/' : '/', // 输出目录 outputDir: 'dist', // 静态资源目录 assetsDir: 'static', // 开发服务器 devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } }, // webpack 配置 configureWebpack: { plugins: [], resolve: { alias: { '@': require('path').join(__dirname, 'src') } } }, // 链式配置 chainWebpack: config => { config.plugin('html').tap(args => { args[0].title = 'Vue App'; return args; }); }, // 生产环境 source map productionSourceMap: false, // CSS 相关配置 css: { extract: true, sourceMap: false, loaderOptions: { sass: { additionalData: `@import "@/styles/variables.scss";` } } } };
8.2 UI 组件库
-
Element Plus
// 安装 npm install element-plus // 完整引入 import ElementPlus from 'element-plus'; import 'element-plus/dist/index.css'; app.use(ElementPlus); // 按需引入 import { createApp } from 'vue'; import { ElButton, ElSelect } from 'element-plus'; const app = createApp(App); app.component(ElButton.name, ElButton); app.component(ElSelect.name, ElSelect); -
Ant Design Vue
// 安装 npm install ant-design-vue@next // 使用 import { createApp } from 'vue'; import Antd from 'ant-design-vue'; import 'ant-design-vue/dist/antd.css'; const app = createApp(App); app.use(Antd); -
Vuetify
// 安装 npm install vuetify@^3.0.0 // 使用 import { createApp } from 'vue'; import { createVuetify } from 'vuetify'; import 'vuetify/styles'; const vuetify = createVuetify({ theme: { defaultTheme: 'light', themes: { light: { colors: { primary: '#1867C0', secondary: '#5CBBF6' } } } } }); app.use(vuetify);
8.3 工具库
// 1. VueUse - 组合式工具集合
import {
useMouse,
useLocalStorage,
useFetch,
useClipboard,
useDark
} from '@vueuse/core';
// 2. lodash-es - 实用工具
import { debounce, throttle, cloneDeep } from 'lodash-es';
// 3. dayjs - 日期处理
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
// 4. axios - HTTP 客户端
import axios from 'axios';
// 创建实例
const api = axios.create({
baseURL: '/api',
timeout: 10000
});
// 请求拦截器
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// 响应拦截器
api.interceptors.response.use(
response => response.data,
error => {
if (error.response.status === 401) {
// 处理未授权
}
return Promise.reject(error);
}
);
8.4 测试工具
// 1. Vitest - 单元测试
// vitest.config.js
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom'
}
});
// 测试示例
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
describe('Counter', () => {
it('increments count', async () => {
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('Count: 1');
});
});
// 2. Cypress - E2E 测试
// cypress/e2e/counter.cy.js
describe('Counter', () => {
it('increments count', () => {
cy.visit('/');
cy.contains('button', '增加').click();
cy.contains('Count: 1');
});
});
// 3. Vue Test Utils
import { shallowMount } from '@vue/test-utils';
import MyComponent from './MyComponent.vue';
describe('MyComponent', () => {
it('renders props', () => {
const wrapper = shallowMount(MyComponent, {
props: { msg: 'Hello' }
});
expect(wrapper.text()).toContain('Hello');
});
});
8.5 代码质量工具
// 1. ESLint
// .eslintrc.js
module.exports = {
root: true,
env: {
node: true,
browser: true
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/multi-word-component-names': 'off'
}
};
// 2. Prettier
// .prettierrc
{
"semi": true,
"tabWidth": 4,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}
// 3. Stylelint
// .stylelintrc
{
"extends": [
"stylelint-config-standard",
"stylelint-config-recommended-vue"
],
"rules": {
"selector-class-pattern": null
}
}
// 4. Husky + lint-staged
// package.json
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"]
}
}
九、服务端渲染(SSR)和静态站点生成(SSG)
9.1 Nuxt.js
// nuxt.config.js
export default defineNuxtConfig({
// 应用配置
app: {
head: {
title: 'Nuxt 3 应用',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
}
},
// 模块
modules: [
'@nuxtjs/tailwindcss',
'@pinia/nuxt',
'@nuxt/image'
],
// 构建模块
buildModules: [
'@nuxt/typescript-build'
],
// 运行时配置
runtimeConfig: {
public: {
apiBase: process.env.API_BASE || 'http://localhost:3000'
}
},
// 渲染模式
ssr: true, // 服务端渲染
// 目标
target: 'static', // 静态站点生成
// 路由
router: {
middleware: ['auth']
},
// 插件
plugins: [
'~/plugins/axios.js'
],
// 构建配置
build: {
extractCSS: true,
transpile: ['some-package']
}
});
9.2 Vue 服务端渲染
// server.js - 基本的 SSR 实现
import express from 'express';
import { createSSRApp } from 'vue';
import { renderToString } from '@vue/server-renderer';
const server = express();
server.get('*', async (req, res) => {
// 创建 Vue 应用实例
const app = createSSRApp({
data: () => ({ url: req.url }),
template: `<div>访问的 URL 是: {{ url }} </div>`
});
// 渲染为 HTML
const appContent = await renderToString(app);
// 完整的 HTML
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR</title>
</head>
<body>
<div id="app">${appContent}</div>
<script>
// 客户端激活
window.__INITIAL_STATE__ = ${JSON.stringify({ url: req.url })}
</script>
<script src="/client.js"></script>
</body>
</html>
`;
res.send(html);
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
9.3 静态站点生成
// 使用 VitePress
// .vitepress/config.js
module.exports = {
title: 'VitePress',
description: 'Vite & Vue powered static site generator.',
themeConfig: {
nav: [
{ text: '指南', link: '/guide/' },
{ text: '配置', link: '/config/' }
],
sidebar: {
'/guide/': [
{
text: '指南',
children: [
{ text: '介绍', link: '/guide/' },
{ text: '开始', link: '/guide/getting-started' }
]
}
]
}
},
// 构建配置
build: {
outDir: '../dist'
}
};
9.4 混合渲染
// Nuxt.js 混合渲染
export default defineNuxtConfig({
routeRules: {
// 首页预渲染
'/': { prerender: true },
// 产品页静态生成
'/products/**': { static: true },
// 用户页服务端渲染
'/users/**': { ssr: true },
// API 路由
'/api/**': { cors: true },
// 动态路由
'/blog/**': { swr: 3600 } // 缓存 1 小时
}
});
十、Vue 3 新特性
10.1 组合式 API
// 1. setup 语法糖
<script setup>
import { ref, computed, onMounted } from 'vue';
// 响应式状态
const count = ref(0);
const name = ref('Vue');
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
// 生命周期
onMounted(() => {
console.log('组件已挂载');
});
// 自动暴露给模板
</script>
<template>
<button @click="increment"> {{ count }} </button>
</template>
// 2. 响应性系统改进
import {
reactive,
ref,
readonly,
shallowReactive,
shallowRef,
toRef,
toRefs,
customRef
} from 'vue';
// 自定义 ref
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
}
};
});
}
// 在组件中使用
const text = useDebouncedRef('hello');
10.2 Teleport
<template>
<!-- 将组件传送到 body -->
<teleport to="body">
<div v-if="showModal" class="modal">
<h2>模态框标题</h2>
<button @click="showModal = false">关闭</button>
</div>
</teleport>
<!-- 多个目标 -->
<teleport to="#modals">
<div>模态框 1</div>
</teleport>
<teleport to="#modals">
<div>模态框 2</div>
</teleport>
<!-- 条件传送 -->
<teleport :to="target">
<div>动态目标</div>
</teleport>
</template>
<script setup>
import { ref } from 'vue';
const showModal = ref(false);
const target = ref('#modals');
</script>
10.3 Fragments
<!-- Vue 3 支持多个根元素 -->
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template>
<!-- 不需要包裹 div -->
10.4 Emits 选项
<script setup>
// 定义 emits
const emit = defineEmits(['update:modelValue', 'submit']);
// 触发事件
const handleSubmit = () => {
emit('submit', formData);
};
</script>
<!-- 或使用对象语法 -->
<script>
export default {
emits: {
// 没有验证
click: null,
// 带验证
submit: (payload) => {
if (payload.email && payload.password) {
return true;
} else {
console.warn('Submit 事件无效');
return false;
}
}
}
};
</script>
10.5 自定义渲染器
// 创建自定义渲染器
import { createRenderer } from '@vue/runtime-core';
const { render, createApp } = createRenderer({
// 平台特定的 API
insert: (child, parent, anchor) => {
// 插入元素
},
remove: (child) => {
// 移除元素
},
createElement: (tag, isSVG, is) => {
// 创建元素
},
setElementText: (el, text) => {
// 设置文本
},
// ... 其他节点操作
});
// 创建 Canvas 渲染器示例
const { createApp: createCanvasApp } = createRenderer({
createElement(type) {
return { type };
},
patchProp(el, key, prevValue, nextValue) {
// 处理属性
},
insert(el, parent) {
// 绘制到 Canvas
}
});
10.6 全局 API 变更
// Vue 2
import Vue from 'vue';
Vue.component('MyComponent', MyComponent);
Vue.directive('focus', focusDirective);
Vue.mixin({ /* ... */ });
Vue.use(MyPlugin);
// Vue 3
import { createApp } from 'vue';
const app = createApp({});
app.component('MyComponent', MyComponent);
app.directive('focus', focusDirective);
app.mixin({ /* ... */ });
app.use(MyPlugin);
app.mount('#app');
10.7 其他新特性
-
Suspense
<template> <Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> <div>加载中...</div> </template> </Suspense> </template> -
v-model 参数
<template> <!-- Vue 2: 一个组件只能有一个 v-model --> <!-- Vue 3: 多个 v-model --> <CustomInput v-model:first-name="firstName" v-model:last-name="lastName" /> <!-- 自定义修饰符 --> <CustomInput v-model.capitalize="text" /> </template> -
样式特性
<style scoped> /* 深度选择器 */ :deep(.child) { color: red; } /* 插槽选择器 */ :slotted(div) { color: blue; } /* 全局选择器 */ :global(.global-class) { font-weight: bold; } </style>
十一、性能优化
11.1 代码分割
// 1. 路由懒加载
const router = createRouter({
routes: [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
});
// 2. 组件懒加载
import { defineAsyncComponent } from 'vue';
const HeavyComponent = defineAsyncComponent(() =>
import('./HeavyComponent.vue')
);
// 3. 第三方库分割
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-library': ['element-plus']
}
}
}
}
});
11.2 虚拟滚动
// 使用 vue-virtual-scroller
import { RecycleScroller } from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
export default {
components: {
RecycleScroller
},
data() {
return {
items: Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `项目 ${i}`
}))
};
}
};
<template>
<RecycleScroller
class="scroller"
:items="items"
:item-size="50"
key-field="id"
>
<template v-slot="{ item }">
<div class="item"> {{ item.name }} </div>
</template>
</RecycleScroller>
</template>
<style>
.scroller {
height: 400px;
}
.item {
height: 50px;
line-height: 50px;
border-bottom: 1px solid #eee;
}
</style>
11.3 图片优化
// 使用 vue-lazyload
import { createApp } from 'vue';
import VueLazyload from 'vue-lazyload';
const app = createApp(App);
app.use(VueLazyload, {
preLoad: 1.3,
error: 'error.png',
loading: 'loading.gif',
attempt: 1
});
<!-- 图片懒加载 -->
<img v-lazy="imageUrl" />
<!-- 响应式图片 -->
<picture>
<source media="(min-width: 1200px)" :srcset="largeImage" />
<source media="(min-width: 768px)" :srcset="mediumImage" />
<img :src="smallImage" alt="描述" />
</picture>
<!-- WebP 格式 -->
<picture>
<source type="image/webp" :srcset="webpImage" />
<source type="image/jpeg" :srcset="jpegImage" />
<img :src="jpegImage" alt="描述" />
</picture>
11.4 组件优化
// 1. 函数式组件
const FunctionalButton = (props, context) => {
return h('button', {
onClick: props.onClick
}, context.slots.default());
};
// 2. 异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
);
// 3. KeepAlive
<KeepAlive :include="['Home', 'About']" :max="10">
<component :is="currentComponent" />
</KeepAlive>
// 4. v-once
<span v-once> {{ staticContent }} </span>
// 5. v-memo
<div v-memo="[valueA, valueB]">
<!-- 只在 valueA 或 valueB 变化时更新 -->
</div>
11.5 性能监控
// 性能测量
import { onMounted, onUnmounted } from 'vue';
export default {
setup() {
let startTime;
onMounted(() => {
startTime = performance.now();
// 监听长任务
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('长任务:', entry);
}
}
});
observer.observe({ entryTypes: ['longtask'] });
});
onUnmounted(() => {
const loadTime = performance.now() - startTime;
console.log(`组件渲染时间: ${loadTime}ms`);
// 发送到监控服务
sendToAnalytics({ loadTime });
});
}
};
11.6 构建优化
// vite.config.js
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
// 分析包大小
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
],
build: {
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// 资源优化
assetsInlineLimit: 4096, // 4KB 以下内联
// 代码分割
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia']
}
}
}
},
// 预构建
optimizeDeps: {
include: ['vue', 'vue-router']
}
});
十二、测试
12.1 单元测试
// Counter.vue
<script setup>
import { ref } from 'vue';
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<template>
<button @click="increment">点击 {{ count }} </button>
</template>
// Counter.spec.js
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
describe('Counter.vue', () => {
it('渲染初始状态', () => {
const wrapper = mount(Counter);
expect(wrapper.text()).toContain('点击 0');
});
it('点击按钮增加计数', async () => {
const wrapper = mount(Counter);
await wrapper.find('button').trigger('click');
expect(wrapper.text()).toContain('点击 1');
});
it('触发自定义事件', async () => {
const wrapper = mount(Counter, {
props: {
initialCount: 5
}
});
expect(wrapper.text()).toContain('点击 5');
});
});
12.2 组件测试
// 测试组件 props
it('接收 props', () => {
const wrapper = mount(Component, {
props: {
msg: 'Hello'
}
});
expect(wrapper.props('msg')).toBe('Hello');
});
// 测试组件事件
it('触发事件', async () => {
const wrapper = mount(Component);
await wrapper.find('button').trigger('click');
expect(wrapper.emitted('submit')).toBeTruthy();
});
// 测试插槽
it('渲染插槽内容', () => {
const wrapper = mount(Component, {
slots: {
default: '插槽内容'
}
});
expect(wrapper.text()).toContain('插槽内容');
});
// 测试异步行为
it('处理异步操作', async () => {
const wrapper = mount(AsyncComponent);
await wrapper.vm.$nextTick();
expect(wrapper.text()).toContain('加载完成');
});
12.3 集成测试
// 测试组件组合
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import App from './App.vue';
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/', component: { template: '<div>Home</div>' } }]
});
const pinia = createPinia();
describe('App 集成测试', () => {
it('完整应用渲染', async () => {
const wrapper = mount(App, {
global: {
plugins: [router, pinia]
}
});
expect(wrapper.html()).toContain('div');
});
});
12.4 E2E 测试
// Cypress 测试
describe('用户登录流程', () => {
it('成功登录', () => {
cy.visit('/login');
cy.get('[data-test="username"]').type('user@example.com');
cy.get('[data-test="password"]').type('password123');
cy.get('[data-test="submit"]').click();
cy.url().should('include', '/dashboard');
cy.get('[data-test="welcome"]').should('contain', 'Welcome');
});
it('登录失败显示错误', () => {
cy.visit('/login');
cy.get('[data-test="username"]').type('wrong@example.com');
cy.get('[data-test="password"]').type('wrong');
cy.get('[data-test="submit"]').click();
cy.get('[data-test="error"]').should('be.visible');
});
});
12.5 测试工具配置
// vitest.config.js
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
coverage: {
provider: 'c8',
reporter: ['text', 'json', 'html']
},
setupFiles: ['./tests/setup.js']
}
});
// tests/setup.js
import { config } from '@vue/test-utils';
// 全局配置
config.global.stubs = {
Transition: false,
'transition-group': false
};
// 全局组件
config.global.components = {
'RouterLink': { template: '<a><slot /></a>' }
};
十三、TypeScript 支持
13.1 基本类型
// 组件 Props 类型
interface Props {
msg: string;
count?: number;
items: string[];
user: {
name: string;
age: number;
};
onClick: (event: MouseEvent) => void;
}
// 定义组件
import { defineComponent } from 'vue';
export default defineComponent({
props: {
msg: {
type: String as PropType<string>,
required: true
},
count: {
type: Number as PropType<number>,
default: 0
}
},
setup(props: Props) {
// props 是类型安全的
console.log(props.msg.toLowerCase());
}
});
13.2 组合式 API 类型
<script setup lang="ts">
import { ref, computed, Ref } from 'vue';
// 响应式引用
const count: Ref<number> = ref(0);
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 函数
const increment = (): void => {
count.value++;
};
// 接口定义
interface User {
id: number;
name: string;
email: string;
}
const user: Ref<User | null> = ref(null);
</script>
13.3 组件类型
// 1. 使用 defineComponent
import { defineComponent, PropType } from 'vue';
interface Book {
title: string;
author: string;
year: number;
}
export default defineComponent({
props: {
book: {
type: Object as PropType<Book>,
required: true
},
isFavorite: {
type: Boolean,
default: false
}
},
emits: {
'update:favorite': (value: boolean) => true
},
setup(props, { emit }) {
const toggleFavorite = () => {
emit('update:favorite', !props.isFavorite);
};
return { toggleFavorite };
}
});
13.4 全局类型
// types/vue.d.ts
import { ComponentCustomProperties } from 'vue';
declare module 'vue' {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date) => string;
formatCurrency: (amount: number) => string;
};
}
}
// 在组件中使用
export default {
mounted() {
console.log(this.$filters.formatDate(new Date()));
}
};
13.5 Pinia 类型
// store/user.ts
import { defineStore } from 'pinia';
interface UserState {
name: string;
age: number;
isLoggedIn: boolean;
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
name: '',
age: 0,
isLoggedIn: false
}),
getters: {
isAdult(): boolean {
return this.age >= 18;
},
userInfo(): string {
return `${this.name} (${this.age})`;
}
},
actions: {
async login(username: string, password: string): Promise<boolean> {
// 登录逻辑
return true;
}
}
});
13.6 Vue Router 类型
// router/index.ts
import { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
requiresAuth: true
}
}
];
// 扩展 RouteMeta
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean;
title?: string;
roles?: string[];
}
}
十四、学习资源
14.1 官方资源
- Vue.js 官网:
- Vue.js 中文官网:
- Vue Router 文档:
- Pinia 文档:
十四、学习资源
14.1 官方资源
- Vue.js 官方网站:https://vuejs.org (英文) / https://cn.vuejs.org (中文)
- 核心文档、教程、API 参考,是学习 Vue 最权威的来源。
- Vue Router 文档:https://router.vuejs.org
- 官方路由管理器,用于构建单页面应用 (SPA)。
- Pinia 文档:https://pinia.vuejs.org
- Vue 官方推荐的下一代状态管理库,用于替代 Vuex。
- Vite 文档:https://vitejs.dev
- 新一代前端构建工具,Vue 项目的推荐构建方案,提供极速的开发服务器启动和热更新。
- Vue.js GitHub 仓库:https://github.com/vuejs/core
- 核心源码仓库,可了解最新进展、提交 Issue 或参与贡献。
- VueUse 文档:https://vueuse.org
- Nuxt.js 文档:https://nuxt.com
- Vue Test Utils:https://test-utils.vuejs.org
- RFC(征求意见稿):https://github.com/vuejs/rfcs
14.2 教程与课程
- Vue.js 官方教程:包含交互式练习,是入门的最佳起点。
- Vue Mastery:高质量的付费视频课程,涵盖从基础到高级的各个主题。
- Vue School:提供免费和付费课程,内容更新及时。
- freeCodeCamp:提供免费的 Vue.js 响应式网页设计认证课程。
- YouTube 频道:
Vue.js(官方):发布核心团队分享和新特性介绍。Vue Mastery/Vue School:课程节选和技巧分享。Traversy Media,The Net Ninja:包含丰富的 Vue 项目实战教程。
14.3 推荐书籍
- 入门与实战
- 《Vue.js 设计与实现》- 霍春阳
- 深入解析 Vue 3 的响应式系统、编译器和运行时核心,适合希望深入理解原理的开发者。
- 《Vue.js 项目实战》- 系列实战书籍
- 通过完整的项目案例学习 Vue 的全栈开发。
- 《Vue.js 设计与实现》- 霍春阳
- 深入原理
- 《深入浅出 Vue.js》- 刘博文
- 针对 Vue 2 的源码进行深入剖析,是理解 Vue 设计思想的经典之作。
- 《深入浅出 Vue.js》- 刘博文
14.4 社区与资讯
- GitHub:关注
vuejs组织下的核心项目(core, router, pinia, vite)及热门 UI 库。 - 掘金/思否:中文开发者社区,有大量 Vue 相关的技术文章、实战经验和问题讨论。
- Discord / 论坛:Vue 官方社区,是获取帮助和与社区交流的直接渠道。
- 博客与周刊:
Vue.js Blog:官方博客,发布版本更新和新特性介绍。Vue.js Developers Newsletter:订阅获取每周精选的 Vue 文章和资源。新闻:关注前端和 Vue 生态的最新动态。
14.5 工具与生态
- 浏览器开发工具:
- Vue DevTools:浏览器扩展,用于调试 Vue 应用组件树、状态和事件。
- IDE/编辑器插件:
- Volar:VS Code 官方推荐的 Vue 语言支持插件,对 Vue 3 和 TypeScript 提供一流支持。
- Vue VSCode Snippets:提供丰富的代码片段,提升开发效率。
- UI 组件库:
- Element Plus:基于 Vue 3,面向桌面端的组件库。
- Ant Design Vue:企业级 UI 设计语言和 React 实现 的 Vue 版本。
- Vuetify:Material Design 风格的组件框架。
- Naive UI:一个全量的 Vue 3 组件库,比较完整,主题可调,使用 TypeScript 开发。
- 实用工具库:
- VueUse:一个基于 Composition API 的实用函数集合,极大地提高了开发效率。
- Vue-i18n:国际化插件。
- Vue Router:官方路由管理器。
14.6 学习建议与路径
- 基础阶段 (1-2周):
- 完成 Vue 官方指南,掌握模板语法、响应式基础、计算属性、侦听器、条件与列表渲染、事件处理、表单输入绑定、组件基础。
- 在 CodePen、CodeSandbox 或本地使用 Vite 创建项目进行实践。
- 核心进阶 (2-3周):
- 深入学习组件(Props、自定义事件、插槽)、生命周期、组合式 API (
setup,ref,reactive,computed,watch)。 - 学习 Vue Router 实现页面路由,学习 Pinia 进行状态管理。
- 深入学习组件(Props、自定义事件、插槽)、生命周期、组合式 API (
- 工程化与实战 (3-4周):
- 掌握 Vite 构建工具,学习使用 TypeScript 增强代码健壮性。
- 选择一个 UI 组件库,并尝试开发一个中等复杂度的全栈项目(如后台管理系统、博客平台)。
- 学习单元测试 (Vitest) 和端到端测试 (Cypress)。
- 持续深入:
- 关注 Vue 和 Vite 的 RFC(征求意见稿),了解生态发展趋势。
- 研究 Nuxt.js 等服务端渲染/静态站点生成框架。
- 阅读优秀开源项目源码,参与社区讨论和贡献。