VUE
Vue是一种流行的JavaScript框架,用于构建用户界面和单页面应用程序(SPA)。它是一个开源项目,专注于视图层,旨在简化前端开发,使开发者能够更轻松地构建交互式和动态的Web应用程序。
Vue实例
每个 Vue 应用都是通过createApp函数创建一个新的应用实例:
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})应用实例并不只限于一个。createApp API 允许你在同一个页面中创建多个共存的 Vue 应用,而且每个应用都拥有自己的用于配置和全局资源的作用域。
使用npm创建基于Vite的Vue项目
npm create vue@latestVue组件
在Vue中,每个Vue文件就是一个组件。传入createApp函数中的对象也是一个Vue组件,称为根组件,其他组件称为子组件。以下示例演示导入一个Vue组件。
import { createApp } from 'vue'
// 从一个单文件组件中导入根组件
import App from './App.vue'
const app = createApp(App)挂载应用
在Vue中使用应用示例的mount()方法将实例挂载到DOM元素上,mount()方法返回的时一个根组件实例。
<div id="app"></div>app.mount('#app')应用配置
应用实例会暴露一个 .config 对象允许我们配置一些应用级的选项,例如定义一个应用级的错误处理器,用来捕获所有子组件上的错误:
app.config.errorHandler = (err) => {
/* 处理错误 */
};应用组件
应用实例还提供了一些方法来注册应用范围内可用的资源,例如注册一个组件:
app.component('TodoDeleteButton', TodoDeleteButton);这使得 TodoDeleteButton 在应用的任何地方都是可用的
模板语法
插值表达式
:插值表达式,将数据与页面显示内容进行双向绑定,仅可在填充元素内容时使用。
<span>这是一段信息:{{msg}}</span>显示原始HTML
v-html:显示原始HTML,v-html属性绑定的文本将会作为原始HTML插入到元素内部。
rawHtml="<span style="color: red">This should be red.</span>";
<span v-html="rawHtml"></span>
最终显示:
<span><span style="color: red">This should be red.</span></span>属性绑定
v-bind:响应式地绑定一个元素的属性,它需要接受一个属性参数。如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。
<span v-bind:id="dynamicId"></span>
简写方式:
<span :id="dynamicId"></span>
v3.4+支持的简写方式,此时id绑定的是名为id的变量:
<span :id></span>
绑定多个属性:
const objectOfAttrs = {
id: 'container',
class: 'wrapper',
style: 'background-color:green'
}
<div v-bind="objectOfAttrs"></div>
动态属性参数:
<div v-bind:[attributeName]="attributeValue"></div>
动态属性参数-缩写:
<div :[attributeName]="attributeValue"></div>事件绑定
v-on:事件绑定,缩写为@。
<button v-on:click="doSomeThing">点我</button>
缩写:
<button @click="doSomeThing">点我</button>
动态事件参数:
<button v-on:[eventName]="doSomeThing">点我</button>
动态事件参数-缩写:
<button @[eventName]="doSomeThing">点我</button>条件渲染
v-show:当为true时,元素的display属性会被设置为true。当为false时,元素的display属性会被设置为false。
<div v-show="ifShow">这是一个div</div>v-if:当为true时显示元素。当为false时销毁元素。v-else-if:与v-if一同使用,必须紧跟着v-if。v-else:与v-if一同使用,必须紧跟着v-if或v-else-if。
<div v-if="condition1">条件1</div>
<div v-else-if="condition2">条件2</div>
<div v-else="condition3">条件3</div>循环渲染
v-for:基于数组对元素进行循环渲染。
<li v-for="item in items">{{item.data}}</li>
<li v-for="(item,index) in items">{{index}}-{{item.data}}</li>
<li v-for="(value,key,index) in object">{{index}}-{{key}}{{value}}</li>注意事项:
- 当同时在一个节点上,同时使用
v-if和v-for时,v-if的优先级更高。- 推荐在任何可行的时候为
v-for提供一个 key attribute。
表单数据绑定双向绑定
v-model:给各种表单元素绑定模型数据,会根据所使用的元素自动使用对应的DOM属性和事件组合。
- 文本类型的
<input>和<textarea>元素会绑定 value 属性 并侦听 input 事件; <input type="checkbox">和<input type="radio">会绑定 checked 属性 并侦听 change 事件;<select>会绑定 value 属性 并侦听 change 事件。
true-value、false-value是vue的特有表单属性,它需要结合v-model使用,表示在复选框或单选框被选择时或未被选择时的值。它们也支持使用v-bind与数据变量绑定。
<script setup>
import { ref } from 'vue'
const toggle = ref()
const dynamicFalseValue=ref('fff');
const dynamicTrueValue=ref('ttt');
</script>
<template>
<div>toggle:{{toggle}}</div>
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" id="myBox"/>
<label for="myBox">toggle</label>
</template>
<style>
</style>
修饰符
- lazy:
v-model在默认情况下,会在每次input事件后更新数据。lazy修饰符可以使,v-model在change事件后更新数据。 - number:会将用户输入转换为数字,如果该值无法被
parseFloat()处理,那么将返回原始值。 - trim:自动去除用户输入内容中两端的空格
响应式变量
Vue中使用ref()或reactive()方法,定义响应式变量,一般推荐使用ref()。
import {ref,reactive} from 'vue';
const a=ref(2);
const b=reactive({count:3});区别总结
- 数据类型:
- ref:适用于任意类型的数据,包括原始类型和对象。
- reactive:适用于对象。
- 使用方式:
- ref:需要通过 .value 访问和修改其包装的值。
- reactive:直接对对象的属性进行访问和修改。
- 响应式对象的深度:
- ref:对于嵌套对象的属性不会自动变为响应式,需要手动处理。
- reactive:会递归地将对象的所有属性都转换为响应式。
自动解包的情况
- 模板中:在模板(template)中使用
ref时,Vue会自动解包引用的值,这样你就不需要在模板中使用.value。 - 解构赋值时:在
setup函数返回的对象中,如果解构赋值包含ref,Vue 也会自动解包。
不会自动解包的情况
- 在JavaScript中使用:在JavaScript代码中,
ref不会自动解包,必须显式地使用.value来访问引用的值。 - 在组合式API中传递ref:当把ref传递给其他函数时,也需要使用
.value来访问引用的值。
计算属性
Vue中使用computed()方法定义计算属性,它接受一个getter函数,getter函数返回一个值。computed()会将返回值包装为一个ref对象返回。
import {computed,ref} from 'vue';
const num=ref(3);
const computedData=computed(()=>{
return ref>10?true:false;
})计算属性会追踪num的值,当num值发生变化时,computedData的值也会发生变化。当num值不变时,使用的时缓存值,以减少计算频率。
获取组件属性
通过$attrs属性,可以在组件的模版中获取赋予给组件的属性。
组件模版:
<p :class="$attrs.class">p</p>
<span>span</span>
组件
<myComponent class="baz"/>进行渲染后显示:
<p class="baz">p</p>
<span>span</span>获取事件属性
在v-on的内联事件处理器中,使用$event获取原生DOM事件对象,或使用箭头函数,传入event对象。
<button v-on:click:"warn('warnMsg',$event)">点我</button>
<button v-on:click:"(evnet)=>warn('warnMsg',event)">点我</button>数组变化侦测
在 Vue.js 中,可以通过侦听响应式数组的变更方法来触发相关的更新。这些方法包括:
push(): 向数组末尾添加一个或多个元素,并返回新的长度。pop(): 移除数组最后一个元素,并返回该元素。shift(): 移除数组第一个元素,并返回该元素。unshift(): 向数组开头添加一个或多个元素,并返回新的长度。splice(): 通过删除或替换现有元素或者添加新元素来修改数组内容。sort(): 排序数组元素,改变原数组,并返回排序后的数组。reverse(): 颠倒数组中元素的顺序,改变原数组,并返回颠倒后的数组。 这些方法会触发 Vue 的响应系统,自动更新相关的视图以反映数组的变化。
Vue生命周期
- beforeCreate:
- created:
- beforeMount:
- mounted:
- beforeUpdate:
- updated:
- beforeUnmount:
- unmounted:

侦听器
vue提供了watch()和watchEffect()方法用于创建侦听器。侦听器用于在响应式状态发生变化时触发回调函数。
watch()方法
watch方法的参数:
- 第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。
- 第二个参数是回调函数,回调函数有两个传入值:newValue、oldValue。
const num=ref(3);
watch(num,(newValue,oldValue)=>{
console.log(`newValue:${newValue}`);
console.log(`oldValue:${oldValue}`);
})- 第三个参数是一个对象,用于提供额外的特性:
{immediate:true}:即时回调{once:true}:一次性侦听{deep:true}:对getter函数返回的对象,进行深层次侦听。{flush:post}:在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM(默认为Vue更新之前)。{flush:sync}:创建一个同步触发的侦听器,它会在 Vue 进行任何更新之前触发。
watch(
source,
(newValue, oldValue) => {
// 当 `source` 变化时,仅触发一次
},
{ once: true }
)watchEffect()方法
watchEffect是一个用来监听数据变化并执行副作用的函数。它与watch类似,但不需要显式地定义要监听的数据,而是自动追踪其依赖,并在依赖变化时重新运行副作用函数。
在使用watchEffect创建侦听器时,回调器会立即执行一次,并自动跟踪响应式数据。
const num = ref(3);
watchEffect(
() => {
console.log(`num ${num.value}`);
}
)手动关闭侦听器
watch和watchEffect方法会返回一个关闭侦听器的函数,调用该函数即可关闭侦听器。
const watch1 = watch(num, (newValue, oldValue) => {
console.log(`newValue: ${newValue}`);
console.log(`oldValue: ${oldValue}`);
});
const watch2 = watchEffect(
() => {
console.log(`num ${num.value}`);
}
)
function closeWatch() {
watch1();
watch2();
console.log('已关闭监听器');
}模板引用
Vue中使用ref属性,可以给DOM元素或组件添加引用,相当于原生中的document.getElementById()。通过ref绑定的变量,获得元素的引用。
<script setup>
import { ref } from 'vue';
const input = ref(null);
</script>
<template>
<input type="text" ref="input" value="123123">
<button @click="() => { input.focus(); }">获取焦点</button>
</template>
<style scoped></style>:ref还可以绑定一个函数,当使用:ref绑定函数时,将会传递一个el参数到函数内,这个el参数表示当前元素的引用。
<input :ref="(el) => {
/* 将 el 赋值给一个数据属性或 ref 变量 */
console.log(el)
}">组件
使用组件
在Vue中,一个.vue文件就是一个组件,可以通过在setup内导入并用于模板。
<script setup>
import CountTest from './count-test.vue';
</script>
<template>
<div>
<CountTest />
<CountTest />
<CountTest />
</div>
</template>在模板中的每一个组件标签都是一个单独的域,不会受到其他相同的组件影响。
对于Vue组件,一般推荐使用PascalCase的命名法,以与常规html标签区分开。
组件注册
要在Vue中使用一个组件,需要先对组件进行注册,才能在Vue模板中使用,注册的方式有两种:全局注册和局部注册。
全局注册:全局注册是指在整个Vue实例范围内注册组件,注册后可以在任意Vue模板中使用。注册方式如下:
import 'Component1' from './components/Component1.vue'
import 'Component2' from './components/Component2.vue'
import 'Component3' from './components/Component3.vue'
app.component('Component1',Component1);
app.component('Component2',Component2);
app.component('Component3',Component3);应尽量避免使用全局注册,因为全局注册的组件,即使没有被使用,也会被打包进JS文件中。
局部注册:指在单文件组件中引入组件,导入的组件只能在当前模板中使用。注册方式如下:
<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
import ComponentC from './ComponentC.vue'
</script>
<template>
<ComponentA />
<ComponentB />
<ComponentC />
</template>需要注意的是:局部注册的组件在后代组件中不可用。
组件属性
组件属性的声明
使用props对象声明组件属性,在父组件定义子组件时,即可设置子组件的属性。
在组合式api中,我们使用defineProps宏方法声明组件的属性,该方法接受一个字符串数组或一个对象,表示组件属性,
// Artical.vue
<script setup>
//字符串数组形式声明属性
defineProps(['todo','title']);
//对象形式声明属性
defineProps({
todo:object,
title:object
});
</script>
//ArticalList.vue
<Artical title='标题一'></Artical>
<Artical title='标题二'></Artical>
<Artical title='标题三'></Artical>defineProps方法会返回一个包含组件属性的对象
const props = defineProps(['title'])
console.log(props.title)组件属性的命名
组件属性在声明时的命名风格应使用camelCase形式。
defineProps({
greetingMessage: String
})在向子组件传递props时使用kebab-case形式,与html的attribute保持一致。
没有参数的v-bind
可以将一个对象通过v-bind传递给组件,对象的属性会自动绑定到组件上。
const post = {
id: 1,
title: 'My Journey with Vue'
}
<BlogPost v-bind="post" />
<BlogPost :id="post.id" :title="post.title" />组件属性校验
在使用defineProps宏声明组件属性时,可以同时定义属性的校验规则和属性类型。声明时传入一个对象,以属性名为key,以校验规则对象作为value。
<script setup>
defineProps({
propA:String,
propB:[String,Number],
propC:{
type:String,
require:True,
default:100
},
propD:{
//使用default自定义默认值,该函数接收组件所接收到的原始 prop 作为参数
default(rawProps){
return { message: 'hello' }
}
},
propE:{
// 自定义类型校验函数
// 在 3.4+ 版本中完整的 props 作为第二个参数传入
validator(value, props) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
}
})
</script>检验选项中的type可以接受原生的构造类型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
- Error 也可以是自定义的类型,vue会使用
instanceOf去判断值类型。
组件属性的默认值
默认情况下:
- 所有组件属性都是可选的,除非声明了
required:true。 - 未传递到子组件的prop属性,其默认值都为undefined,Boolean的默认值为false,除非声明了default。
组件事件
组件事件的基础使用
使用emits声明组件可能触发的事件,在组件中使用defineEmits宏方法声明组件可能触发的事件,它接受一个数组参数,数组为会触发的事件名称。
<script setup>
//声明了一个open事件
defineEmits(['open'])
</script>在父组件中,使用v-on或@绑定事件处理函数。
<ClickItem title="标题三" @open="content = '点击标题三'" />组件事件的命名风格
与组件属性一样,以camelCase形式命名的事件,在父组件中可以使用kebab-case形式来监听。
组件事件的参数验证
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})组件的v-model与defineModel
在Vue3.4开始,可以通过defineModel宏方法,定义v-model属性,实现父子组件数据的双向绑定。
基础使用方式如下:
//子组件
<script setup>
const model=defineModel();
functon update(){
model.value++;
}
</script>
//父组件
<template>
<Child v-model="num"></Child>
</template>通过声明多个v-model实现多个数据的双向绑定,通过:号指定绑定的值,使用方法如下:
//子组件
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>
<template>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
//父组件
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>v-model修饰符可以给v-model实现特殊处理,通过.号指定修饰符,用法如下:
//子组件
<script setup>
const [firstName, firstNameModifiers] = defineModel('firstName')
const [lastName, lastNameModifiers] = defineModel('lastName')
console.log(firstNameModifiers) // { capitalize: true }
console.log(lastNameModifiers) // { uppercase: true }
</script>
//父组件
<UserName
v-model:first-name.capitalize="first"
v-model:last-name.uppercase="last"
/>组件插槽
vue中使用<slot/>标签声明组件的插槽。
插槽允许你在父组件中插入内容,并在子组件中指定这些内容的位置和用法。
基础插槽
最基本的插槽用法是未命名插槽。它允许父组件在子组件的特定位置插入内容。
//子组件
<template>
<div>
<p>{{ title }}</p>
<p>
内容:
<slot></slot>
</p>
</div>
</template>//父组件
<template>
<div>
<Artical title="标题一">内容一</Artical>
<Artical title="标题二">内容二</Artical>
<Artical title="标题三">内容三</Artical>
</div>
</template>具名插槽
有时候你可能需要在子组件中插入多个不同的位置。这时可以使用具名插槽,居名插槽使用:号表示。
- 在父组件中使用
<template>标签和v-slot属性结合使用。 - 在子组件的
<slot>标签中,使用name属性声明插槽名称。
<!-- 父组件 -->
<template>
<ChildComponent>
<template v-slot:header>
<h1>这是标题</h1>
</template>
<template v-slot:footer>
<p>这是页脚</p>
</template>
</ChildComponent>
</template>
<!-- 子组件 -->
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>动态插槽
可以给v-slot绑定一个参数,实现动态模板。
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>以上示例中dynamicSlotName为JavaScript中定义的响应式变量。
在JavaScript中获取插槽
在JavaScript中通过$slots可以获取到父组件传递的插槽的引用。
作用域插槽
Vue的作用域插槽(Scoped Slots)是一种允许你在父组件中使用子组件内容时,将子组件的数据传递给父组件的机制。它提供了一种灵活的方式来组合组件,使组件之间的交互更加灵活和强大。作用域插槽主要用于需要复用复杂结构的场景,尤其是在需要将子组件的数据传递给父组件进行渲染的时候。
//Child.vue
<script setup>
import {ref} from 'vue';
const user=ref({
name: 'John Doe',
age: 30
});
</script>
<template>
<div>
<slot :user="user"></slot>
</div>
</template>
//Parent.vue
<template>
<child>
<template v-slot:default="slotProps">
<p>User Name: {{ slotProps.user.name }}</p>
<p>User Age: {{ slotProps.user.age }}</p>
</template>
</child>
</template>动态组件
在 Vue.js 中,<component>是一个功能强大的内置组件,可以用于动态地渲染不同的组件。它的主要功能是根据某个值动态选择并渲染对应的组件。
<component>的is属性用于指定需要渲染的组件。
<script setup>
import Artical from './Artical.vue'
import Guide from './Guide.vue';
import { KeepAlive, ref } from 'vue';
const current = ref(Artical);
function change() {
current.value = (current.value.__name == 'Artical' ? Guide : Artical);
console.log(current.value);
}
</script>
<template>
<div><button @click="change">切换</button>
<KeepAlive include="Artical">
<component :is="current"></component>
</KeepAlive>
</div>
</template>
<style scoped></style>默认情况下,<component>标签在多个组件切换时,被切换掉的组件会被卸载掉。可以通过<keep-alive>或<KeepAlive>组件强制被切换掉的组件仍然保持“存活”的状态。
组件的继承性
“透传 attribute”指的是在模板上传递属性给一个组件,该属性却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。
属性的透传
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。如根元素已存在同名的attribute,则属性会被合并。
事件的透传
当一个组件以单个元素为根作渲染时,对组件使用的v-on事件监听器,渲染后会被添加到根元素上。如组件上的根元素相同的事件也绑定了事件监听器,事件发生时,父组件和子组件的事件监听器都会被触发。
禁用Attributes继承
透传可以通过设置inheritAttrs: false来禁用。
<script setup>
defineOptions({
inheritAttrs: false
})
// ...setup 逻辑
</script>禁用之后透传到组件的属性,不会应用到根组件上,如果需要应用到其他元素上,可以使用$attrs来访问。
<span>Fallthrough attribute: {{ $attrs }}</span>$attrs是一个字典,通过key可以访问指定的指,使用时名称需要保持与透传时设置的属性名一致,如:
$attrs['foo-bar']
$attrs.onClick多根节点的Attributes继承
自动透传行为只会发生在单根节点的组件上,在多个根节点的组件没有自动 attribute 透传行,因为Vue不知道需要透传到哪个节点上,此时会抛出一个运行时警告,如果使用$attrs显示绑定后,则不会有警告
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>在javascript中访问透传 Attributes
使用useAttrs()方法,获取透传到组件的Attributes,如下:
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>需要注意的时,attrs并不是响应式的。如需要响应式数据,应该使用props。
Vue组件的依赖注入
依赖注入的基础用法
Vue的依赖注入主要解决Prop多层传递的问题,以简洁的方式实现父组件向深层次子组件传递数据。
依赖注入功能主要依赖于provide()提供方法和inject()方法。
在父组件中使用provide()方法定义透传的Prop。provide基础用法是接受两个参数,第一个参数注入参数的key,可以是字符串或Symbol标识符。第二个参数是注入参数的具体值,可以是任意类型,包括响应式的状态。
<script setup>
import { provide } from 'vue'
provide('message', 'hello!')
</script>在子组件中使用inject()方法获取透传的Props,提供获取Prop的key。
<script setup>
//基础用法
const value = inject('message');
//提供默认值(当父组件没提供时,则返回默认值)
const value=inject('message','hello world');
//使用工厂函数,在需要用到默认值时才计算默认值。此时第三个true必填
const value=inject('message',()=>new ExpensiveClass(),true);
</script>依赖注入与响应式数据的结合
依赖注入与响应式数据结合使用时,应遵循供给方组件维护响应式数据的原则,这样可以避免数据多方维护从而造成逻辑混乱。如需在接受方组件中更改响应式数据的值,可以在供给方中定义修改响应式数据的方法,并提供给接受方。
//Parent.vue
<script setup>
import {ref,provide} from 'vue';
const message=ref("hello world");
function updateMessage(){
message.value='newMessage';
}
provide('message',{message,updateMessage});
<script>
//Child.Vue
<script setup>
import {ref,inject} from 'vue';
const {message,updateMessage}=inject('message');
<script>
<template>
<button @click='updateMessage'>{{message}}</button>
</template>使用Symbol修饰符作为注入名
什么是Symbpl?
在JavaScript中,Symbol是一种基本数据类型,其每一个实例都是唯一且不可变的。它主要用于创建独一无二的标识符,这些标识符不会与其他变量名或属性名发生冲突。
每次调用Symbol()都会创建一个新的、唯一的Symbol值,即使它们的描述(description)相同。这意味着即使两个Symbol具有相同的描述,它们也是不同的。
const symbol1=Symbol('symbol');
const symbol2=Symbol('symbol');
console.log(symbol1===symbol2);//返回false,引用不一样。Symbol修饰符作为注入名有什么好处?
虽然使用字符串作为注入名很方便,但是在构建大型应用时,因组件数量庞大,字符串作为注入名反而会导致混乱。而Symbol因为其可确保唯一性,更有利于在大型项目中指定注入名。
要使用Symbol修饰符作为注入名,需要将在一个js模块中使用Symbol修饰符定义注入名并导出。在接受方组件使用时,导入注入名js模块,这样可以确保供给方与接收方的注入名一致,且不会于其他模块混淆。
//key.js
export const MySymbol = Symbol('MySymbol');
//Parent.vue
<script setup>
import {provide} from 'vue';
import {MySymbol} from './key.js'
provide(MySymbol,'hello world');
</script>
//Child.vue
<script setup>
import {inject} from 'vue';
import {MySymbol} from './key.js';
const msg=inject(MySymbol);
</script>异步组件
异步组件可以在我们使用组件时再去异步加载组件,从而优化应用的性能。
在Vue 3中,可以使用defineAsyncComponent异步函数来定义异步组件:
//组件内注册
<script setup>
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
)
</script>
//全局注册
app.component('AsyncComponent', defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
))或使用导入的方式注册组件
Vue.component('AsyncComponent', () => import('./components/AsyncComponent.vue'));
export default {
components: {
AsyncComponent: () => import('./components/AsyncComponent.vue')
}
};注册后,就可以将普通组件一样在模板中使用。
<template>
<AsyncComponent />
</template>defineAsyncComponent是Vue 3引入的一个高级API,用于定义异步组件。它提供了更多的选项和控制,可以处理加载状态、错误状态、重试机制等。
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})组合式函数
vue的组合式函数是指利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
组合式函数除了可以复用逻辑,还可接收响应式状态,达到响应式数据变化时,自动执行函数。其依赖于toValue()方法和watchEffect()方法。
// fetch.js
import { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = () => {
// reset state before fetching..
data.value = null
error.value = null
fetch(toValue(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
watchEffect(() => {
fetchData()
})
return { data, error }
}一般我们将响应式函数以use为开头命名。
自定义指令
除了Vue自带的v-if、v-model等指令外,Vue还提供了自定义指令的方式。自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
在 <script setup> 中,任何以v开头的驼峰式命名的变量都可以被用作一个自定义指令,通过以下方式定义:
<script setup>
// 在模板中启用 v-focus
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>或在vue应用内进行全局注册,通过以下方式:
const app = createApp({})
// 使 v-focus 在所有组件中都可用
app.directive('focus', {
/* ... */
})自定义指令的原理是通过组件生命周期钩子函数的形式影响DOM元素状态。自定义指令实际也是由多个可选的生命周期钩子函数组成:
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode) {}
}钩子函数参说说明:
- el:表示绑定到的元素,可以直接用于操作DOM元素。
- binding:是一个对象,里面包含了各种属性。包括:
- value:传递给指令的值。如:
v-color='123',value=123。 - oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
- arg:传递给指定的参数。如
v-color:background='red',arg=background。 - modifiers:一个包含了修饰符的对象。如
v-color.gradient.fluorescence='red',modifiers={gradient:true,fluorescence:true} - instance:使用指令的组件实例。
- dir:指令定义的对象
- value:传递给指令的值。如:
- vnode:代表绑定元素的底层 VNode。
- prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。 自定义指令提供了简写形式,在不指定钩子函数只提供指令函数实现的情况下,默认会实现mounted和updated方法。即:
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
<div v-color="color"></div>插件
插件 (Plugins) 是一种能为 Vue 添加全局功能的工具代码。
通过使用vue应用的use()方法全局安装插件。
import { createApp } from 'vue'
const app=creatApp({});
app.use(myPlugin,{
/*可选选项*/
})一个插件可以是一个拥有install()方法的对象,或者是一个安装函数本身。安装函数会接收到安装它的应用实例和传递给app.use()的额外选项作为参数:
const myPlugin={
install:(app,options)=>{
//向app中配置插件
}
}
myPlugin:(app,options)=>{
}插件功能没有具体定义,一般会在插件中实现以下功能:
- 通过
app.component()全局注册组件。 - 通过
app.directive()全局注册自定义指令。 - 通过
app.provide()使一个资源可被注入进整个应用。 - 向
app.config.globalProperties中添加全局实例属性或方法。 - 组合以上各种方法,实现更强大的功能,如vue-router路由插件。
一般情况下,应该谨慎使用全局属性,如果在整个应用中使用不同插件注入的太多全局属性,很容易让应用变得难以理解和维护。通过插件定义全局属性或方法时,推荐通过provide()、inject()依赖注入的方式定义。
在Vue组件setup部分获取全局属性
在 Vue 3 的 <script setup> 语法中,setup 函数在组件实例创建之前被调用,因此没有 this 上下文来引用 Vue 实例,因此无法直接访问 app.config.globalProperties 上定义的全局属性,需要使用getCurrentInstance()方法获取当前组件实例proxy对象。
<script setup>
import { getCurrentInstance } from 'vue';
const {proxy} = getCurrentInstance();
</script>再通过proxy获取全局属性或全局方法。
需要注意的是,Vue3的模板中,依然可以直接访问全局属性。
Vue内置组件
Vue提供了5个内置组件,内置组件意味着它在任意别的组件中都可以被使用,无需注册。
内置组件包括:
- Transition:用于给元素或组件在进入或离开DOM时应用动画。
- TransitionGroup:用于对使用
v-for渲染的列表中的元素的插入、删除、移动添加动画效果。 - KeepAlive
- Teleport
- Suspense
Transition组件
<Transition>用于在一个组件或元素进入和离开DOM时应用动画,它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。
<Transition name="fade">
<p v-if="show">这是一段被定义了进出过渡动画的文字<p/>
</Transition>进入和离开由以下条件之一触发:
v-if切换显示、隐藏状态v-show切换显示、隐藏状态<Component>动态切换组件时- 改变特殊的
key属性时
<Transition>定义了6个可应用进入和离开动画的CSS类,包括:
- v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
- v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
- v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
- `v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
- v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
- v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

- 使用transition定义过渡动画时,
v-enter-from定义进入的起始状态,v-leave-to定义离开的结束状态,v-enter-active定义进入的过渡效果,v-leave-active定义离开的过渡效果。
//进入时的过渡效果
.fade-enter-active,
.fade-leave-active {
//应用过渡效果的属性,持续事件,变化曲线
transition: opacity 0.5s ease;
}
//进入时的起始状态与离开的结束状态
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}- 使用animation定义过渡动画时,
v-enter-active定义进入的过渡动画,v-leave-active定义离开的过渡动画。
.bounce-enter-active {
animation: bounce-in 0.5s;
}
.bounce-leave-active {
animation: bounce-in 0.5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.25);
}
100% {
transform: scale(1);
}
}除了在CSS上定义<Transition>组件的进出动画,还可以通过组件props的方式定义动画样式。<Transition>提供了以下props来指定自定义的过渡 class。
- enter-from-class
- enter-active-class
- enter-to-class
- leave-from-class
- leave-active-class
- leave-to-class 这在引用第三方CSS动画库时非常有用。
<!-- 假设你已经在页面中引入了 Animate.css -->
<Transition
name="custom-classes"
enter-active-class="animate__animated animate__tada"
leave-active-class="animate__animated animate__bounceOutRight"
>
<p v-if="show">hello</p>
</Transition>此外<Transition>组件还提供了组件事件,利用组件事件我们实现js与css结合完成过渡动画,或者完全交给js完成过渡动画。组件事件如下:
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<!-- ... -->
</Transition>// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}在使用仅由 JavaScript 执行的动画时,最好是添加一个
:css="false"prop。这显式地向 Vue 表明可以跳过对 CSS 过渡的自动探测。除了性能稍好一些之外,还可以防止 CSS 规则意外地干扰过渡效果
<Transition>还提供了很多属性以实现不同的特性:
- appear:仅在某个节点初次渲染时应用一个过渡效果。
- mode:定义组件间切换的过渡模式。可以设置为
out-in:等上一个元素离开后,再开始下一个元素的进入动画。in-out:等下一个元素进入后,再开始上一个元素的离开动画。这个属性很重要,适当的设置可以避免文档流的位置发生调整。
TransitionGroup组件
<TransitionGroup> 支持和 <Transition>相同的props、css过渡动画class、js监听事件,其不同点是:
- TransitionGroup组件不渲染列表容器,因此需要通过
tag属性指定容器标签。 - 过渡模式mode属性,在TransitionGroup中不可用。
- 每个列表元素都必须设置key属性,用于区分和实现切换动画。
- CSS动画样式应该在列表元素上而不是容器元素上。
- 增加了
v-move样式类,用于定义列表元素移动时的动画效果。
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</TransitionGroup>
<template>
<style scoped>
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 确保将离开的元素从布局流中删除
以便能够正确地计算移动的动画。 */
.list-leave-active {
position: absolute;
}
</style>KeepAlive组件
<KeepAlive>是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例,也是通过插槽的形式,实现组件的状态缓存。
<KeepAlive>默认会缓存组件内部所有的组件实例,可以通过include、exclude属性包含、排除需要缓存的组件。其值为组件名称,可以是逗号分隔的字符串、正则表达式、字符串数组。
在 3.2.34 或以上的版本中,使用
<script setup>的单文件组件会自动根据文件名生成对应的name选项,无需再手动声明。
<KeepAlive :include="['CountComponent']">
<Component :is="page" style="position: absolute;" />
</KeepAlive>也可以通过max属性来限制缓存组件的最大数量。添加max属性后,将会使用类似LRU算法,当缓存组件数量超过max时,将会销毁最久没被访问的组件,以腾出空间。
生命周期钩子函数onActivated和onDeactivated也会在缓存组件被激活或卸载时触发。
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>Teleport
<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。
<Teleport>可以用在模态框的实现上,将与组件关联的模态框传送到指定标签下。避免DOM结构太深,导致CSS样式混乱。
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport><Teleport>的to属性,用于指定其内部的模板传送到哪个标签下,to的值可以是一个CSS选择器字符串,也可以是一个DOM元素对象。
<Teleport>的disabled属性,用于禁止传送行为,可以利用该属性实现动态传送。
Suspense
<Suspense> 是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
这里的异步依赖项包括:异步组件、或setup函数内存在异步操作的组件。
<Suspense>提供了两个插槽,fallback用来表示加载中和default用来表示加载完成的。
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<Dashboard />
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>异步组件也可以通过在选项中指定
suspensible: false表明不用 Suspense控制,并让组件始终自己控制其加载状态。
<Suspense>组件会触发3个事件:
- pending:事件是在进入挂起状态时触发。
- resolve:事件是在 default 插槽完成获取新内容时触发。
- fallback:事件则是在 fallback 插槽的内容显示时触发。
和其他组件一起使用时,需要注意嵌套顺序,如下为正确示例:
<!-- 路由 -->
<RouterView v-slot="{ Component }">
<template v-if="Component">
<!-- 进出动画 -->
<Transition mode="out-in">
<!-- 组件状态缓存 -->
<KeepAlive>
<!-- 异步依赖项加载页面优化 -->
<Suspense>
<!-- 主要内容 -->
<component :is="Component"></component>
<!-- 加载中状态 -->
<template #fallback>
正在加载...
</template>
</Suspense>
</KeepAlive>
</Transition>
</template>
</RouterView>路由Vue Router
状态管理Pinia
测试
测试方式
单元测试
函数的单元测试
测试框架:
- Jest
组件的单元测试
测试方式:
- 白盒:单元测试
- 黑盒:组件测试
测试框架:
- Vitest
- Jest
组件测试
测试这个组件做了什么,而不是测试它是怎么做到的。
- 对于 视图 的测试:根据输入 prop 和插槽断言渲染输出是否正确。
- 对于 交互 的测试:断言渲染的更新是否正确或触发的事件是否正确地响应了用户输入事件。
测试框架:
- @vue/test-utils:是官方的底层组件测试库,用来提供给用户访问 Vue 特有的 API。
- @testing-library/vue:基于@vue/test-utils构建的。是一个专注于测试组件而不依赖于实现细节的 Vue 测试库。
- Cypress 组件测试
- Nightwatch:是一个端到端测试运行器,支持 Vue 的组件测试。
- WebdriverIO:用于跨浏览器组件测试,该测试依赖于基于标准自动化的原生用户交互
端到端测试
模拟用户的真实操作,测试用户使用你的应用会发生的事情,测试应用发送的请求。
测试框架:
- Cypress
- Playwright
- Nightwatch
- WebdriverIO
用例指南
在项目根目录下创建test文件夹用户存放测试文件,以*.test.js为文件名存放测试代码。
Vue的服务端渲染(SSR)
- Nuxt
- Quasar
- Vite SSR
生产部署
- 推荐使用构建工具对应用进行打包编译。
- 使用 应用级错误处理 可以用来向追踪服务报告错误,或集成 Sentry 和 Bugsnag等服务。
import { createApp } from 'vue'
const app = createApp(...)
app.config.errorHandler = (err, instance, info) => {
// 向追踪服务报告错误
}使用Vue的多种方式
- 独立脚本的方式引入Vue,将Vue挂载到指定的元素上,实现Vue对该元素的控制。
- 使用Vue创建WebComponent,将WebComponent插入到任何HTML中。
- 单页应用SPA
Vue渲染机制

Vue Router
Vue Router 是 Vue.js 的官方路由。它与 Vue.js 核心深度集成,让用 Vue.js 构建单页应用变得轻而易举。
引入Vue Router
- 使用npm安装vue router
npm install vue-router@4- 创建router.js,配置router,导出router对象。
import { createWebHistory, createRouter } from "vue-router";
import HelloWorld from "@/components/HelloWorld.vue";
import HomeComponent from "@/components/test/keepalive-component/HomeComponent.vue";
const routes = [
{
path: '/',
component: HelloWorld
},
{
path: '/home',
component: HomeComponent
},
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;- 在mian.js中,给vue app安装router。
import './assets/main.css'
import { createApp } from 'vue'
import router from "./router"
import App from './App.vue'
const app = createApp(App);
app.use(router)
app.mount('#app');插件安装过程中完成了以下内容:
- 全局注册RouterView和RouterLink组件。
- 添加全局
$router和$route属性。 - 启用
useRouter()和useRoute()组合式函数。 - 触发路由器解析初始路由。
Vue Router内置组件
Vue Router为我们提供了两个内置组件,<RouterLink>和<RouterView>。
- RouterLink:用于创建链接。这使得 Vue Router能够在不重新加载页面的情况下改变URL,处理URL的生成、编码和其他功能。
- RouterView:使Vue Router知道你想要在哪里渲染当前URL路径对应的路由组件
路由匹配
带命名参数的动态路由匹配
使用:表示命名路径参数,在路由匹配时会自动匹配该路由,并获取路由参数。
{
path: '/routerParam/:id/:name',
component: RouterParam
}在模板中,使用$route.param获取路由参数。
<template>
<div>
<p>route.fullPath:{{ $route.fullPath }}</p>
<p>route.params{{ $route.params }}</p>
</div>
</template>带正则表达式的路由匹配
在命名参数后面添加(),括号内的内容为正则表达式,表示要匹配的内容。匹配到的字符串将放入命名参数内,可通过$route.param.命名参数获取。
const routes = [
// 将匹配所有内容并将其放在 `route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `route.params.afterUser` 下
{ path: '/user-:afterUser(.*)', component: UserGeneric },
]路由匹配选项
默认情况下,所有路由是不区分大小写的,并且能匹配带有或不带有尾部斜线的路由。
Vue提供了两个属性,用于控制以上规则:
- sensitive:用于控制路由匹配是否对大小写敏感。是一个布尔值。当为true时,大小写敏感。当为false时,大小写不敏感。
- strict:用于控制路由匹配是否严格匹配尾部的斜杠。是一个布尔值。当为true时,严格要求斜杠,
/about和/about/被认为是不同的路径。当为false时,不严格要求斜杠。
嵌套路由
在路由中配置children,可以实现嵌套路由,children属性是一个路由对象数组。
const routes = [
{
path: '/parent',
component: ParentComponent,
children: [
//当没有匹配到嵌套路由,在嵌套的<router-view>中会渲染这个空路由指向的组件
{ path: '', component: UserHome },
{
path: 'child', // 这里的路径是相对于父级路径 '/parent' 的,即完整路径为 '/parent/child'
component: ChildComponent
},
{
path: '/independent', // 这里的路径是根路径,即完整路径为 '/independent'
component: IndependentComponent
}
]
}
]注意,以
/开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的URL。
命名路由
路由有一个name属性,可以通过该属性指定路由名称。然后我们可以使用name属性而不是path属性来传递给<router-link>的to属性。
{
name: 'routerParam',
path: '/routerParam/:id/:name',
component: RouterParam
}<RouterLink :to="{ name: 'routerParam', params: { id: '123', name: 'chenzw' } }">
/routerParam
</RouterLink>使用 name 有很多优点:
- 没有硬编码的 URL。
- params 的自动编码/解码。
- 防止你在 URL 中出现打字错误。
- 绕过路径排序,例如展示一个匹配相同路径但排序较低的路由。
所有路由的命名都必须是唯一的。如果为多条路由添加相同的命名,路由器只会保留最后那一条。
编程式导航
vue提供了编程式导航的api:
router.push:相当于<router-link :to="...">,实现路由跳转。该方法会向history 栈添加一个新的记录。该方法可以接收一个参数,或一个描述地址的对象。
// 字符串路径
router.push('/users/eduardo')
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })如果提供了path,params会被忽略,query则不会。
router.replace:实现路由跳转,但不会向history栈添加一个新的记录,而是覆盖当前的记录。router.forward:在历史堆栈中前进一步。router.back:在历史堆栈中后退一步。router.go:接受一个整数n为参数。正数时表示前进n步,负数时表示后退n步。
命名视图
命名视图是指给RouterView组件添加上name属性。命名视图在同级路由需要显示多个视图时很有用。在定义路由时,使用components属性,定义多个视图组件。
声明命名视图
<router-view class="view left-sidebar" name="LeftSidebar" />
<router-view class="view main-content" />
<router-view class="view right-sidebar" name="RightSidebar" />定义命名视图路由
routes: [
{
path: '/',
components: {
default: Home,
// LeftSidebar: LeftSidebar 的缩写
LeftSidebar,
// 它们与 `<router-view>` 上的 `name` 属性匹配
RightSidebar,
},
},
]路由重定向
通过redirect属性定义重定向路由。该属性可以接收一个url字符串,一个命名路由,或是一个方法(返回一个重定向目标)。
{
path: '/redirect',
redirect: '/',
},
//或
{
path: '/redirect',
redirect: { name: 'routerParam', params: { id: '123', name: 'chenzw' } },
},
//或
{
path: '/redirect',
redirect: (to)=>{
return {path:'/search',query:{q:to.params.searchText}}
},
},当redirect的path不加/时,表示相对重定向。
const routes = [
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
// 相对位置不以`/`开头
// 或 { path: 'profile'}
return 'profile'
},
},
]路由别名
通过alias属性可以设置路由别名,比如给/home设置别名/,当访问/时,浏览器URL依然显示/,但路由会被匹配为用户访问/home。
{
path: '/',
component: HelloWorld,
alias: '/home'
},路由组件传参
给路由配置props:true时,路由参数将会作为prop传递给组件。这种传值的方式可以避免组件与路由的绑定,在组件中无需使用$route或useRoute()即可获取路由参数。
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]如上例子,id将会作为props传递给组件User,不作为props传递给Sidebar。
路由激活样式
使用RouterLink组件创建导航链接。默认情况下,当链接的目标与当前路径匹配时,RouterLink会自动添加router-link-active和router-link-exact-active类。通过类可以设置激活时的样式。
- router-link-active:当目标路径是当前路径的子路径时,链接会被认为是活动的。
- router-link-exact-active:只有当目标路径完全匹配当前路径时,链接才会被认为是精确活动的。
我们也可以通过RouterLink组件的activeClass和exactActiveClass属性,单独对某个routerLink自定义样式类。
<RouterLink to="/home" activeClass="my-active-class" exactActiveClass="my-exact-active-class">Home</RouterLink>或在创建路由对象时,进行全局配置。
const router = new VueRouter({
routes: [...],
linkActiveClass: 'my-active-class',
linkExactActiveClass: 'my-exact-active-class'
})历史模式
在创建路由时,history配置允许我们在不同历史模式中进行选择。
- Hash模式:URL使用
#作为层级区分。 - Memory模式:Memory 模式不会假定自己处于浏览器环境,因此不会与URL交互也不会自动触发初始导航。也即时浏览器的地址栏不会发生变化,前进、后退无效。
- History模式:正常的URL显示。推荐使用这种模式。 在
createWebHistory中设置base配置,base配置项用于设置应用的基础路径,这个基础路径会被添加到所有路由的前面。例如,如果你的应用部署在http://example.com/myapp/下,你就需要将base设置为/myapp/。
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory('/base-directory/'),
routes: [],
})导航守卫
导航是指应用要进行URL跳转动作了。守卫是指安插在进行导航过程中的 导航守卫主要用来通过跳转或取消的方式守卫导航。
导航过程中的关键阶段
- 导航触发:
- 当用户点击链接、调用
router.push等方式改变 URL 时,导航被触发。
- 组件离开守卫(beforeRouteLeave)执行
- 在失活的组件里调用
- 全局前置守卫 (beforeEach) 执行:
- 这是导航过程的早期阶段,所有的全局前置守卫在此时执行。
- 适用于进行一些全局性的检查和处理,比如权限验证、全局数据准备等。
- 可复用组件的路由更新守卫(beforeRouteUpdate)执行
- 路由独享守卫(beforeEnter)执行:
- 只在进入路由时触发,不会在params、query、hash改变时触发。
- 路由配置中解析异步组件:
- 如果目标路由包含异步组件,在这一步将异步加载这些组件。
- 在异步组件解析完成之前,不会进行下一步的守卫检查。
- 组件内守卫(beforeRouteEnter)执行:
- 组件解析完毕后,在目标路由对应的组件中,执行定义的组件内导航守卫。
- 全局解析守卫 (beforeResolve) 执行:
- 在所有组件内守卫和异步组件解析完成之后,导航被确认之前,执行全局解析守卫。
- 这是在导航被确认前最后一个可以插入逻辑的地方,适合做一些最终的检查和处理。
- 导航确认:
- 通过以上所有守卫和检查之后,导航确认,开始改变 URL 并挂载目标路由对应的组件。
- 全局后置守卫 (afterEach) 执行:
- 导航确认并且组件挂载完成之后,执行全局后置守卫。
- 适用于进行一些导航结束后的处理,比如页面统计、日志记录等。
- 触发DOM更新
- 触发beforeRouteEnter的next回调
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
全局前置守卫beforeEach
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。
//to: 即将要进入的目标
//from: 当前导航正要离开的路由
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})可以返回的值:
- false:取消当前导航,并重置到from对应的地址。
- true或undefined:导航有效,进入下一个导航守卫。
- 返回一个新的路由地址或对象,将会重定向到新地址,如同调用
router.push()。此时会中断原来的导航,并用相同的from,不同的to,创建一个新导航。
全局解析守卫beforeResolve
解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置。
//to: 即将要进入的目标
//from: 当前导航正要离开的路由
router.beforeResolve((to, from) => {
// ...
// 返回 false 以取消导航
return false
})全局后置钩子
全局后置守卫发生在导航确认并且组件挂载完成之后,它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from,failure) => {
sendToAnalytics(to.fullPath)
})路由独享的守卫
在route内使用beforeEnter配置路由独享守卫。
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]beforeEnter可以是一个函数,也可以是一个函数数组。
组件内的守卫
- beforeRouteEnter:在渲染该组件的对应路由被验证前调用。不能获取组件实例
this,因为当守卫执行时,组件实例还没被创建。进入组件后会调用next回调方法。 - beforeRouteUpdate:在当前路由改变,但是该组件被复用时调用。在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例
this。 - beforeRouteLeave:在导航离开渲染该组件的对应路由时调用。可以访问组件实例
this。
onBeforeRouteEnter((to,from,next)=>{
console.log(to);
console.from(from);
});
onBeforeRouteUpdate((to,from)=>{
console.log(to);
console.from(from);
console.log(this);
});
onBeforeRouteLeave((to,from)=>{
console.log(to);
console.from(from);
console.log(this);
});路由元信息
有时我们希望给任意路由添加一些附加信息,这时就可以使用路由meta属性,为路由添加元信息。
const routes = [
{
path: '/posts',
component: PostsLayout,
meta:{ requiresAuth: true }
}
]路由元信息,可以使用route.mate获取路由元信息访问。比如在导航守卫中,可以这样获取:
router.beforeEach((to,from)=>{
console.log(to.meta.requireAuth);
});路由属性总结
routes路由提供了多个属性,用于实现不同的路由功能。
- path:url路由匹配模式。
- component:路由匹配成功后渲染的组件。
- sensitive:url大小写敏感。
- strict:严格匹配url尾部斜杠。
- name:命名路由。
- components:命名视图路由。为一个命名视图与组件映射的对象。
- children:嵌套路由。为一个路由数组。
- redirect:重定向路由。
- alisa:定义路由别名,类似于重定向,但URL地址不改变。
- props:布尔值。当配置为true时,路由参数将作为prop传递给组件。
- beforeEnter:定义路由独享的守卫
- meta:定义路由元信息
在组件中使用router和route
- 在选项式Api中,使用
this.$router和this.$route访问。 - 在模板中,使用
$router和$route访问。 - 在组合式Api中,使用
useRouter()和useRoute()访问。
RouterView插槽
RouterView组件内部定义了一个作用域插槽,插槽提供了一个component属性,表示当前匹配路由所要渲染的组件引用。
通过作用域插槽,可以在父组件中获取到当前渲染的组件,给RouterView增加其他功能。比如增加缓存和过渡动画:
<router-view v-slot="{ Component,route }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>或者是利用插槽给组件传递属性或给组件再添加插槽:
<router-view v-slot="{ Component }">
//传递属性
<component :is="Component" some-prop="a value">
//添加插槽
<p>Some slotted content</p>
</component>
</router-view>滚动行为
当页面路由切换到新页面时,可能希望能滚动到新页面的指定位置。这种场景可以使用scrollBehavior方法实现。这在createRouter中定义。
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// return 期望滚动到哪个的位置
return {
//el是可选参数,top、left是相对偏移量
el:'#main',
top:10,
left:10
}
},
})路由懒加载
使用动态加载,避免打包构建应用后,js文件过大。
// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')
const router = createRouter({
// ...
routes: [
{ path: '/users/:id', component: UserDetails }
// 或在路由定义里直接使用它
{ path: '/users/:id', component: () => import('./views/UserDetails.vue') },
],
})导航故障
导航故障有三种类型:
- aborted:在导航守卫中返回 false 中断了本次导航。
- cancelled: 在当前导航完成之前又有了一个新的导航。比如,在等待导航守卫的过程中又调用了 router.push。
- duplicated:导航被阻止,因为我们已经在目标位置了。
使用isNavigationFailure和NavigationFailureType来判断失败类型:
import { NavigationFailureType, isNavigationFailure } from 'vue-router'
// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
// 给用户显示一个小通知
showToast('You have unsaved changes, discard and leave anyway?');
console.log(failure.to);
console.log(failure.from);
}在导航守卫中返回一个新路由时,不会中断原来的路由,而是创建一个新导航。检测重定向的方法如下:
await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
// redirectedFrom 是解析出的路由地址,就像导航守卫中的 to和 from
}动态路由
动态路由主要通过两个函数实现。
router.addRoute()router.removeRoute()。 它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,且需要显示新页面就需要用router.push()或router.replace()来手动导航,才能显示该新路由。
添加路由的方法:
router.addRoute({ path: '/about', component: About })添加嵌套路由,第一个参数传递父路由的name,第二个参数传递具体要添加的路由:
router.addRoute('admin', { path: 'settings', component: AdminSettings })删除路由的方法:
- 通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由。
- 通过调用
router.addRoute()返回的回调。
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话- 通过使用
router.removeRoute()按名称删除路由
router.addRoute({ path: '/about', name: 'about', component: About })
// 删除路由
router.removeRoute('about')判断路由是否存在的方法
router.hasRoute(routeName):判断路由器中是否存在某个特定的路由,返回一个布尔值,true 表示存在该路由,false 表示不存在。router.getRoutes():获取路由器中所有路由的列表。
Pinia状态管理
引用Pinia
- 通过npm安装Pinia
npm install pinia- 在main.js中给Vue应用安装Pinia。
import { createApp } from 'vue';
import { createPinia } from "pinia";
import App from './App.vue'
const pinia = createPinia();
const app = createApp(App);
app.use(pinia)
app.mount('#app');Store是什么?
Store (如 Pinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定,它承载着全局状态,它有三个概念:state、getter和action。
- State: 存储应用的全局数据,类似于组件中的data。
- Getter: 计算或派生数据,类似于组件中的computed。
- Action: 包含业务逻辑,处理数据的变化或异步操作,类似于组件中的methods。
定义一个Store
Pinia中使用defineStore函数定义一个Store,其返回值应使用useXXXStore的形式命名。
import { defineStore } from 'pinia'
export const useAlertsStore = defineStore('alerts', {
// 其他配置...
})该函数第一个参数是这个store的唯一id。
第二个参数是store的具体信息,可接收两类值:Setup函数或Option对象
Option Store(选项式Store)
在Option Store中,我们可以传入一个带有state、getters、actions的Option对象。
//count.js
import { defineStore } from "pinia";
export const useCountStore = defineStore("count", {
state: () => {
return {
count: 0,
};
},
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++;
},
},
});Setup Store(组合式Store)
在Setup Store中,我们传入一个匿名函数,函数中使用ref等响应式函数定义date,使用computed定义getter,使用命名函数定义action,并且返回一个带有我们想暴露出去的属性和方法的对象。
import { defineStore } from "pinia";
import { computed } from "vue";
export const useCountSetupStore = defineStore("countSetup", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
return { count, doubleCount, increment };
});需要注意的是,在Setup Store中,必须暴露返回state的所有属性,不能在store中定义私用属性。如果需要使用私有属性,建议使用其他方式实现,比如组件内变量。
使用Store
使用Store时,只需要导入定义Store的js文件即可。store 是一个用reactive包装的对象,这意味着不需要在getters后面写.value,也不能对其state进行解构。
<script setup>
import { useCountSetupStore } from "@/stores/countSetup";
const countSetupStore = useCountSetupStore();
</script>
<template>
<div>
<p>数字为:{{ countSetupStore.count }}</p>
<p>数字为:{{ countSetupStore.doubleCount }}</p>
<button @click="countSetupStore.increment()">点我</button>
</div>
</template>
<style scoped></style>从Store中解构
要想对Store进行解构,需要借助storeToRefs函数,它可以将state和Getter属性提取为响应式ref,而action可以直接解构。
<script setup>
import { useCountSetupStore } from "@/stores/countSetup";
import { storeToRefs } from "pinia";
const countSetupStore = useCountSetupStore();
//解构
const { count, doubleCount } = storeToRefs(countSetupStore); storeToRefs(countSetupStore);
const { increment } = countSetupStore;
</script>
<template>
<div>
<div>
<p>数字为:{{ countSetupStore.count }}</p>
<p>数字为:{{ countSetupStore.doubleCount }}</p>
<button @click="countSetupStore.increment()">点我</button>
</div>
<div>
<p>数字为:{{ count }}</p>
<p>数字为:{{ doubleCount }}</p>
<button @click="increment()">点我</button>
</div>
</div>
</template>
<style scoped></style>State
访问State
在Store定义State时,必须设置初始值。如果没有设置初始值,则不能访问。State可以通过store直接访问或修改。
const store=useStore();
console.log(store.count);
store.count++;重置State
store提供了$reset函数去重置state的状态,在其内部会调用state()函数创建一个新的状态对象,并用它替换当前的状态。
import { defineStore } from "pinia";
import { computed, ref } from "vue";
export const useCountSetupStore = defineStore("countSetup", () => {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);
function increment() {
count.value++;
}
function $reset() {
count.value = 0;
}
return { count, doubleCount, increment, $reset };
});<script setup>
import { useCountSetupStore } from "@/stores/countSetup";
import { storeToRefs } from "pinia";
const countSetupStore = useCountSetupStore();
const { count, doubleCount } = storeToRefs(countSetupStore);
const { increment, $reset } = countSetupStore;
</script>
<template>
<div>
<div>
<p>数字为:{{ countSetupStore.count }}</p>
<p>数字为:{{ countSetupStore.doubleCount }}</p>
<button @click="countSetupStore.increment()">点我</button>
</div>
<div>
<p>数字为:{{ count }}</p>
<p>数字为:{{ doubleCount }}</p>
<button @click="increment()">点我</button>
</div>
<div><button @click="$reset()">重置</button></div>
</div>
</template>
<style scoped></style>需要注意的是,在Setup Stores中,需要自己创建
$reset函数,并且导出。
变更state
除了通过直接访问,直接修改的方式变更state。还可以通过$patch方式修改。
$patch接受一个state对象,表示变更后的状态。
store.$patch({
count:store.count+1,
age:10
});或是接受一个匿名函数,处理state。
store.$patch((state)=>{
state.count++;
state.items.push({age:10});
});订阅state
store提供了$subscribe()用于监听state的变化。相比于watch(),$subscribe()可以在$patch后只触发一次。
// 订阅状态变化
store.$subscribe((mutation, state) => {
console.log(mutation); // 输出变更信息
console.log(state); // 输出当前状态
});其中,mutation对象:
- type: 变更的类型(通常是direct表示直接更改,或者patch object表示通过
$patch方法更新)。 - storeId: 触发变更的 store 的 ID。和
store.$id一样。 - events: 变更事件的细节。
- payload:只有
mutation.type === 'patch object'的情况下才可用,传递给$patch()的补丁对象。 默认情况下,定义在组件中的subscribe,在组件被卸载时,会被自动删除。如果仍需保留,请将{ detached: true }作为第二个参数。
Getter
Getter相当于state的计算值。在OptionStore中通过defineStore的getters属性定义,推荐使用箭头函数定义。
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
})Getters可以通过返回一个函数的形式,来向getter传递参数。
export const useUserListStore = defineStore('userList', {
getters: {
getUserById: (state) => {
return (userId) => state.users.find((user) => user.id === userId)
},
},
})Action
action相当于组件中的method,可以定义任意方法。
action还支持异步处理,可以使用asyn、await。
action还支持订阅,可以通过store.$onAction()来监听action和它们的结果。该函数接受一个函数,表示在action之前执行的动作。
- 传递给action的函数会在acton之前执行,里面可以定义一些钩子函数。
- after:定义action返回后的钩子函数
- onError:定义action抛出异常或拒绝时的钩子函数
const unsubscribe = someStore.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
after, // 在 action 返回或解决后的钩子
onError, // action 抛出或拒绝的钩子
}) => {
// 为这个特定的 action 调用提供一个共享变量
const startTime = Date.now()
// 这将在执行 "store "的 action 之前触发。
console.log(`Start "${name}" with params [${args.join(', ')}].`)
// 这将在 action 成功并完全运行后触发。
// 它等待着任何返回的 promise
after((result) => {
console.log(
`Finished "${name}" after ${
Date.now() - startTime
}ms.\nResult: ${result}.`
)
})
// 如果 action 抛出或返回一个拒绝的 promise,这将触发
onError((error) => {
console.warn(
`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
)
})
}
)
// 手动删除监听器
unsubscribe()Pinia插件
插件可以为Pinia store拓展新功能。在Pinia中使用use方法安装插件,插件是一个返回对象的函数,会为每个store添加或覆盖属性,其接受一个可选的context参数。
function SecretPiniaPlugin() {
return { secret: 'the cake is a lie' }
}
const pinia = createPinia()
// 将该插件交给 Pinia
pinia.use(SecretPiniaPlugin)
pinia.use((context)=>{
context.pinia // 用 `createPinia()` 创建的 pinia。
context.app // 用 `createApp()` 创建的当前应用(仅 Vue 3)。
context.store // 该插件想扩展的 store
context.options // 定义传给 `defineStore()` 的 store 的可选对象。
// ...
})
pinia.use(({ store }) => {
store.hello = 'world'
})