vue3之基础学习

  1. 1.开始
    1. 1.1简介
      1. 1.1.1vue介绍
    2. 1.2快速上手
      1. 1.2.1创建一个应用
      2. 1.2.2通过 CDN 使用 Vue
  2. 2.基础
    1. 2.1创建一个应用
      1. 2.1.1创建应用实例并挂载
      2. 2.1.2应用配置
    2. 2.2模板语法
      1. 2.2.1文本插值
      2. 2.2.2原始html
      3. 2.2.3Attribute绑定
      4. 2.2.4js表达式
      5. 2.2.5指令 Directives
    3. 2.3响应式基础
      1. 2.3.1 setup
      2. 2.3.2 ref和reactive
      3. 2.3.3 toRefs和toRef(解构赋值时使用)
    4. 2.4计算属性
      1. 2.4.1基本用法
      2. 2.4.2可写计算属性
      3. 2.4.3获取上一个值
    5. 2.5类与样式绑定
      1. 2.5.1绑定class
      2. 2.5.1绑定内联样式
    6. 2.6条件渲染
      1. 2.6.1v-if和v-show区别
      2. 2.6.2v-if和v-for执行顺序
    7. 2.7列表渲染 v-for
      1. 2.7.1 v-for与数组
      2. 2.7.2 v-for与对象
      3. 2.7.3 key作用
      4. 2.7.4 数组变化侦测
      5. 2.7.5 splice和slice区别及使用
    8. 2.8事件处理
      1. 2.8.1监听事件
      2. 2.8.2事件修饰符
      3. 2.8.2按键,鼠标修饰符
    9. 2.9表单输入绑定
      1. 2.9.1 表单v-model的原始实现
      2. 2.9.2 v-model基本用法
      3. 2.9.3 修饰符
    10. 2.10侦听器watch
      1. 2.10.1侦听器作用
      2. 2.10.2 watch监听数据
      3. 2.10.3 watchEffect
      4. 2.10.4 副作用清理
      5. 2.10.5 停止监听
    11. 2.11模板引用
      1. 2.11.1 模板引用方式
    12. 2.12组件基础
      1. 2.12.1 组件介绍
    13. 2.13生命周期
      1. 2.13.1 vue2的生命周期
      2. 2.13.2 vue3的生命周期
  3. 3.深入组件
    1. 3.1 注册组件
      1. 3.1.1 组件名格式
      2. 3.1.2 全局注册
      3. 3.1.3 局部注册
    2. 3.2 props(小驼峰)
      1. 3.2.1 props声明方式
      2. 3.2.2 响应式 Props 解构
      3. 3.2.3 单向数据流
    3. 3.3 事件
      1. 3.3.1 事件的触发及传参
    4. 3.4 组件v-model
      1. 3.4.1 基本用法
      2. 3.4.2 多个v-model绑定
      3. 3.4.3 处理 v-model 修饰符
    5. 3.5 透传Attributes
      1. 3.5.1 什么是透传Attributes
      2. 3.5.2 单个根节点
      3. 3.5.3 多个根节点
      4. 3.5.4 在js中使用及阻止自动继承
    6. 3.6 插槽(内容分发)
      1. 3.6.1 插槽作用
      2. 3.6.2 插槽的基本使用
      3. 3.6.3 插槽的基本语法
      4. 3.6.4 插槽的实际运用场景
    7. 3.7 依赖注入
      1. 3.7.1 基本使用
      2. 3.7.2 应用层provide
      3. 3.7.3 Symbol注入名
    8. 3.8 异步组件
      1. 3.8.1 基本用法
      2. 3.8.2 加载与错误状态
  4. 4.逻辑复用
    1. 4.1 组合式函数(hooks)
      1. 4.1.1 什么是组合式函数
      2. 4.1.2 鼠标跟踪器示例
      3. 4.1.3 export和export default区别
    2. 4.2 自定义指令
      1. 4.2.1 介绍
      2. 4.2.2 基本示例
      3. 4.2.3 指令钩子
      4. 4.2.4 节流/防抖
    3. 4.3 插件
      1. 4.3.1 介绍
      2. 4.3.2 基本使用
  5. 5.内置组件
    1. 5.1 transition
      1. 5.1.1 transition组件介绍
      2. 5.1.2 基础使用
    2. 5.2 transitionGroup
    3. 5.3 KeepAlive
      1. 5.3.1 keepAlive 基本使用
      2. 5.3.2 动态组件相关
    4. 5.4 Teleport
      1. 5.4.1 Teleport介绍
    5. 5.5 Suspense
      1. 5.5.1 Teleport介绍
  6. 6.应用规模化
    1. 6.1 vue-router
      1. 6.1.1 路由基础介绍
      2. 6.1.2 路由入门
      3. 6.1.3 动态路由匹配
      4. 6.1.4 嵌套路由
      5. 6.1.5 编程式导航及路由传参
      6. 6.1.6 命名视图
      7. 6.1.7 重定向和别名
      8. 6.1.8 路由组件传参(props)
      9. 6.1.9 hash和history模式区别
      10. 6.1.10 导航守卫(拦截)
      11. 6.1.11 路由元信息
      12. 6.1.12 routerview
      13. 6.1.13 滚动行为(scrollBehavior)
      14. 6.1.14 路由懒加载
      15. 6.1.15 动态路由
    2. 6.2 pinia
      1. 6.1.1 SPA,SSR区别
      2. 6.2.2 pinia介绍
      3. 6.2.3 pinia使用示例

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(); // 恢复初始状态
×

喜欢就点赞,疼爱就打赏