1.开始
1.1简介
1.1.1vue介绍
vue是什么:用于构建用户界面的js框架。渐进式框架:功能可以逐步集成
核心功能:
声明式渲染:声明式描述html和js状态之间的关系
响应式:跟踪js状态变化,更新dom
vue标志性功能:单文件组件
API风格:选项式API和组合式API
1.2快速上手
1.2.1创建一个应用
npm create vue@latest
1.2.2通过 CDN 使用 Vue
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
// (1)全局构建版本 - 所有顶层 API 都以属性的形式暴露在了全局的 Vue 对象上
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
...
</script>
// (2)使用ES模块构建版本
<script type="module">
import { createApp, ref } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
</script>
// (3)启用 Import maps- 告诉浏览器如何定位到导入的 vue
<script type="importmap">
{
"imports": {
"vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
}
}
</script>
<script type="module">
import { createApp, ref } from 'vue'
createApp({
setup() {
const message = ref('Hello vue!')
return {
message
}
}
}).mount('#app')
</script>
// (4)拆分模块
注意:ES模块不能通过 file://协议工作:打开本地文件时浏览器使用的协议,要启动一个本地的HTTP服务器
注意:type="module":浏览器才能支持ES模块语法,才支持import等
2.基础
2.1创建一个应用
2.1.1创建应用实例并挂载
import { createApp } from 'vue'
import App from './App.vue'
// createApp 的对象实际上是一个组件
const app = createApp(App)
// 挂载应用
app.mount('#app')
2.1.2应用配置
// 全局注册组件
app.component('button', button) --
// 应用级的错误处理器
app.config.errorHandler= (err) => {
/* 处理错误 */
}
2.2模板语法
2.2.1文本插值
<span>Message: {{ msg }}</span>
2.2.2原始html
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
2.2.3Attribute绑定
<!-- 单个 -->
<div v-bind:id="dynamicId"></div>
<!-- 多个 -->
<div> v-bind="objectOfAttrs"</div>
2.2.4js表达式
{{ number + 1 }}
{{ formatDate(date) }}
2.2.5指令 Directives
<p v-if="seen">Now you see me</p>
<a @click="doSomething"> ... </a>
<form @submit.prevent="onSubmit">...</form>
2.3响应式基础
2.3.1 setup
介绍:setup是vue3中一个新的配置项,值是一个函数,组合式api均写在其中。
特点:
(1)函数返回的对象内容可以直接在模板中使用
(2)访问this是undefined
(3)会在beforecreate之前调用
正常使用:
<script>
import { ref } from 'vue'
export default {
name: 'homeView',
setup(){
let message = ref('hello world')
return {
message
}
}
}
</script>
语法糖:
<script setup>
import { ref } from 'vue'
let message = ref('hello world')
</script>
2.3.2 ref和reactive
(1)定义数据类型不同
ref:可用于定义基本数据类型和对象数据类型
reactive:只用于定义对象数据类型
(2)使用注意
ref:必须使用.value,模板自动结构
reactive:重新分配一个对象,会失去响应式(可使用object.assign整体替换)
(3)什么时候使用reactive - 定义响应式对象且层级较深
2.3.3 toRefs和toRef(解构赋值时使用)
toRefs:将一个响应式对象的所有属性都转换为响应式引用
const { name, age } = toRefs(person);
toRef:将一个响应式对象的单个属性转换为响应式引用
const name = toRef(person, 'name');
2.4计算属性
2.4.1基本用法
和函数区别:计算属性值会基于其响应式依赖被缓存--返回值为一个计算属性 ref
const publishedBooksMessage = computed(() => author.books.length > 0 ? 'Yes' : 'No' )
2.4.2可写计算属性
// 注意:计算属性的 getter 应只做计算而没有任何其他的副作用,避免直接修改计算属性值
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
运行fullName.value = 'John Doe',firstName 和 lastName 会随之更新
2.4.3获取上一个值
const alwaysSmall = computed((previous) => {
if (count.value <= 3) {
return count.value
}
return previous
})
2.5类与样式绑定
2.5.1绑定class
绑定对象:<div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div>
绑定数组:<div :class="[activeClass, errorClass]"></div>
组件上使用:只有一个根元素的组件,会被添加到根元素上。否则需要通过组件的$attrs属性来指定接收的元素
2.5.1绑定内联样式
绑定对象:<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
绑定数组:多个样式对象的数组,对象会被合并后渲染到同一元素上
<div :style="[baseStyles, overridingStyles]"></div>
2.6条件渲染
2.6.1v-if和v-show区别
v-if:更高的切换开销,v-show:更高的初始渲染开销
2.6.2v-if和v-for执行顺序
同时存在于一个节点上,v-if 会首先被执行,且无法获取item
2.7列表渲染 v-for
2.7.1 v-for与数组
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
// of只能遍历数组
2.7.2 v-for与对象
<li v-for="(value, key) in obj">
{{ key }}: {{ value }}
</li>
2.7.3 key作用
用于标识每个元素的唯一性,帮助Vue高效地追踪和更新DOM元素
2.7.4 数组变化侦测
数组变化侦测:push,pop,shift,unshift,splice,sort,reverse
不会改变原数组的:filter,concat,slice
2.7.5 splice和slice区别及使用
// splice:修改原数组(截取并添加,包含起始索引)
let arr = [1, 2, 3, 4, 5];
arr.splice(2, 1, 'a', 'b'); // 从索引2开始,删除1个元素,添加'a'和'b'
console.log(arr); // [1, 2, 'a', 'b', 4, 5]
// slice:不修改数组[)(只截取,不包含结束索引)
let arr = [1, 2, 3, 4, 5];
let newArr = arr.slice(1, 4); // 从索引1到3(不包括4)
console.log(newArr); // [2, 3, 4]
console.log(arr); // [1, 2, 3, 4, 5] 原数组不变
2.8事件处理
2.8.1监听事件
// 内联事件处理器
<button @click="count++">Add 1</button>
// 方法事件处理器
<button @click="greet">Greet</button>
2.8.2事件修饰符
stop,prevent,self,capture,once,passive
// 事件冒泡:目标元素-根元素(从内到外)
// 事件捕获:根元素-目标元素(从外到内,通常不会默认触发,除非明确指定启用)
2.8.2按键,鼠标修饰符
@keyup.enter,@keyup.alt.enter
.left,.right,.middle
2.9表单输入绑定
2.9.1 表单v-model的原始实现
<input :value="text" @input="event => text = event.target.value">
<input v-model="text">
2.9.2 v-model基本用法
<input v-model="text">
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
2.9.3 修饰符
.lazy,.number,.trim
2.10侦听器watch
2.10.1侦听器作用
在状态变化时执行一些“副作用”:例如更改DOM,异步操作
2.10.2 watch监听数据
(1)ref定义的数据
(2)reactive定义的数据
(3)getter函数
(4)包含上述内容的数组
// 监听地址值,需要考虑细枝末节才需要开启deep:true
// 情况一:ref定义的基本类型数据
// 注意:监听的是value值的变化
import { ref, watch } from 'vue'
let number = ref(1);
watch(number, (newValue, oldValue) => {
console.log(newValue, oldValue)
})
// 情况二:ref定义的对象类型数据
// 注意:
(1)监听ref定义的对象类型数据,监听的是对象地址值,若想监听内部数据,需要开启deep: true
(2)若修改对象属性值,newValue和oldValue值一致取最新
(3)若修改整个对象值,newValue取最新,oldValue取旧值
<!-- 修改对象属性值 -->
<button @click="person.active.score++">增长</button>
watch(person, (newValue, oldValue)=> {
console.log(newValue, oldValue)
}, {
deep: true
})
<!-- 修改整个对象 -->
person.value = { ... }
watch(person, (newValue, oldValue)=> {
console.log(newValue, oldValue)
})
// 情况三:reactive定义的对象类型数据
// 注意:
(1)监听ref定义的对象类型数据,监听的是对象地址值(newValue和oldValue值一致取最新)
(2)默认开启深度监听(隐式创建深度监听)
<button @click="person.active.score++">增长</button>
watch(person, (newValue, oldValue)=> {
console.log(newValue, oldValue)
})
// 情况四:监听ref或reactive定义对象的某个属性
// 注意:
(1)如属性不是对象类型,需要写成函数形式
(2)如属性是对象类型,建议写成函数类型,不写成函数类型
import { ref, watch } from 'vue'
let person = ref({
name: '张三',
age: 18,
active: {
one: '打游戏',
score: 100
}
})
const changePerson = () => {
person.value.active = {
one: '吃饭',
score: 101
}
}
// changePerson不会执行,person.active.score++执行
watch(person.value.active, (newValue, oldValue)=> {
console.log(newValue, oldValue)
})
// () => person.value.active监听的是地址值
// changePerson执行,person.active.score++不执行
watch(() => person.value.active, (newValue, oldValue)=> {
console.log(newValue, oldValue)
})
// changePerson执行(newValue,oldValue不一致),person.active.score++执行
watch(() => person.value.active, (newValue, oldValue)=> {
console.log(newValue, oldValue)
}, {
deep: true
})
// 情况五:包含上述内容的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})
2.10.3 watchEffect
// 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数
// 不用明确指出监视的数据
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
2.10.4 副作用清理
// id 变为新值时取消过时的请求 onWatcherCleanup(Vue 3.5+)
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 回调逻辑
})
onWatcherCleanup(() => {
// 终止过期请求
controller.abort()
})
// onCleanup(Vue 3.5+ 之前)
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})
2.10.5 停止监听
const unwatch = watchEffect(() => {})
// const unwatch = watch(() => {})
// ...当该侦听器不再需要时
unwatch()
2.11模板引用
2.11.1 模板引用方式
<template>
<input ref="inputRef">
</template>
<script setup>
import { ref, useTemplateRef, onMounted } from 'vue'
// 3.5+
const input = useTemplateRef('inputRef')
onMounted(() => {
input.value.focus()
})
// 3.5之前
// const inputRef = ref(null)
// onMounted(() => {
// inputRef.value.focus()
// })
</script>
2.12组件基础
2.12.1 组件介绍
什么是组件:独立,可重用的部分
SFC:单文件组件
2.13生命周期
2.13.1 vue2的生命周期
创建:beforeCreate, created
挂载:beforeMount, mounted
更新:beforeUpdate, updated
销毁:beforeDestroy, destroyed
2.13.2 vue3的生命周期
创建:setup
挂载:onBeforeMount, onMounted
更新:onBeforeUpdate, onUpdated
卸载:onBeforeUnmount, onUnmounted
3.深入组件
3.1 注册组件
3.1.1 组件名格式
PascalCase(大驼峰命名法)
3.1.2 全局注册
import MyComponent from '@/components/MyComponent.vue';
app.component('MyComponent', MyComponent)
<my-component></my-component>
3.1.3 局部注册
<script setup>
import MyComponent from '@/components/MyComponent.vue';
</script>
<my-component></my-component>
3.2 props(小驼峰)
3.2.1 props声明方式
// (1)不指定类型
// const props = defineProps(['name', 'age']);
// (2)指定类型
const props = defineProps({
name: {
type: String,
required: true
},
age: Number
})
console.log(props.name);
3.2.2 响应式 Props 解构
const { age } = defineProps(['name', 'age']);
watchEffect(() => {
// 在 3.5 之前只运行一次
// 在 3.5+ 中在 "foo" prop 变化时重新执行
console.log(age)
})
// 监听ref或reactive定义对象的某个属性,需采用函数形式
watch(() => age, (val) => {
console.log(val);
})
// 监听ref或reactive定义对象的某个属性,需采用函数形式
watch(() => props.age, (val) => {
console.log(val);
})
3.2.3 单向数据流
// prop 是只读的,可通过以下方式取值处理,但只能取初始值
const props = defineProps(['name', 'age']);
// (1)
const number = ref(props.age)
number.value++;
// (2)
const doubleNum = computed(() => props.age*2)
3.3 事件
3.3.1 事件的触发及传参
// 父组件
<my-component @handleClick="(msg) => str = msg + str"></my-component>
<!-- 或 -->
<script setup>
const handleClick = (msg) => {
console.log(msg + '子组件触发父组件的函数');
}
</script>
// 子组件
<button @click="$emit('handleClick', 'hello')">修改</button>
<!-- 或 -->
<script setup>
const emit = defineEmits(['handleClick']);
emit('handleClick', 'hello')
</script>
3.4 组件v-model
3.4.1 基本用法
// 3.4 版本之前
// 父组件
<my-component :modelValue="number" @update:modelValue ="(e) => number = e"></my-component>
// 自组件
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue'])
const handleChange = () => {
emit('update:modelValue', props.modelValue + 1)
}
// 3.4 版本之后
// 父组件
<template>
{{ number }}
<my-component v-model="number"></my-component>
</template>
// 子组件
<script setup>
const model = defineModel();
model.value++;
</script>
3.4.2 多个v-model绑定
// 父组件
{{ number }}{{name }}
<my-component v-model:number="number" v-model:name="name"></my-component>
// 子组件
<template>
<input type="text" v-model="number">
<input type="text" v-model="name">
</template>
<script setup>
const number = defineModel('number');
const name = defineModel('name')
</script>
3.4.3 处理 v-model 修饰符
// 父组件
<my-component v-model:number.capitalize="number"></my-component>
// 子组件
<script setup>
const [number, modifiers] = defineModel('number');
console.log(number.value, modifiers)
</script>
3.5 透传Attributes
3.5.1 什么是透传Attributes
透传Attributes:父组件传递给子组件但未被显式声明的属性,它们会自动应用到子组件的根元素上
一般用于传递与 DOM 相关的属性
3.5.2 单个根节点
注意:未被显示声明的属性和方法,声明后的在$attrs中获取不到。
// 父组件
<my-component
class="red-style"
:style="{ fontSize: '36px' }"
name="张三"
@click="handleClick">
</my-component>
// 子组件
<div>你好</div>
3.5.3 多个根节点
// 父组件
<my-component
class="red-style"
:style="{ fontSize: '36px' }"
name="张三"
@click="handleClick">
</my-component>
// 子组件
<div v-bind="$attrs">你好</div>
<div :style="$attrs.style">世界</div>
// 注意:若red-style样式为scope,多个根子组件能绑定的到,但样式不起作用
3.5.4 在js中使用及阻止自动继承
defineOptions({
// 阻止自动继承,但js中和$attrs中依然能获取,可自由绑定
inheritAttrs: false
});
// js使用
import { useAttrs } from "vue";
const attrs = useAttrs();
console.log(attrs);
3.6 插槽(内容分发)
3.6.1 插槽作用
允许子组件在特定位置插入父组件的内容,实现内容的灵活分发和复用
3.6.2 插槽的基本使用
// 父组件
<my-component>
这是父组件插槽内容
</my-component>
// 子组件
<slot>
这是插槽默认内容
</slot>
3.6.3 插槽的基本语法
// 父组件
<template>
<my-component>
<!-- 父组件接收子组件数据 -->
<template v-slot:slotA = slotAProps>
<div>插槽A</div>
<div>{{ slotAProps }}</div>
</template>
<template #slotB>
<div>插槽B</div>
</template>
默认插槽
</my-component>
</template>
// 子组件
<template>
<div>子组件</div>
<!-- 向父组件传递数据 -->
<slot name="slotA" message="张三" :age="18"></slot>
<slot></slot>
<!-- 条件插槽:根据是否有插槽B,给其添加额外样式 -->
<div v-if="$slots.slotB" class="slotB">
<slot name="slotB"></slot>
</div>
</template>
3.6.4 插槽的实际运用场景
// 列表-获取数据等通用逻辑由组件完成,列表样式等由使用它的父组件完成
// 父组件
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
// 子组件
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
3.7 依赖注入
3.7.1 基本使用
// 父组件
import { ref, provide } from "vue";
let number = ref(0);
const changeNumber = () => {
number.value++
}
provide('handleData', {
number,
changeNumber
})
// 后代组件
import { inject } from "vue";
const { number, changeNumber} = inject('handleData');
3.7.2 应用层provide
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
3.7.3 Symbol注入名
// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, {
/* 要提供的数据 */
})
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
3.8 异步组件
3.8.1 基本用法
<!-- 需要的时候才加载相关组件,MyComponentB每次切换的时候会重新加载一遍 -->
<template>
<component :is="componentName"></component>
<div>
<button @click="componentName = MyComponentA">切换组件A</button>
<button @click="componentName = MyComponentB">切换组件B</button>
</div>
</template>
<script setup>
import { defineAsyncComponent, shallowRef } from "vue";
import MyComponentA from "@/components/MyComponentA.vue";
// import MyComponentB from "@/components/MyComponentB.vue";
const MyComponentB = defineAsyncComponent(() => import('@/components/MyComponentB.vue'))
let componentName = shallowRef(MyComponentA);
</script>
3.8.2 加载与错误状态
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
4.逻辑复用
4.1 组合式函数(hooks)
4.1.1 什么是组合式函数
利用Vue的组合式API来封装和复用有状态逻辑的函数(类似于vue2的Mixin)
无状态逻辑:时间可视化
有状态逻辑:跟踪鼠标位置-会随时间变化的状态
4.1.2 鼠标跟踪器示例
// hooks/useMouse.js
import { reactive, onMounted, onUnmounted } from "vue";
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
const location = reactive({
x: 0,
y: 0
})
function updateLocation (event) {
location.x = event.x;
location.y = event.y;
}
onMounted(() => window.addEventListener('mousemove', updateLocation))
onUnmounted(() => window.removeEventListener('mousemove', updateLocation))
return location
}
// 使用
<script setup>
import { useMouse } from '@/hooks/useMouse.js'
const location = useMouse();
</script>
4.1.3 export和export default区别
export → 多个、需要相同名字、用 {} 导入
export default → 只能一个、可以自定义名字、不用 {} 导入
4.2 自定义指令
4.2.1 介绍
作用:为了重用涉及普通元素的底层DOM访问的逻辑(方便重用dom访问)
使用时机:所需功能只能通过直接的DOM操作来实现时
应用场景:
(1)根据鼠标位置改变某个元素的颜色或背景色
(2)输入框添加焦点
(3)滚动事件的监听 - 实现懒加载,当滚动到页面底部时加载更多内容
(4)权限控制 - 根据用户权限动态显示或隐藏某些元素
(5)防抖/节流
4.2.2 基本示例
//(1)聚焦
<template>
<input v-focus />
</template>
<script setup>
// script setup 中任何以v开头的驼峰式命名的变量都可以当作自定义指令使用
const vFocus = {
mounted: (el) => el.focus()
}
</script>
// (2)防抖
app.directive('debounce', {
beforeMount(el, binding) {
let timeout;
// 给输入框添加输入事件
el.addEventListener('input', () => {
// 清除上一次的定时器
clearTimeout(timeout);
// 设置一个新的定时器,在输入停止后的300ms触发
timeout = setTimeout(() => {
// 执行传入的回调函数
binding.value();
}, 300); // 300ms 防抖延迟
});
}
});
// 使用
<template>
<input v-debounce="search" type="text" placeholder="搜索...">
</template>
<script setup>
const search = () => {
console.log('执行搜索');
};
</script>
4.2.3 指令钩子
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) {}
}
4.2.4 节流/防抖
节流:一定时间内只执行一次(如点击,滚动等)
防抖:多次操作只执行最后一次(输入框搜索,窗口大小调整)
4.3 插件
4.3.1 介绍
作用:为Vue添加全局功能的工具代码
应用场景:
(1)全局组件或自定义指令:app.component,app.directive
(2)依赖注入:app.provide
(3)全局实例属性或方法:app.config.globalProperties
(4)包含以上所有的功能库(vue-router)
4.3.2 基本使用
const myPlugin = {
install(app, options) {
// 配置此应用
app.component('componentA',componentA);
app.directive('debounce', {});
app.provide('i18n', options);
app.config.globalProperties.$translate = () => {};
}
}
// 使用
app.use(myPlugin, {
/* 可选的选项 */
})
5.内置组件
5.1 transition
5.1.1 transition组件介绍
// 作用:会在一个元素或组件进入和离开 DOM 时应用动画
// 注意:仅支持单个元素或组件(只有一个根元素)作为其插槽内容
// 触发条件:
(1)v-if
(2)v-show
(3)动态组件component
(4)改变特殊key值
// 项目中实战:路由切换、组件状态变化、动态组件加载等
5.1.2 基础使用
<template>
<button @click="show = !show">Toggle</button>
<Transition>
<p v-if="show">hello</p>
</Transition>
</template>
<script setup>
import { ref } from "vue";
let show = ref(true)
</script>
<style scoped>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
5.2 transitionGroup
5.3 KeepAlive
5.3.1 keepAlive 基本使用
<!-- 作用:组件间动态切换时缓存被移除的组件实例-->
// 父组件
<template>
<div>
<button @click="toggleComponent">切换组件</button>
</div>
<!-- 3.2.34 或以上 script语法糖组件会自动根据文件名生成对应的name-->
<!-- max:超出最大值,最久未被访问的将会被销毁 -->
<KeepAlive :include="['MyComponentA']" :max="10">
<component :is="componentMap[currentKey]"></component>
</KeepAlive>
</template>
<script setup>
import MyComponentA from "@/components/MyComponentA.vue";
import MyComponentB from "@/components/MyComponentB.vue";
import { ref } from "vue";
const componentMap = {
ComA: MyComponentA,
ComB: MyComponentB,
}
const currentKey = ref('ComA');
const toggleComponent = () => {
currentKey.value = currentKey.value == 'ComA' ? 'ComB' : 'ComA';
}
</script>
// 子组件
<template>
子组件A
</template>
<script setup>
import { onMounted, onActivated, onDeactivated } from "vue";
// 缓存后只会触发一次
onMounted(() => {
console.log('加载子组件A')
})
// 缓存实例的生命周期(只有缓存的组件才会触发)
onActivated(() => {
console.log('onActivated -- A')
})
onDeactivated(() => {
console.log('onDeactivated -- A')
})
</script>
5.3.2 动态组件相关
动态组件会通过全局组件注册表查找name,全局注册组件可以直接用字符串名访问,局部注册组件只能通过组件对象查找
5.4 Teleport
5.4.1 Teleport介绍
<!-- 作用:将一个组件内部的一部分模板“传送”到该组件的DOM结构外层的位置去(传送dom元素位置) -->
<!-- 示例 -->
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
5.5 Suspense
5.5.1 Teleport介绍
<!-- 作用:处理异步组件的加载状态 -->
<!-- 简单示例 -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<p>加载中...</p>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
new Promise((resolve) =>
setTimeout(() => resolve(import('@/components/MyComponentA.vue')), 2000)
)
)
</script>
6.应用规模化
6.1 vue-router
6.1.1 路由基础介绍
服务端路由:点击链接,服务端返回全新html,加载整个页面
客户端路由:单页面应用中,js可以拦截页面的跳转,动态获取数据,在无需加载页面的情况下更新页面。
RouterLink组件:使得Vue Router能够在不重新加载页面的情况下改变URL
RouterView组件:告诉Vue Router在哪里渲染当前URL路径对应的路由组件
过程:当你通过Vue Router来进行页面跳转时,地址栏的URL会发生变化,而页面不会刷新,Vue Router会根据URL的变化来匹配对应的路由并加载对应的组件
定义路由:配置路径与组件的映射关系
动态路由匹配:根据不同url加载不同组件
嵌套路由:支持父路由嵌套子路由,形成负责页面结构
路由守卫:控制页面访问机制如:权限验证
6.1.2 路由入门
(1)创建路实例:
const router = createRouter({
history: createMemoryHistory(),
routes,
})
(2)注册路由器插件
app.use(router)
// 作用:1.全局注册RouterView和RouterLink组件。
2.全局添加$router,$route。
3.启用useRoute()和useRouter组合式函数
4.触发路由器解析初始路由
(3)访问路由器和当前路由
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
6.1.3 动态路由匹配
// 示例 - 实例没有被销毁重建,通过监听或导航守卫beforeRouteUpdate监听参数变化
{ path: '/users/:id/:name', component: User } => {{ $route.params.id }}
// 实际应用:404 页面
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
{ path: '/:orderId(\\d+)' }, -> :orderId -> 仅匹配数字
{ path: '/:productName' }, -> :productName -> 匹配其他任何内容
{ path: '/:chapters+' } ->/one, /one/two, /one/two/three
6.1.4 嵌套路由
// 注意:以 / 开头的嵌套路径将被视为根路径。这允许你利用组件嵌套,而不必使用嵌套的 URL
// 注意:嵌套路由一般给子路由命名,方便根据name跳转
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: () => import('../views/HomeView.vue'),
},
{
path: '/user/:username',
component: () => import('../views/User.vue'),
children: [
{
path: "userHome", name: "userHome", component: () => import('../views/UserHome.vue'),
}
]
}
],
})
// user页面 (/user/张三/userHome)
<template>
User页面
{{ $route.params.username }}
<router-view></router-view>
</template>
6.1.5 编程式导航及路由传参
// 注意: `params` 不能与 `path` 一起使用
// 注意:传递params参数时,需要提前在规则中占位
router.push({ name: 'userHome', params: { username: '张三' } });
router.push({ path: '/register', query: { plan: 'private' } });
// 替换
router.push({ path: '/home', replace: true });
router.replace({ path: '/home' });
// 横跨历史
router.go(1); // 前进一
router.go(-1) // 后退一
6.1.6 命名视图
<!-- 同级展示多个视图 -->
<template>
<router-view />
<router-view name="user"></router-view>
</template>
routes: [
{
path: '/',
name: 'home',
components:{
default: () => import('../views/HomeView.vue'),
user: () => import('../views/User.vue')
}
}
]
6.1.7 重定向和别名
// 重定向: 将某个路由自动跳转到另一个路由
const routes = [
// (1)重定向到地址
{ path: '/home', redirect: '/' }
// (2)重定向到命名的路由
{ path: '/home', redirect: { name: 'homepage' } },
// (3)动态返回重定向目标
{
// /search/screens -> /search?q=screens
path: '/search/:searchText',
redirect: to => {
// 方法接收目标路由作为参数
// return 重定向的字符串路径/路径对象
return { path: '/search', query: { q: to.params.searchText } }
},
},
// (4)相对重定向
{
// 将总是把/users/123/posts重定向到/users/123/profile。
path: '/users/:id/posts',
redirect: to => {
// 该函数接收目标路由作为参数
return to.path.replace(/posts$/, 'profile')
},
},
]
// 别名:允许为同一个路由配置多个访问路径。如:'/'别名是 '/home',访问'/home'时路径还是'/home',匹配为'/'
const routes = [
// (1)单个
{ path: '/', component: Homepage, alias: '/home' },
// (2)多个 - /users /people /users/list 展示UserList
{
path: '/users',
component: UsersLayout,
children: [
{ path: '', component: UserList, alias: ['/people', 'list'] },
],
},
]
6.1.8 路由组件传参(props)
// (1)当props设置为 true 时,route.params 将被设置为组件的 props
routes: [
{
path: '/user',
component: () => import('../views/User.vue'),
children: [
{
path: "details/:id/:name",
name: 'details',
component: () => import('../views/details.vue'),
props: true
}
]
}
],
// 页面
{{ $attrs.id + $attrs.name }}
// (2)函数模式
{
path: '/user',
component: () => import('../views/User.vue'),
children: [
{
path: "details",
name: 'details',
component: () => import('../views/details.vue'),
props: route => route.query
}
]
}
$route.push({ path: '/user/details', query: { name: '张三', id: 666 } })
// 页面
{{ $attrs.id + $attrs.name }}
6.1.9 hash和history模式区别
// 区别
hash:url带#,兼容性更好,不需要服务器端处理路径,不利于seo搜索(createWebHashHistory)
history:url不带#,请求路径发送给服务器,需要服务器端配合处理路径,否则报错404(createWebHistory)
// history模式
1.请求路径发给服务器(服务器配置:try_files $uri $uri/ /index.html;)
2.正常情况服务器根据路径返回文件,但实际单页面应用不存在物理文件,所以返回统一文件入口(index.html)
3.Vue Router解析URL渲染正确的组件
// history模式base问题
createWebHistory(base)
base: /my-app/ // 应用部署在 /my-app/ 子目录下
6.1.10 导航守卫(拦截)
// 口诀:全局管权限,独享管路由,组件管自己;
先离旧,再全局,复用更新后进门;
解析完,记录完,流程结束页面现。
beforeRouteLeave -> beforeEach -> beforeRouterUpdate ->
beforeEnter -> beforeRouterEnter -> beforeResolve(确保摄像头权限已授权) -> afterEach
// 路由重定向:访问路径A时,自动跳转到路径B
// 1.导航守卫类型有哪些?
(1)全局守卫
router.beforeEach - 全局权限拦截
router.beforeResolve - 终极校验(页面摄像头权限)
router.afterEach - 事后记录
(2)路由独享守卫
beforeEnter - 单路由权限
(3)组件内守卫
beforeRouteEnter - 进门前的数据加载
beforeRouteUpdate - 参数改变,更新内容
beforeRouteLeave - 离开前的未保存提示
// 2.vue router3.x和vue router4.x有什么区别?
// vue router4.x中next函数不是必须的,但如果写了,就必须放行
(1)允许导航: next() -> return true || undefined
(2)取消导航:next(false) -> return false
(4)重定向到其他路由:next('/login') -> return '/login' // 会重新触发一次导航流程,重新执行相关守卫
// 3.replace: true 什么时候使用?
(1)需要返回原页面场景:详情页,表单步骤页等
(2)不需要返回原页面场景:登录页,错误页,成功页等
// 4.各导航守卫的实际应用场景
(1)beforeEach - 全局权限拦截
// 作用:在路由跳转前触发,用于全局权限验证、登录状态检查等。
// 场景:未登录时拦截访问受限页面,并重定向到登录页
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isLoggedIn) return '/login';
});
(2)beforeResolve - 终极校验
// 作用:刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用
// 场景:根据路由在元信息中的requiresCamera属性确保用户访问摄像头的权限,确保异步数据加载完成后才允许导航
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
(3)afterEach - 事后记录
// 作用:导航完成后触发,不影响导航本身
// 场景:记录用户访问日志或修改页面标题,添加meta动画
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
(4)beforeEnter - 单路由权限
// 作用:仅对当前路由生效,适用于特定页面的访问控制
// 场景:管理员页面需校验用户角色
const routes = [
{
path: '/admin',
component: AdminPage,
beforeEnter: (to) => {
if (!isAdmin) return '/403';
}
}]
(5)beforeRouteEnter - 进门前的数据加载
// 作用:组件渲染前调用,无法访问 this,但可通过回调操作实例
// 场景:进入页面时从接口加载数据(如用户详情)
beforeRouteEnter(to, from, next) {
next((vm) => {
vm.fetchData(); // 组件实例创建后调用
});
}
(6)beforeRouteUpdate - 参数变化,更新内容
// 作用:路由参数变化但组件复用时触发(如 /user/1 → /user/2)
// 场景:动态更新组件数据
beforeRouteUpdate(to) {
this.userId = to.params.id;
this.loadUserData();
}
(7)beforeRouteLeave - 离开前的未保存提示
// 作用:离开当前路由前触发,用于阻止未保存的操作
// 场景:表单未保存时提示用户确认
beforeRouteLeave() {
if (this.hasUnsavedChanges) {
return confirm('确定离开吗?未保存的数据将丢失!');
}
}
6.1.11 路由元信息
// 作用:用于在路由配置中存储任意的自定义数据
// 场景:页面标题管理,权限管理,路由缓存等
// 使用:
const routes = [
{
path: '/home',
component: Home,
meta: { title: '首页', requiresAuth: true, transition: 'slide-left' },
}
]
{{$route.meta}}
router.beforeEach((to) => {
console.log(to.meta)
})
6.1.12 routerview
// (1)插槽 下面两种方式效果一样
<router-view />
<router-view v-slot="{ Component }">
<component :is="Component"></component>
</router-view>
// (2)KeepAlive & Transition
<router-view v-slot="{ Component, route }">
<transition :name="route.meta.transition || 'fade'">
<keep-alive>
<component :is="Component" ref="mainContent" />
</keep-alive>
</transition>
</router-view>
// 注意:可在组件上加:key="route.path"强制过渡,但会导致keep-alive失效
// 根据路径的深度动态添加信息到 meta 字段
// router.afterEach 保证路由切换完成后,页面已更新好,动画需要在页面渲染后执行
router.afterEach((to, from) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
to.meta.transition = toDepth < fromDepth ? 'slide-right' : 'slide-left'
})
6.1.13 滚动行为(scrollBehavior)
const router = createRouter({
history: createWebHashHistory(),
routes: [...],
scrollBehavior (to, from, savedPosition) {
// return 期望滚动到哪个的位置
// 始终滚动到顶部
return { top: 0 }
}
})
6.1.14 路由懒加载
// 作用:减少打包体积
// 将import UserDetails from './views/UserDetails.vue'替换成
const UserDetails = () => import('./views/UserDetails.vue')
6.1.15 动态路由
// 一.添加路由 - addRoute
// (1)路由文件中添加路由
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:articleName', component: Article }],
})
router.addRoute({ path: '/about', component: About })
router.replace(router.currentRoute.value.fullPath) // 显示新路由
// (2)导航守卫中添加路由
router.beforeEach(to => {
if (!hasNecessaryRoute(to)) {
router.addRoute(generateRoute(to))
// 触发重定向
return to.fullPath
}
})
// (3)添加嵌套路由
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
// 等同于
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
// 二.删除路由 - removeRoute
// (1)删除同名的
router.addRoute({ path: '/about', name: 'about', component: About })
// 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的
router.addRoute({ path: '/other', name: 'about', component: Other })
// (2)按名称删除路由
router.removeRoute('about')
// (3)无名称,根据回调删除
const removeRoute = router.addRoute(routeRecord)
removeRoute() // 删除路由如果存在的话
// 三.查看现有路由
router.hasRoute() // 检查一个给定名称的路由是否存在
router.getRoutes() // 获得所有路由记录的完整列表
6.2 pinia
6.1.1 SPA,SSR区别
SPA:单页面应用,html生成者浏览器,典型框架:vue/react
SSR:服务端渲染,html生成这服务器,典型框架:nuxt.js(vue)/next.js(react)
SSR 解决的是 SEO 和首屏速度问题,但需要更复杂的架构设计;SPA 仍是大多数前端项目的默认选择
6.2.2 pinia介绍
// (1)pinia是什么?
符合直觉的vue.js状态管理库
// (2)为什么使用pinia,而非直接 export const state = reactive({})
最重要原因:某些特殊场景,模块会被多次执行,直接使用export const 方式可能会出现「多实例污染」问题
如:SSR:服务器同时处理多个用户请求,若直接导出state,所有用户共享同一个内存中的对象,导致数据互相覆盖
注意:SPA:每个用户的浏览器是独立环境,state只在当前用户页面内存在,互不影响。
Pinia在SSR中会为每个请求创建一个独立的状态实例,确保不同用户的数据隔离
好处:保证全局状态的唯一性,Devtools深度集成,TypeScript 友好,完美支持 Composition API,代码更简洁直观
// (3)安装及引入
npm install pinia
import { createPinia } from 'pinia'
app.use(pinia)
6.2.3 pinia使用示例
//(1) 选项式Store(counter.js)
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
// 状态定义
state: () => ({
count: 0,
title: 'Counter'
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2
},
// 操作方法(同步/异步)
actions: {
increment() {
this.count++;
},
async fetchData() {
const res = await fetch('api/data');
this.data = await res.json();
}
}
});
//(2) 组合式Store(counter.js)
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCounterStore = defineStore('counter', () => {
// 状态定义(使用 ref/reactive)
const count = ref(0);
const title = ref('Counter');
// 计算属性(使用 computed)
const doubleCount = computed(() => count.value * 2);
// 操作方法(同步/异步)
function increment() {
count.value++;
}
async function fetchData() {
const res = await fetch('api/data');
data.value = await res.json();
}
// 返回所有状态和方法
return { count, title, doubleCount, increment, fetchData };
});
// (3) 使用
import { useCounterStore } from './stores/counter';
// 获取 store 实例
const counterStore = useCounterStore();
{{ counterStore.count++ }}
// (4) 修改方式
// 直接修改响应式状态
counterStore.count++;
// 批量修改状态
counterStore.$patch({
count: counterStore.count + 1,
title: 'New Title'
});
// 重置状态
counterStore.$reset(); // 恢复初始状态