欢迎光临
我们一直在努力

vue3笔记

Vue3 笔记


1. 项目的创建与运行

1.1 基于 webpack 构建工具 —— vue-cli 脚手架

# 安装脚手架(全局)
npm i -g @vue/cli
# 切换到项目所在目录
cd 项目所在目录
# 创建项目(按提示选择 Vue 版本)
vue create 项目名
# 进入项目
cd 项目名
# 运行项目(开发环境)
npm run serve

1.2 基于 Vite 构建工具 —— Vite

# 创建项目
npm init vite-app 项目名
# 按提示选择开发框架:输入 Vue 并回车
# 选择版本:默认为 Vue 3,直接回车确认
# 选择是否需要 TypeScript:按需选择 Yes / No
# 进入项目
cd 项目名
# 启动项目
npm run dev

注:npm init vite-app 是早期 Vite 的创建方式,目前已较少使用,推荐直接使用下面的 create-vue。

1.3 基于 Vite 构建工具 —— create-vue 脚手架【推荐】

# 切换到项目所在目录
cd 项目所在目录
# 创建项目(无论是否安装过脚手架都用此方式)
npm init vue@latest
# 运行后会交互式配置:项目名、TypeScript、JSX、Router、Pinia、Vitest 等
# 进入项目
cd 项目名
# 初始化安装依赖
npm i
# 运行项目
npm run dev


2. 目录结构

2.1 main.js 入口文件

// 引入的不再是 Vue 构造函数,而是名为 createApp 的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象 app(类似于 Vue2 中的 vm,但 app 比 vm 更"轻")
const app = createApp(App)
// 挂载到 #app 容器上
app.mount('#app')

// 整合写法:
// createApp(App).mount('#app')

// Vue2 写法(对比):
/* import Vue from 'vue'
const vm = new Vue({
render: h => h(App)
})
vm.$mount('#app') */

注:

  • Vue3 中创建应用使用 createApp() 工厂函数,不再需要 new Vue();
  • 其他文件与 Vue2 基本一致,需要注意的是:Vue3 的 .vue 文件模板中可以没有根标签(支持多根节点,即 Fragment)。

  • 3. 组合式 API

    3.1 setup()

    setup() 是组合式 API 的核心入口,所有组合式 API(数据、方法、计算属性、侦听属性等)都可以写在它里面。它返回一个对象,对象中为该函数的数据、函数等。

    注意点:

    • setup() 在 beforeCreate 之前执行一次,且其中的 this 是 undefined(不能用 this 访问组件实例);
    • setup() 不能被 async 修饰。若写成 async 函数,返回的是 Promise,需要配合 Suspense 组件使用,一般不建议这样写;
    • setup() 有两个参数:props 和 context。

    <script>
    export default {
    name: 'App',
    setup() {
    // 数据
    let name = '张三'
    let age = 18
    // 方法
    function sayHello() {
    alert(`我叫${name},我${age}岁了,你好!`)
    }
    // 返回对象:数据和方法都要 return 出去才能被模板使用
    return {
    name,
    age,
    sayHello
    }
    }
    }
    </script>

    3.1.1 setup() 中的 props 参数

    作用:接收父组件传递过来的数据。

    <!– App.vue(父组件):向子组件传值 –>
    <template>
    <User name="张三" age="30" :sex="sex"></User>
    </template>

    <script>
    import { ref } from 'vue'
    import User from './components/User.vue'
    export default {
    name: 'App',
    components: { User },
    setup() {
    let sex = ref('男')
    return { sex }
    }
    }
    </script>

    <!– User.vue(子组件):接收数据 –>
    <template>
    </template>

    <script>
    export default {
    name: 'User',
    // 首先像 Vue2 一样用 props 配置项接收
    props: ['name', 'age', 'sex'],
    // 再在 setup 中通过 props 参数接收(setup 中没有 this!)
    setup(props) {
    // props 被包装成响应式代理对象(Proxy)
    console.log(props.name)
    console.log(props.age)
    console.log(props.sex)
    }
    }
    </script>

    注:

  • props 会被包装成响应式的 Proxy 对象,不需要 return 出去即可在模板中使用;
  • props 是只读的,子组件不应修改 props(数据流是单向的);
  • 对 props 进行解构会失去响应式,若确需解构请使用 toRefs(props)(setup 非语法糖场景)。
  • 3.1.2 setup() 中的 context 参数 —— 组件上下文对象

    context 上下文对象包含三个属性:attrs、slots、emit。

    属性作用相当于 Vue2
    attrs 值为对象,包含组件外部传递过来、但没有在 props 配置中声明的属性 this.$attrs
    slots 收到的插槽内容 this.$slots
    emit 分发自定义事件的函数 this.$emit

    (1)自定义事件 —— 子传父:context 中的 emit()

    子组件利用 context.emit() 触发事件,第一个参数为父组件在子组件标签上绑定的事件名,后续参数为父组件事件处理函数所需的参数。

    <!– App.vue(父组件) –>
    <template>
    <Demo @hello="showHelloMsg" msg="你好啊" school="尚硅谷"/>
    </template>

    <script>
    import Demo from './components/Demo'
    export default {
    name: 'App',
    components: { Demo },
    setup() {
    function showHelloMsg(value) {
    alert(`你好啊,你触发了hello事件,我收到的参数是:${value}!`)
    }
    return { showHelloMsg }
    }
    }
    </script>

    <!– Demo.vue(子组件) –>
    <template>
    <h1>一个人的信息</h1>
    <h2>姓名:{{person.name}}</h2>
    <h2>年龄:{{person.age}}</h2>
    <button @click="test">测试触发一下Demo组件的Hello事件</button>
    </template>

    <script>
    import { reactive } from 'vue'
    export default {
    name: 'Demo',
    props: ['msg', 'school'],
    // 声明自定义事件(推荐写法,增强代码可读性)
    emits: ['hello'],
    setup(props, context) {
    let person = reactive({
    name: '张三',
    age: 18
    })
    function test() {
    // 触发自定义事件并传参
    context.emit('hello', 666)
    }
    return { person, test }
    }
    }
    </script>

    (2)context 中的 attrs 与 slots

    setup(props, context) {
    // attrs:接收子组件标签上定义的、但未在 props 中声明的属性
    console.log(context.attrs) // 相当于 Vue2 的 this.$attrs
    // slots:接收子组件标签体的插槽节点内容
    console.log(context.slots) // 相当于 Vue2 的 this.$slots
    // emit:触发自定义事件
    console.log(context.emit)
    }

    关于 attrs 的注意点:

    • Vue2 中可以不显式声明 props 配置项,未声明的属性最终都会存放在 $attrs 中;
    • Vue3 中未在 props 中声明的属性同样会进入 attrs,单根组件会自动继承到根元素上(不会报警告);只有当组件是多根节点且未用 attrs 显式绑定时,才会出现"无法自动继承"的警告。建议始终显式声明 props 以保证代码清晰;
    • props 可以声明类型、默认值、必填项等校验规则,attrs 无法对这些属性做限制。

    3.2 ref() —— 引用对象

    ref() 将数据变成响应式数据,数据可以是基本类型,也可以是对象类型。在 JS 中读写需要 .value,在模板中不需要 .value(模板会自动解包)。

    <script>
    import { ref } from 'vue'
    export default {
    name: 'App',
    setup() {
    // 数据
    let name = ref('张三')
    let age = ref(18)
    // 方法
    function changeInfo() {
    name.value = '李四'
    age.value = 48
    }
    return { name, age, changeInfo }
    }
    }
    </script>

    ref 的底层原理:

    • ref 处理基本类型时,通过 RefImpl 实例的 get value() / set value() 访问器实现数据劫持(思路与 Object.defineProperty 的 getter/setter 类似,但实现机制不同,用的是 ES6 Class 的访问器属性);
    • ref 处理对象类型时,底层调用的是对 reactive() 的封装,即通过 ES6 的 Proxy 实现响应式,嵌套对象都有响应式。

    // 对象类型会被包装成 RefImpl,其 .value 是 reactive 产生的 Proxy 代理对象
    let job = ref({
    type: '前端工程师',
    salary: '30K'
    })
    // 修改时:
    job.value.type = 'UI设计师'
    job.value.salary = '60K'

    // Proxy 实现的响应式,对象中的对象,对象中的对象,都有响应式处理(底层递归处理)
    let userRefImpl = ref({
    name: 'jack',
    age: 20,
    addr: {
    city: '北京',
    street: '大兴区凉水河二街'
    }
    })
    function modifyUser() {
    // 这种整体替换 .value 的方式,走的是 RefImpl 的 setter
    // counterRefImpl.value = 200
    // userRefImpl.value 是一个 Proxy 代理对象
    userRefImpl.value.name = '李四'
    userRefImpl.value.age = 90
    // 修改嵌套地址信息(有响应式)
    userRefImpl.value.addr.city = '邯郸'
    userRefImpl.value.addr.street = '太极二路'
    }

    3.3 reactive()

    reactive() 只处理对象类型(对象/数组)的数据,读取或修改数据时不需要 .value,返回的是一个 Proxy 对象。

    <script>
    import { ref, reactive } from 'vue'
    export default {
    name: 'App',
    setup() {
    let name = ref('张三')
    let age = ref(18)
    // reactive 只能处理对象类型
    let job = reactive({
    type: '前端工程师',
    salary: '30K'
    })
    function changeInfo() {
    name.value = '李四'
    age.value = 48
    // reactive 对象修改不需要 .value
    job.type = 'UI设计师'
    job.salary = '60K'
    }
    return { name, age, job, changeInfo }
    }
    }
    </script>

    3.3.1 回顾 Vue2 对象、数组的响应式处理

    在 Vue2 中:

    • 想添加/删除对象的属性或方法,不能直接修改;
    • 数组不能通过下标直接修改;
    • Vue2 提供 $set()、$delete() 等方法解决,修改/删除数组元素也可用数组自身方法(Vue2 重写了数组方法使其具有响应式)。

    // Vue2 中的写法
    methods: {
    addSex() {
    this.$set(this.person, 'sex', '女') // 添加属性
    // Vue.set(this.person, 'sex', '女')
    },
    deleteName() {
    Vue.delete(this.person, 'name') // 删除属性
    // this.$delete(this.person, 'name')
    },
    updateHobby() {
    this.person.hobby.splice(0, 1, '逛街') // 修改数组元素
    // this.$set(this.person.hobby, 0, '逛街')
    }
    }

    模拟 Vue2 的响应式原理(Object.defineProperty):

    // 源数据
    let person = { name: '张三', age: 18 }
    // 模拟 Vue 中实现响应式
    let p = {}
    Object.defineProperty(p, 'name', {
    get() { // 有人读取 name 时调用
    return person.name
    },
    set(value) { // 有人修改 name 时调用
    console.log('有人修改了name属性,我发现了,我要去更新界面!')
    person.name = value
    }
    })
    Object.defineProperty(p, 'age', {
    get() { return person.age },
    set(value) {
    console.log('有人修改了age属性,我发现了,我要去更新界面!')
    person.age = value
    }
    })

    3.3.2 Vue3 中的响应式原理(Proxy + Reflect)

    Vue3 不存在上述 Vue2 的问题,功劳是 reactive() 内部通过 Proxy 实现代理,并通过 Reflect 操作源对象内部数据。

    模拟 Vue3 的响应式原理:

    // 模拟 Vue3 中实现响应式
    const p = new Proxy(person, {
    // 有人读取 p 的某个属性时调用
    get(target, propName) {
    console.log(`有人读取了p身上的${propName}属性`)
    return Reflect.get(target, propName)
    },
    // 有人修改 p 的某个属性、或给 p 追加某个属性时调用
    set(target, propName, value) {
    console.log(`有人修改了p身上的${propName}属性,我要去更新界面了!`)
    return Reflect.set(target, propName, value)
    },
    // 有人删除 p 的某个属性时调用
    deleteProperty(target, propName) {
    console.log(`有人删除了p身上的${propName}属性,我要去更新界面了!`)
    return Reflect.deleteProperty(target, propName)
    }
    })

    Proxy 三个拦截器说明:

    拦截器触发时机参数
    get(target, propName) 读取属性 target:源数据(person);propName:属性名
    set(target, propName, value) 修改/新增属性 value:修改的新值
    deleteProperty(target, propName) 删除属性 —

    对比 Vue2 的优势:不再需要像 Vue2 一样遍历被代理对象的各个属性,只需一套 Proxy() 并配置上 get()、set()、deleteProperty() 即可;新增/删除属性、数组下标修改都具备响应式,嵌套对象的数据也都有响应式。

    控制台验证效果:

    let person = { name: '张三', age: 18 }
    const p = new Proxy(person, { /* 上面的三个拦截器 */ })

    p.name = '李四' // 输出:有人修改了p身上的name属性,我要去更新界面了!
    p.sex = '男' // 新增属性同样触发 set
    delete p.name // 输出:有人删除了p身上的name属性,我要去更新界面了!

    3.3.3 reactive 对比 ref
    对比维度refreactive
    定义数据 用来定义基本类型数据;也可以定义对象/数组(内部会自动通过 reactive 转为代理对象) 用来定义对象(或数组)类型数据
    原理 基本类型通过 RefImpl 的 getter/setter 实现数据劫持;对象类型借助 reactive 通过 Proxy 实现数据劫持,并通过 Reflect 操作源对象内部数据
    使用 操作数据需要 .value;模板中读取不需要 .value 操作与读取数据均不需要 .value

    使用建议:基本类型用 ref,对象/数组用 reactive;若 ref 包裹对象,则需注意"整体替换 .value 与修改内部属性的差异"。

    3.4 computed 计算属性

    computed 是组合式 API,需要从 vue 中引入。参数是回调函数(简写)或对象(完整写法,包含 get/set)。与 Vue2 计算属性写法一致,同样有简写形式与完整 get/set 写法;若计算属性会被修改,请写成完整写法。

    <script>
    import { reactive, computed } from 'vue'
    export default {
    name: 'Demo',
    setup() {
    let person = reactive({
    firstName: '张',
    lastName: '三'
    })

    // 计算属性-简写(没有考虑计算属性被修改的情况)
    /* person.fullName = computed(() => {
    return person.firstName + '-' + person.lastName
    }) */

    // 计算属性-完整写法(考虑读和写)
    person.fullName = computed({
    get() {
    return person.firstName + '-' + person.lastName
    },
    set(value) {
    const nameArr = value.split('-')
    person.firstName = nameArr[0]
    person.lastName = nameArr[1]
    }
    })

    return { person }
    }
    }
    </script>

    补充:computed 具备缓存机制,只有依赖的响应式数据变化时才会重新计算;而普通函数每次渲染都会重新执行。

    3.5 watch() 侦听属性

    组合式 API,监听数据是否发生变化,与 Vue2 功能相同,但写法上有一些细节差异。

    import { ref, reactive, watch } from 'vue'
    // 示例数据
    let sum = ref(0)
    let msg = ref('你好啊')
    let person = reactive({
    name: '张三',
    age: 18,
    job: { j1: { salary: 20 } }
    })

    (1)侦听 ref 响应式的单个基本类型数据

    watch(sum, (newValue, oldValue) => {
    console.log('sum变了', newValue, oldValue)
    }, { immediate: true }) // 配置对象:immediate 立即监听 / deep 深度监听

    (2)侦听 ref 响应式的多个数据

    watch([sum, msg], (newValue, oldValue) => {
    console.log('sum或msg变了', newValue, oldValue) // 新值旧值都是数组形式
    }, { immediate: true })

    (3)侦听 reactive 对象的全部属性

    watch(person, (newValue, oldValue) => {
    console.log('person变化了', newValue, oldValue)
    }, { deep: false }) // 注意:此处的 deep 配置无效

    注意:

  • 此处无法正确获取旧值,目前获取到的新旧值相同;
  • 默认开启深度监视,deep: false 无效。
  • (4)侦听 reactive 对象的某个基本类型属性

    // 参数1需要写成回调函数,返回值为需要被侦听的对象属性
    watch(() => person.name, (newValue, oldValue) => {
    console.log('person的name变化了', newValue, oldValue)
    })

    (5)侦听 reactive 对象的多个基本类型属性

    // 参数1需要写成回调函数数组
    watch([() => person.name, () => person.age], (newValue, oldValue) => {
    console.log('person的name或age变化了', newValue, oldValue)
    })

    (6)侦听 reactive 对象的属性为对象(拿不到旧值)

    // 写法一:回调函数,返回 reactive 对象中的某个对象 —— deep 配置有效
    watch(() => person.job, (newValue, oldValue) => {
    console.log('person的job变化了', newValue, oldValue)
    }, { deep: true })

    // 写法二:以 xx.yy 的形式,只需保证 yy 是一个响应式对象即可
    // watch(person.job, (newValue, oldValue) => {
    // console.log('person的job变化了', newValue, oldValue)
    // }, { deep: false }) // 注意:第二种形式仍默认开启深度监视,deep:false 无效

    注:参数 1 如果写成函数写法,则可以有效配置 deep;若采用第二种形式,则仍默认开启深度监视,deep: false 配置无效。

    小细节 —— 侦听 ref 响应式的对象:

    let data = ref({
    a: { b: { c: { counter: 1 } } }
    })

    // 写法一:直接写 ref 对象(本质是 RefImpl 对象)
    // 该写法支持 deep 配置;若需侦听对象内部某个属性的变化,需要手动开启 deep: true
    watch(data, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    }, { deep: true })

    // 写法二:写 .value(本质是 Proxy 对象,即 reactive 对象)
    // 与上述情况(3)一致:默认开启深度监视,无法获取旧值,deep 配置无效
    /* watch(data.value, (newValue, oldValue) => {
    console.log(newValue, oldValue)
    }, { deep: false }) // 拿不到 oldValue,deep 配置无效 */

    小细节汇总:

  • 若侦听的基本类型数据是 ref 响应式的,被侦听的数据不用加 .value;
  • 若侦听的对象是 ref 响应式的:
    • 直接写 ref 对象(RefImpl):支持 deep 配置,默认没有开启深度监视;
    • 写 .value(Proxy/reactive 对象):默认开启深度监视且无法关闭,无法获取旧值。
  • 3.6 watchEffect() 监听某些数据

    watchEffect 接收一个回调函数作为参数,该回调函数中用到了哪些数据,就会监听哪些数据(自动收集依赖);回调函数中用到的数据发生改变时,回调会重新执行,且会立即执行一次。

    watchEffect(() => {
    const x1 = sum.value
    const x2 = person.job.j1.salary
    console.log('watchEffect所指定的回调执行了')
    })

    3.6.1 与 watch() 的区别
    对比watchwatchEffect
    套路 既要指明监视的属性,也要指明监视的回调 不用指明监视哪个属性,回调中用到的属性即被监视
    类似 computed — 有点像 computed,但 computed 注重计算出来的值(必须有返回值);watchEffect 更注重过程(函数体),不用写返回值
    立即执行 需配置 immediate: true 默认立即执行一次

    补充:watchEffect 返回一个停止函数,调用即可停止监听;Vue3.5+ 还提供 watchPostEffect(在组件更新后触发)和 watchSyncEffect(同步触发)。

    3.7 toRef() —— 将响应式对象的某个属性变成响应式

    toRef 接收两个参数:第一个为响应式对象,第二个为对象中的某个属性(属性名,字符串)。

    import { reactive, toRef } from 'vue'

    let person = reactive({
    name: '张三',
    age: 18,
    job: { j1: { salary: 20 } }
    })

    // 在 setup 的 return 中用上该函数:
    // 1. 可以简化模板中的书写(模板里直接写 name 而不是 person.name)
    // 2. 使其数据变为响应式,修改后也会联动使得该对象数据发生响应
    return {
    person,
    name: toRef(person, 'name'), // ✔ 正确写法
    age: toRef(person, 'age'),
    salary: toRef(person.job.j1, 'salary')
    }

    易错点:

    • toRef(person, 'name') 才是正确写法,不是 toRef(person.name);
    • 若使用 ref(person.name),虽然也会使数据变为响应式,但实际上是重新生成了一个 RefImpl 对象,该数据的变化不会影响原对象数据的变化(两者脱钩);
    • 官方文档中 toRef 一般用于 reactive 对象第一层属性;深层属性建议先取到那层对象再 toRef(如 toRef(person.job.j1, 'salary'))。

    3.8 toRefs()

    toRefs 与 toRef 的区别:该函数传入响应式对象作为参数,可以将响应式对象的所有属性与之联动并形成响应式,常配合解构展开使用。

    import { reactive, toRefs } from 'vue'

    let person = reactive({
    name: '张三',
    age: 18,
    job: { j1: { salary: 20 } }
    })

    const x = toRefs(person)
    console.log('******', x)

    // 返回一个对象(常用)
    return {
    person,
    // name: toRef(person, 'name'),
    // age: toRef(person, 'age'),
    // salary: toRef(person.job.j1, 'salary'),
    …toRefs(person) // ✔ 一次性展开所有属性,且保持响应式
    }

    补充:toRefs 常用于解构 props / reactive 对象后仍保持响应式的场景。

    3.9 provide() 与 inject()

    顶层组件(祖)向底层组件(孙)传递数据和方法,实现跨组件通信(无需逐层通过 props 传递)。

    <!– App.vue(顶层组件 / 祖) –>
    <script>
    import { reactive, toRefs, provide } from 'vue'
    import Child from './components/Child.vue'
    export default {
    name: 'App',
    components: { Child },
    setup() {
    let car = reactive({ name: '奔驰', price: '40W' })
    // provide(key, value):两个参数,第一个为自定义 key,第二个为传递的数据
    provide('car', car)
    return { …toRefs(car) }
    }
    }
    </script>

    <!– Son.vue(底层组件 / 孙) –>
    <script>
    import { inject } from 'vue'
    export default {
    name: 'Son',
    setup() {
    // inject(key):参数需与 provide 的第一个参数对应,用变量接收返回值
    let car = inject('car')
    return { car }
    }
    }
    </script>

    <template>
    <h3>我是Son组件(孙),{{car.name}}–{{car.price}}</h3>
    </template>

    传递方法(实现底层组件间接修改顶层组件的数据):

    // 1. 顶层组件提供数据 / 响应式数据 / 方法
    provide('data-key', 'this is room data')
    const count = ref(0)
    provide('count-key', count)
    setTimeout(() => { count.value = 100 }, 3000)

    const setCount = () => { count.value++ }
    provide('setCount-key', setCount)

    // 2. 底层组件接收数据 / 响应式数据 / 方法
    const roomData = inject('data-key')
    const countData = inject('count-key')
    const setCount = inject('setCount-key')

    <button @click="setCount">修改顶层组件的数据</button>

    注:

  • 底层组件接收到的数据一般不被修改,所以顶层组件可以传递方法给底层组件,由底层组件调用方法间接修改顶层组件的数据;
  • 传递 ref/reactive 数据时保持响应式;inject 还可以传入默认值:inject('key', 默认值)。
  • 3.10 判断响应式类型

    import { ref, reactive, toRefs, readonly, isRef, isReactive, isReadonly, isProxy } from 'vue'

    let car = reactive({ name: '奔驰', price: '40W' })
    let sum = ref(0)
    let car2 = readonly(car) // 将 reactive 对象再包一层只读

    console.log(isRef(sum)) // TRUE
    console.log(isReactive(car)) // TRUE
    console.log(isReadonly(car2)) // TRUE
    console.log(isProxy(car)) // TRUE
    console.log(isProxy(sum)) // FALSE(ref 基本类型不是 Proxy)

    API作用
    isRef 检查某个值是否为 ref
    isReactive 检查一个对象是否是由 reactive() 或 shallowReactive() 创建的代理
    isReadonly 检查传入的值是否为只读对象
    isProxy 检查一个对象是否是由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理

    4. 生命周期钩子

    4.1 配置项的形式 —— 在 setup() 之外写

    与 Vue2 的区别之一:原来的 beforeDestroy() 和 destroyed() 变为 beforeUnmount() 和 unmounted()。

    export default {
    name: 'Demo',
    setup() { … },
    // 通过配置项的形式使用生命周期钩子
    beforeCreate() { console.log('—beforeCreate—') },
    created() { console.log('—created—') },
    beforeMount() { console.log('—beforeMount—') },
    mounted() { console.log('—mounted—') },
    beforeUpdate() { console.log('—beforeUpdate—') },
    updated() { console.log('—updated—') },
    beforeUnmount(){ console.log('—beforeUnmount—') },
    unmounted() { console.log('—unmounted—') }
    }

    4.2 组合式 API 的写法 —— 在 setup() 内部写

    需要从 vue 中引入,在函数名之前加上 on;setup() 相当于 beforeCreate() 及 created()。

    import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

    setup() {
    // 在 setup 内部使用组合式生命周期钩子
    onBeforeMount(() => { console.log('—onBeforeMount—') })
    onMounted(() => { console.log('—onMounted—') })
    onBeforeUpdate(() => { console.log('—onBeforeUpdate—') })
    onUpdated(() => { console.log('—onUpdated—') })
    onBeforeUnmount(() => { console.log('—onBeforeUnmount—') })
    onUnmounted(() => { console.log('—onUnmounted—') })
    }

    注:setup() 比 beforeCreate() 先执行的前提是这两个函数是同级写法(即 4.1 的配置项写法)。

    4.3 生命周期对照表

    Vue3.0 提供 Composition API 形式的生命周期钩子,与 Vue2.x 钩子对应关系如下:

    Vue2.x 选项式钩子Vue3.x 组合式钩子
    beforeCreate setup()
    created setup()
    beforeMount onBeforeMount
    mounted onMounted
    beforeUpdate onBeforeUpdate
    updated onUpdated
    beforeUnmount onBeforeUnmount
    unmounted onUnmounted

    补充:还有两个较少用的组合式钩子:

    • onActivated / onDeactivated:配合 <keep-alive> 缓存组件时触发;
    • onErrorCaptured:捕获后代组件传递的错误时触发。

    5. 全局事件总线(mitt)

    Vue3 中移除了 $on、$off、$once 等实例方法,官方推荐使用第三方库 mitt 实现全局事件总线(类似于 Vue2 的 EventBus)。

    5.1 安装

    npm i mitt

    5.2 创建事件总线文件

    创建文件:src/utils/event-bus.js

    import mitt from 'mitt'
    export default mitt()

    5.3 使用 —— 以子组件向父组件传值为例

    原则:谁接收,谁绑定(on);谁传值,谁调用(emit)。

    <!– App.vue(父组件,接收方) –>
    <template>
    <User></User>
    </template>

    <script>
    // 导入全局事件总线对象
    import emitter from './utils/event-bus.js'
    import User from './components/User.vue'
    import { onMounted } from 'vue'
    export default {
    name: 'App',
    components: { User },
    setup() {
    // 生命周期钩子:mounted
    onMounted(() => {
    // 给全局事件总线绑定事件(第二个参数必须传函数!)
    emitter.on('event1', showInfo)
    })
    function showInfo(user) {
    alert(`姓名:${user.name},年龄:${user.age}岁`)
    }
    return { showInfo }
    }
    }
    </script>

    <!– User.vue(子组件,传值方) –>
    <template>
    <button @click="triggerEvent1">触发全局事件总线上的event1事件</button>
    </template>

    <script>
    import emitter from '../utils/event-bus.js'
    export default {
    name: 'User',
    setup() {
    function triggerEvent1() {
    // 触发全局事件总线上的事件 event1 并传参
    emitter.emit('event1', { name: '张三', age: 30 })
    }
    return { triggerEvent1 }
    }
    }
    </script>

    5.4 解绑事件

    // 解绑全部事件
    function clearAllEvent() {
    emitter.all.clear()
    }

    // 解绑指定事件
    function clearEvent1() {
    emitter.off('event1')
    }

    易错点:emitter.on('event1', 'showInfo') 是错误写法 —— mitt 的 on 方法第二个参数必须是回调函数,不能传字符串;应写成 emitter.on('event1', showInfo)。


    6. 其他知识点

    6.1 钩子函数 hook(自定义 hook)

    本质就是代码复用,与 Vue2 中的 mixin 类似,但更灵活、更清晰。

    使用步骤:

  • 在 src/hooks/ 目录下新建 js 文件,以该函数功能命名,例如 sum.js:
  • // src/hooks/sum.js
    import { reactive } from 'vue'

    export default function () {
    // data
    let data = reactive({
    num1: 0,
    num2: 0,
    result: 0
    })
    // methods
    function sum() {
    data.result = data.num1 + data.num2
    }
    // 返回一个对象
    return { data, sum }
    }

  • 在组件中导入并调用:
  • <!– App.vue –>
    <template>
    <div>
    数字2: <input type="number" v-model="data.num2">
    求和结果: {{data.result}}
    <button @click="sum">求和</button>
    </div>
    </template>

    <script>
    import sum from './hooks/sum.js'
    export default {
    name: 'App',
    setup() {
    // 调用钩子函数
    let result = sum()
    // 返回一个对象
    return result
    }
    }
    </script>

    6.2 shallowRef 组合式 API 之一

    • 对于基本数据类型,结果上与 ref() 一样;
    • 对于对象数据类型,使用后该数据的内部属性不具有响应式(不会创建 Proxy 代理对象);
    • 使用场景:当对象中的数据不会改变,或以后要改变时是整体替换对象(data.value = {…}),此时用 shallowRef 效率更高。

    import { ref, shallowRef } from 'vue'
    export default {
    name: 'App',
    setup() {
    // shallowRef 和 ref 用在基本数据类型上,没有区别。
    // 数据本身就是基本数据类型,再浅也浅不到哪儿去。
    // let counter = ref(1)
    // let counter = shallowRef(1)

    // 当 shallowRef 和 ref 都用在对象上的时候:
    // ref 是有响应式的,底层会创建 Proxy 代理对象;
    // shallowRef 是没有响应式的,底层也不会创建 Proxy 对象。
    // 有的时候,这个对象中的属性可能永远都不可能修改,如果要改,也是更换对象。
    // 显然这个时候可以使用 shallowRef,进行优化。
    let data = shallowRef({ counter: 1 })
    console.log(data)

    function changeCounter() {
    // 整体替换对象(触发响应式)
    data.value = { counter: 10000 }
    }
    return { data, changeCounter }
    }
    }

    6.3 shallowReactive 组合式 API 之一

    与 reactive 的区别:shallowReactive 只支持第一层的响应式,深层嵌套的对象没有响应式。

    import { shallowReactive } from 'vue'
    export default {
    name: 'App',
    setup() {
    let data = shallowReactive({
    counter2: 1000, // 第一层,有响应式
    a: { // 深层嵌套
    b: {
    c: { counter: 1 } // 无响应式,修改不会触发更新
    }
    }
    })
    return { data }
    }
    }

    <template>
    <h2>计数器:{{data.a.b.c.counter}}</h2> <!– 深层,不响应 –>
    <button @click="data.a.b.c.counter++">点我加1</button>

    <h2>计数器:{{data.counter2}}</h2> <!– 第一层,响应 –>
    <button @click="data.counter2++">点我加1</button>
    </template>

    6.4 readonly 与 shallowReadonly —— 深只读与浅只读

    将响应式对象再套一层 readonly() 或 shallowReadonly():

    • readonly():深只读,不允许修改该对象中任意层级的数据;
    • shallowReadonly():浅只读,只不允许修改对象第一层的数据。

    应用场景:当其他组件将数据传递过来但不允许被修改时,可以使用 readonly 保护数据。

    import { reactive, readonly, shallowReadonly } from 'vue'

    let car = reactive({ name: '奔驰', price: '40W' })
    let car2 = readonly(car) // 深只读:car2.name / car2.price 都不可修改
    let car3 = shallowReadonly(car) // 浅只读:car3.name 不可修改,嵌套层可修改

    6.5 toRaw 与 markRaw

    API作用
    toRaw 将响应式对象转换为普通对象(只适用于 reactive 生成的响应式对象)
    markRaw 标记某个对象,让这个对象永远都不具备响应式。比如集成第三方库时,有一个巨大的只读列表,不让其具备响应式是一种性能优化

    // toRaw 示例
    import { reactive, toRaw } from 'vue'
    export default {
    name: 'App',
    setup() {
    // data 是具备响应式的
    let data = reactive({ counter1: 1 })

    // 获取 data 这个响应式对象的原始对象
    // 操作原始对象的时候,没有响应式处理的
    function getRawObject() {
    let rawObj = toRaw(data) // rawObj 就是 data 的原始对象
    rawObj.counter1++ // 修改原始对象,不会具有响应式
    console.log('原始对象', rawObj)
    }
    return { data, getRawObject }
    }
    }

    <template>
    <h2>计数器1:{{data.counter1}}</h2>
    <button @click="data.counter1++">计数器加1</button>
    <button @click="getRawObject">获取data的原始对象</button>
    </template>

    6.6 customRef —— 自定义 ref

    customRef 用于创建自定义的 ref 对象,可以显式控制依赖追踪(track)与触发更新(trigger)的时机。

    // 自定义一个防抖 ref(示例)
    import { customRef } from 'vue'

    function useDebouncedRef(value) {
    // 通过调用 customRef 函数来获取一个自定义的 ref 对象
    // 必须给该函数传递一个回调(可称为 factory)
    // 回调有两个重要参数:track 是追踪,trigger 是触发
    const x = customRef((track, trigger) => {
    return {
    // 模板语句中只要使用到该数据,get 会自动调用
    get() {
    console.log('get执行了')
    track() // 告诉 Vue 去追踪这个 value 的变化
    return value
    },
    // 当模板语句中修改该数据的时候,set 会自动调用
    set(newValue) {
    console.log('set执行了', newValue)
    value = newValue
    trigger() // 触发(通知去调用 get 方法,更新界面)
    }
    }
    })
    return x
    }

    // 使用自定义的 ref 同样具备响应式
    let name = useDebouncedRef('test')

    6.7 teleport 组件 —— 将内容传送到指定位置

    <teleport> 将内容传送到 to 属性指定的位置,to 属性可以写 HTML 标签(如 body)或 CSS 选择器(如 #app)。

    <template>
    <teleport to="body">
    <div v-if="isShow" class="mask">
    <div class="dialog">
    <h3>我是一个弹窗</h3>
    <h4>一些内容</h4>
    <h4>一些内容</h4>
    <h4>一些内容</h4>
    <button @click="isShow = false">关闭弹窗</button>
    </div>
    </div>
    </teleport>
    </template>

    应用场景:弹窗、提示框、loading 等需要脱离当前组件层级(如避免被 overflow: hidden / z-index 影响)的内容。

    6.8 defineAsyncComponent —— 动态/异步引入组件

    参数是一个回调函数,需要返回一个 import() 的 Promise。

    <script>
    // import Child from './components/Child' // 静态引入
    import { defineAsyncComponent } from 'vue'

    // 异步引入
    const Child = defineAsyncComponent(() => import('./components/Child'))

    export default {
    name: 'App',
    components: { Child },
    }
    </script>

    静态引入与动态引入的区别:当网速慢、页面加载时——

    • 静态引入:所有组件打包在同一个 bundle 中,会等所有组件都加载完成后一起展示;
    • 动态引入(异步):每个异步组件独立加载,先加载完成的先展示,可加快首屏渲染速度。

    6.9 Suspense 组件

    如果采用异步引入,可能会造成一个问题:当加载过于慢时会导致用户以为页面仅有这些内容。为此可以采用 <Suspense> 组件解决。

    <Suspense> 底层采用的是具名插槽:当组件加载完毕时就展示该组件(default 插槽);当组件还在加载中时展示指定内容(fallback 插槽)。

    <template>
    <div class="app">
    <h3>我是App组件</h3>
    <Suspense>
    <!– 异步组件加载完成后展示的内容 –>
    <template v-slot:default>
    <Child/>
    </template>
    <!– 异步组件加载中展示的内容 –>
    <template v-slot:fallback>
    <h3>稍等,加载中…</h3>
    </template>
    </Suspense>
    </div>
    </template>

    <script>
    // import Child from './components/Child' // 静态引入
    import { defineAsyncComponent } from 'vue'
    const Child = defineAsyncComponent(() => import('./components/Child')) // 异步引入

    export default {
    name: 'App',
    components: { Child },
    }
    </script>

    补充:Suspense 在 Vue 3.x 中长期处于实验性状态(experimental),Vue 3.5+ 中已标记为稳定,但日常开发仍建议优先使用异步组件 + 加载状态组件(如骨架屏)的方案。

    6.10 setup 语法糖(<script setup>)

    原版写计数器:

    <script>
    import { ref } from 'vue'
    export default {
    name: 'App',
    setup() {
    let counter = ref(100)
    function add() {
    counter.value++
    }
    return { counter, add }
    }
    }
    </script>

    <template>
    <h2>计数器: {{counter}}</h2>
    <button @click="add">计数器加1</button>
    </template>

    使用 setup 语法糖写计数器:

    <script setup>
    import { ref } from 'vue'
    // data
    let counter = ref(100)
    // methods
    function add() {
    counter.value++
    }
    // watch
    // computed
    // …
    </script>

    <template>
    <h2>计数器:{{counter}}</h2>
    <button @click="add">计数器加1</button>
    </template>

    语法糖的优势:

    • 无需写 export default {}、setup(){}、return {…};
    • 组件无需 components 注册,只需 import 即可直接使用;
    • 顶层变量/函数自动暴露给模板。

    使用语法糖后如何获取 setup() 中的 props 和 context 参数:

    <script setup>
    // 1. props —— 使用 defineProps 定义组件的 props(自动推断类型;TS 中需显式定义类型)
    const props = defineProps({
    title: String,
    // 其他 props 定义…
    });
    console.log(props.title);

    // 2. emits —— 使用 defineEmits 定义组件可以发出的事件
    const emit = defineEmits(['update:modelValue', 'otherEvent']);
    // 使用 emit
    emit('update:modelValue', newValue);

    // 3. 插槽与属性 —— 使用 useSlots / useAttrs 访问插槽和 attrs
    import { useSlots, useAttrs } from 'vue';
    const slots = useSlots();
    const attrs = useAttrs();
    console.log(slots.default());
    console.log(attrs.id);
    </script>

    6.10.1 父组件向子组件传值(defineProps)

    父组件给子组件传递数据的写法与 Vue2 一致;子组件通过 defineProps({…}) 接收,参数是一个对象,对象中键为属性名,值为数据类型(或详细配置)。该函数返回一个 Proxy 对象,存储父组件传过来的属性名和值,使用时直接写属性名即可。

    <!– App.vue(父组件) –>
    <script setup>
    // setup 语法糖下局部组件无需注册,直接可以使用
    import SonCom from './son-com.vue'
    </script>

    <template>
    <div class="father">
    <h2>父组件App</h2>
    <!– 1. 绑定属性 –>
    <SonCom message="father message"/>
    </div>
    </template>

    <!– son-com.vue(子组件) –>
    <script setup>
    // 2. defineProps 接收数据
    const props = defineProps({
    message: String
    })
    console.log(props) // Proxy {message: 'father message'}
    </script>

    <template>
    <div class="son">
    <h3>子组件Son</h3>
    <div>父组件传入的数据 – {{ message }}</div>
    </div>
    </template>

    为属性设置更多细节(类型校验、默认值、必填等):

    <script setup>
    const props = defineProps({
    names: {
    type: Array,
    default: () => [],
    // required: true, // 是否必传
    // validator: (value) => value.length > 0 // 自定义校验
    }
    });
    </script>

    6.10.2 子组件向父组件传值(defineEmits)

    父组件在子组件标签中绑定自定义事件(写法与 Vue2 一致);子组件通过 defineEmits([…]) 声明事件(参数是数组,元素为事件名),返回函数 emit;通过 emit 在子组件中调用父组件传来的函数,需要传递的数据以参数形式发送到父组件。

    <!– App.vue(父组件) –>
    <script setup>
    import SonCom from './son-com.vue'
    // 参数用于接收子组件传来的数据
    const getMessage = (msg) => {
    console.log(msg)
    }
    </script>

    <template>
    <div class="father">
    <h2>父组件App</h2>
    <!– 1. 绑定事件 –>
    <SonCom @get-message="getMessage" />
    </div>
    </template>

    <!– son-com.vue(子组件) –>
    <script setup>
    // 2. 通过 defineEmits() 声明事件(相当于 Vue2 的 this.$emit)
    const emit = defineEmits(['get-message'])

    const sendMsg = () => {
    // 触发自定义事件,传数据给父组件
    emit('get-message', 'this is son message')
    }
    </script>

    <template>
    <div class="son">
    <div>子组件Son</div>
    <button @click="sendMsg">触发自定义事件</button>
    </div>
    </template>

    6.11 ref 属性 —— 模板引用

    思想与 Vue2 的 ref 属性类似,用于获取 DOM 元素或子组件实例。

    <script setup>
    import { ref, onMounted } from 'vue'
    import TestCom from './test-com.vue'

    // 1. 调用 ref 函数 -> ref 对象
    const h1Ref = ref(null)
    const comRef = ref(null)

    // 组件挂载完毕之后才能获取
    onMounted(() => {
    console.log(h1Ref.value) // DOM 元素
    console.log(comRef.value) // 子组件实例
    })
    </script>

    <template>
    <!– 2. 通过 ref 标识绑定 ref 对象 –>
    <h1 ref="h1Ref">我是dom标签h1</h1>
    <TestCom ref="comRef" />
    </template>

    注:模板实例需要在组件挂载完毕(onMounted)后才能获取到。

    setup 语法糖下获取子组件内部数据 —— defineExpose:

    在 <script setup> 语法糖下,通过 ref 获取到组件对象后,该组件内部的数据是默认不开放给父组件的;若想获取子组件的数据,需要在子组件中使用 defineExpose({…}) 将需要暴露的值写上。

    <!– test-com.vue(子组件) –>
    <script setup>
    import { ref } from 'vue'
    const name = ref('test name')
    const setName = () => {
    name.value = 'test new name'
    }
    // 通过该函数把组件内部的值暴露给父组件
    defineExpose({
    name,
    setName
    })
    </script>

    <template>
    <div>我是test组件</div>
    </template>

    补充(Vue 3.5+):新写法 useTemplateRef('h1Ref') 可以替代"ref + 同名变量"的繁琐声明,模板中 ref="h1Ref" 与 const h1Ref = useTemplateRef('h1Ref') 自动关联。

    6.12 数据共享仓库:Pinia

    Pinia 是 Vue 的专属最新状态管理库,是 Vuex 的替代品。

    优势:

    • 提供更加简单的 API(去掉了 mutation);
    • 提供组合式风格的 API,与 Vue3 新语法统一;
    • 去掉了 modules 的概念,每一个 store 都是一个独立的模块;
    • 搭配 TypeScript 一起使用提供可靠的类型推断。

    安装与配置:

    # 创建项目、安装依赖(npm i)后:
    npm install pinia

    // main.js
    import { createApp } from 'vue'
    import App from './App.vue'
    import { createPinia } from 'pinia' // 1. 导入 createPinia

    const pinia = createPinia() // 2. 执行方法得到实例

    createApp(App)
    .use(pinia) // 3. 把 pinia 实例加入到 app 应用中
    .mount('#app')

    定义 store(组合式写法,推荐):

    创建 src/stores/counter.js:

    import { defineStore } from 'pinia'
    import { ref, computed } from 'vue'

    export const useCounterStore = defineStore('counter', () => {
    // 定义数据 (state)
    const count = ref(0)
    // 定义修改数据的方法 (action 同步+异步)
    const increment = () => {
    count.value++
    }
    // 定义 getter(实际运用 computed)
    const doubleCount = computed(() => count.value * 2)
    // 以对象的方式 return 供组件使用
    return { count, increment, doubleCount }
    })

    在组件中使用:

    <script setup>
    // 1. 导入 use 打头的方法
    import { useCounterStore } from '@/stores/counter'
    // 2. 执行方法得到 store 实例对象
    const counterStore = useCounterStore()
    console.log(counterStore)
    </script>

    <template>
    <button @click="counterStore.increment">{{ counterStore.count }}</button>
    </template>

    getter 与异步 action:

    // store 中
    const doubleCount = computed(() => count.value * 2) // getter
    const list = ref([])
    const getList = async () => {
    const res = await axios.get(API_URL)
    list.value = res.data.data.channels
    }

    <script setup>
    import { useCounterStore } from '@/store'
    import { onMounted } from 'vue'
    const counterStore = useCounterStore()
    onMounted(() => { counterStore.getList() })
    </script>

    <template>
    <button @click="counterStore.increment">{{ counterStore.doubleCount }}</button>
    <ul>
    <li v-for="item in counterStore.list">{{ item }}</li>
    </ul>
    </template>

    响应式丢失问题与 storeToRefs:

    ⚠️ 若数据运用解构赋值会使得响应式丢失!!!方法解构则无碍!

    <script setup>
    import { useCounterStore } from '@/stores/counter'
    import { onMounted } from 'vue'
    import { storeToRefs } from 'pinia'

    const counterStore = useCounterStore()
    console.log(counterStore)

    // ❌ 直接解构赋值(响应式丢失)
    // const { count, doubleCount } = counterStore

    // ✔ 方法包裹(保持响应式更新)
    const { count, doubleCount } = storeToRefs(counterStore)

    // 触发 action
    onMounted(() => { counterStore.getList() })
    </script>

    <template>
    <button @click="counterStore.increment">{{ count }}</button>
    {{ doubleCount }}
    </template>

    注:方法直接解构赋值即可(方法本身没有响应式概念),如 const { increment } = counterStore;只有 state / getter 数据需要用 storeToRefs 包裹。

    6.13 路由(vue-router)

    6.13.1 获取路由的 query 参数

    import { useRoute, useRouter } from 'vue-router';

    export default {
    setup() {
    const route = useRoute(); // 当前路由实例(相当于 this.$route)
    const router = useRouter(); // 路由器实例(相当于 this.$router)

    const userId = route.query.id;
    const userName = route.query.name;
    console.log(userId); // 输出查询参数 id
    console.log(userName); // 输出查询参数 name

    // 也可以使用 router.currentRoute.value.query
    // const userId = router.currentRoute.value.query.id;
    // const userName = router.currentRoute.value.query.name;

    return {};
    }
    }

    使用场景:router.push({ path: '/user', query: { id: 1, name: '张三' } }) 跳转后通过 route.query 读取。

    6.13.2 获取路由的 params 参数

    动态路由配置:

    {
    path: '/user/:id',
    component: UserComponent
    }

    组件中获取参数:

    import { useRoute } from 'vue-router';

    export default {
    setup() {
    const route = useRoute();
    const userId = route.params.id;
    return { userId };
    }
    }

    使用场景:router.push({ path: '/user/123' }) 跳转后通过 route.params.id 读取 123。

    6.13.3 动态路由(不同账户登录后有不同的权限路由)

    【1】权限校验(账户密码)—— 采用 Element Plus 中的 <el-form> 组件绘制登录页面

    <el-form> 组件主要需要以下三个属性:

    • ref:用于绑定该组件实例;
    • :model:用于绑定数据对象;
    • :rules:用于绑定校验规则。

    <el-form> 下的子组件:

    • <el-form-item>:主要需要 prop 属性,用于绑定规则校验的字段名;
    • <el-input>:主要需要指令 v-model,用于绑定对应的数据对象中的具体数据;
    • <el-button>:用于提交,主要写一个点击事件,该事件参数为 el-form 组件中 ref 属性绑定的值。

    <template>
    <el-form ref="ruleFormRef" :model="temp" :rules="rules" label-width="120px" class="demo-ruleForm">
    <el-form-item label="用户名" prop="account">
    <el-input v-model="temp.account"/>
    </el-form-item>
    <el-form-item label="密码" prop="password">
    <el-input type="password" v-model="temp.password"/>
    </el-form-item>
    <el-form-item>
    <el-button type="success" @click="submit(ruleFormRef)">登录</el-button>
    </el-form-item>
    </el-form>
    </template>

    数据对象与校验规则:

    // 初始化表单 数据对象
    const temp = reactive({
    account: '',
    password: ''
    })

    // 表单校验
    const rules = reactive({
    account: [
    { required: true, message: '请输入用户名', trigger: 'blur' },
    { min: 2, max: 18, message: '长度在2-18个字符', trigger: 'blur' },
    ],
    password: [
    { required: true, message: '请输入密码', trigger: 'blur' },
    { min: 3, max: 18, message: '长度在3-18个字符', trigger: 'blur' },
    ]
    })

    登录提交:

    const submit = (formEl) => {
    if (!formEl) return;
    formEl.validate(async valid => {
    if (valid) { // 校验成功
    // 发送请求
    let { data: { uid, token, msg } } = await login(temp);
    if (uid) {
    // token 缓存
    setToken(token);
    // 跳转
    router.push('/home')
    }
    } else { // 失败
    console.log('error submit!')
    }
    })
    }

    【2】根据 token 发请求 —— 对 axios 的封装

    ① 初次封装:新建 src/http/index.js,创建 axios 实例(包含 baseURL、timeout 配置)、请求拦截器(将登录后获取的 token 设置在请求头 header 中)、响应拦截器(对返回的状态码进行对应的处理)。

    import axios from 'axios'
    import { getToken } from '../utils/auth'

    // 处理访问的 url
    const service = axios.create({
    baseURL: "http://127.0.0.1:7003/",
    timeout: 1000
    })

    // 请求拦截器 — 在发送请求之前做的事
    service.interceptors.request.use(config => {
    config.headers['token'] = getToken();
    return config;
    })

    // 响应拦截器 — 对响应数据的处理
    service.interceptors.response.use(response => {
    return response
    }, err => {
    return Promise.reject(err);
    })

    export default service;

    ② 二次封装:在 src/http/http.js 中封装对应后端接口的请求方法。

    import axios from '.'

    // 登录
    export const login = data => axios.post(`itlove/login`, data);

    // 获取 getResources(权限资源)
    export const getResources = () => axios.get(`getResources`);

    ③ 调用请求接口的方法:对应登录接口,账户密码正确后会拿到 token,将 token 进行本地存储;在请求拦截器中从本地获取 token 添加到请求头 headers 中。登录对应账户后,后端根据账户权限返回对应的路由信息(该信息可通过后端接口获取)。

    【3】处理获取到的权限路由数据

    ④ 将获取到的权限路由数据存到 Pinia 中,在路由配置文件中设置路由守卫 beforeEach:

    • 每当发生路由跳转时判断 Pinia 是否有数据:有则代表已将该账户的权限数据缓存,直接放行(避免不断点击路由重复发请求);
    • 没有则代表数据没缓存,发起获取权限数据的接口,并存放到 Pinia 中;
    • 拿到权限数据后,将该数据处理成路由格式({path: xxx, name: xxx, component: () => import(xxx), children: xxx}),传给 router.addRoute()。

    // 路由守卫(router/index.js)
    router.beforeEach(async (to, from, next) => {
    if (to.path === '/login') {
    next();
    } else {
    if (mainStore() && mainStore().nav.length === 0) { // 没有缓存
    // 1、发送请求
    const { data: res } = await getResources();
    // 2、缓存数据
    mainStore().setNav(res);
    // 3、服务端返回的数据转换为路由数据
    const dynamicRoutes = addDynamicRoutes(res);
    // 4、动态路由配置,传入路由格式的数据
    router.addRoute(dynamicRoutes)
    next({ path: to.path });
    } else {
    next();
    }
    }
    })

    // 将后端返回的权限数据转成路由配置
    function addDynamicRoutes(res) {
    // 1、home 添加 children
    const homeRoutes = routes.filter(v => v.path === '/home')[0];
    homeRoutes.children = [];
    // 2、追加配置
    res.forEach(item => {
    homeRoutes.children.push({
    path: item.path,
    name: item.name,
    component: () => import(`../views/home/content/${item.component}/index.vue`)
    })
    })
    return homeRoutes;
    }

    // store/index.js(Pinia 缓存权限数据)
    import { defineStore } from 'pinia'

    export const mainStore = defineStore('main', {
    state: () => ({
    nav: []
    }),
    actions: {
    setNav(v) {
    this.nav = v;
    }
    }
    })

    【4】退出登录

    需要清空 Pinia 以及 token,避免下一次请求时返回的是上一次的数据。

    function logout() {
    // 清空 pinia 数据
    mainStore().$reset()
    // 清空 token
    removeToken()
    // 跳转登录页
    router.push('/login')
    }


    6.14 插槽(Slot)

    6.14.1 默认插槽

    <!– A.vue(子组件) –>
    <template>
    <div>
    <h1>子:A组件</h1>
    <header>
    <div>头部</div>
    <slot></slot> <!– 默认插槽(匿名插槽) –>
    </header>
    <footer>
    <div>底部</div>
    <slot></slot>
    </footer>
    </div>
    </template>

    <!– Home.vue(父组件) –>
    <script setup>
    import A from '../components/A.vue'
    </script>

    <template>
    <div>
    <!– 标签体内容会填充到子组件的 <slot></slot> 位置 –>
    <A>这是xxxx数据</A>
    </div>
    </template>

    6.14.2 具名插槽

    <!– A.vue(子组件) –>
    <template>
    <div>
    <h1>子:A组件</h1>
    <header>
    <div>头部</div>
    <slot name='xxx'></slot> <!– 具名插槽 xxx –>
    </header>
    <footer>
    <div>底部</div>
    <slot name='yyy'></slot> <!– 具名插槽 yyy –>
    </footer>
    </div>
    </template>

    <!– Home.vue(父组件) –>
    <script setup>
    import A from '../components/A.vue'
    </script>

    <template>
    <div>
    <A>
    <!– 完整写法:v-slot:插槽名 –>
    <template v-slot:xxx>
    这是xxxxx数据
    </template>
    <template v-slot:yyy>
    这是yyyyy数据
    </template>
    </A>
    </div>
    </template>

    简写:v-slot:xxx 简写为 #xxx

    <template>
    <div>
    <A>
    <template #xxx>这是xxxx数据</template>
    <template #yyy>这是yyyyy数据</template>
    </A>
    </div>
    </template>

    6.14.3 作用域插槽

    子组件通过插槽向父组件传数据,父组件在插槽内容中接收。

    <!– A.vue(子组件) –>
    <script setup>
    import { ref } from 'vue'
    let list = ref([
    { id: 1, name: '张三', age: 18 },
    { id: 2, name: '张大三', age: 22 },
    { id: 3, name: '张小三', age: 33 },
    { id: 4, name: '里斯', age: 55 },
    ]);
    </script>

    <template>
    <div>
    <header>
    <slot name='xxx'></slot>
    <slot name='yyy'></slot>
    </header>
    <section>
    <div>中间</div>
    <!– 作用域插槽:通过 :data='item' 把数据传给插槽使用者 –>
    <div v-for='item in list' :key='item.id'>
    <slot :data='item'></slot>
    </div>
    </section>
    <footer>
    <div>底部</div>
    <slot name='xxx'></slot>
    </footer>
    </div>
    </template>

    <!– Home.vue(父组件) –>
    <script setup>
    import A from '../components/A.vue'
    </script>

    <template>
    <div>
    <A>
    <template #xxx>这是xxxxx数据</template>
    <!– v-slot='{data}' 解构接收子组件传出的数据 –>
    <template v-slot='{data}'>
    {{ data.name }} –> {{ data.age }}
    </template>
    <template #yyy>这是yyyyy数据</template>
    </A>
    </div>
    </template>

    简写: 默认插槽的作用域写法也可写作 #default='{data}'(显式指名默认插槽):

    <template>
    <div>
    <A>
    <template #xxx>这是xxxxx数据</template>
    <template #default='{data}'>
    {{ data.name }} –> {{ data.age }}
    </template>
    <template #yyy>这是yyyyy数据</template>
    </A>
    </div>
    </template>

    6.14.4 动态插槽

    插槽名可以是动态的(由变量控制)。

    <!– Home.vue(父组件) –>
    <script setup>
    import A from '../components/A.vue'
    // 可动态更改 str 的值来切换插槽
    let str = ref('xxx');
    </script>

    <template>
    <div>
    <A>
    <!– 动态插槽名:#[str] –>
    <template #[str]>这是xxxxx数据</template>
    <template #default='{data}'>
    {{ data.name }} –> {{ data.age }}
    </template>
    <template #yyy>这是yyyyy数据</template>
    </A>
    </div>
    </template>


    6.15 插件(Plugin)

    插件是自包含的代码,通常用来为 Vue 添加全局功能。Vue3 中有以下几种常见写法。

    6.15.1 写法 1:install()

    ① 在 js 文件中定义插件:

    // plugin_libs/plug_1.js
    // 第一个插件
    export default {
    install: (app, options) => {
    // 添加全局实例的方法
    app.config.globalProperties.$xxplug = key => {
    // 插件方法的业务逻辑,就可以写在这里
    console.log('第一个插件')
    }
    }
    }

    app 参数即 app 实例,options 参数即 main.js 中 .use() 该插件所传入的第二个参数。

    ② 在 main.js 中引入并使用该插件(可传入参数):

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    // 插件
    import plug_1 from './plugin_libs/plug_1'

    createApp(App)
    .use(plug_1, '第一个插件,main.js 调用的,.use方法的第二个参数')
    .use(store)
    .use(router)
    .mount('#app')

    ③ 在组件中使用:

    import { getCurrentInstance } from 'vue'
    export default {
    name: 'HelloWorld',
    props: { msg: String },
    setup() {
    // proxy 可以在开发环境、生产环境下都能拿到组件实例(推荐)
    const { proxy } = getCurrentInstance();
    proxy.$xxplug('我是老尚,欢迎关注一对一零基础课')

    // ctx 只在开发环境好用,生产环境会报错,不推荐使用
    // const { ctx } = getCurrentInstance();
    // ctx.$xxplug('…')
    }
    }

    注:ctx(即 getCurrentInstance().ctx)只在开发环境有用,在生产环境下会报错,因此直接解构 proxy 代替 ctx。

    6.15.2 写法 2:将该插件文件作为函数导出

    ① 在 js 文件中导出一个函数:

    // plugin_libs/plug_2.js
    export function plugFn(key) {
    console.log('第二个插件:' + key)
    }

    ② 在组件中导入并调用该函数即可:

    import { getCurrentInstance } from 'vue'
    // 插件
    import { plugFn } from '../plugin_libs/plug_2'

    export default {
    setup() {
    plugFn('我是老尚') // 直接调用
    }
    }

    6.15.3 写法 3:通过 provide 和 inject

    ① 在插件文件中定义插件方法并对外暴露两个方法:

    // plugin_libs/plug_3.js
    import { provide, inject } from 'vue'

    // 私有方法
    const _fn = key => {
    console.log('第三个插件')
    }

    // 向后代组件传
    export function provideFn() {
    provide('onlyId', _fn);
    }

    // 子组件,从父组件接收
    export function injectFn() {
    return inject('onlyId')
    }

    ② 在父组件引入并调用 provideFn:

    // App.vue
    import { provideFn } from './plugin_libs/plug_3'
    export default {
    setup() {
    provideFn(); // 向所有后代组件提供 _fn 方法
    }
    }

    ③ 在子组件引入并调用 injectFn:

    // children.vue(子组件)
    import { injectFn } from '../plugin_libs/plug_3'
    export default {
    name: 'children',
    setup() {
    let msg = '子组件';
    // 所有的子组件,都能拿到 _fn 方法
    const fns = injectFn();
    fns('第三个插件,我是老尚')
    return { msg }
    }
    }

    <template>
    <div><h1>{{msg}}</h1></div>
    </template>


    7. Vue3 的其他改变

    7.1 全局 API 调整

    Vue3.0 中对这些 API 做出了调整:将全局的 API(即 Vue.xxx)调整到应用实例(app)上:

    2.x 全局 API(Vue)3.x 实例 API(app)
    Vue.config.xxxx app.config.xxxx
    Vue.config.productionTip 移除
    Vue.component app.component
    Vue.directive app.directive
    Vue.mixin app.mixin
    Vue.use app.use
    Vue.prototype app.config.globalProperties

    说明:Vue.config.productionTip(生产提示)在 3.x 中被移除;Vue.prototype 改为 app.config.globalProperties。这也让 Vue3 的 API 对 tree-shaking 更友好(按需打包)。

    7.2 其他破坏性变更

    (1)data 选项应始终被声明为一个函数。(与 Vue2 一致,但必须写成函数形式,防止多个组件实例共享同一份数据)

    (2)过渡类名的更改:

    /* Vue2.x 写法 */
    .v-enter,
    .v-leave-to
    {
    opacity: 0;
    }
    .v-leave,
    .v-enter-to
    {
    opacity: 1;
    }

    /* Vue3.x 写法:v-enter 更名为 v-enter-from,v-leave 更名为 v-leave-from */
    .v-enter-from,
    .v-leave-to
    {
    opacity: 0;
    }
    .v-leave-from,
    .v-enter-to
    {
    opacity: 1;
    }

    (3)移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes。(Vue3 中应使用事件名而非按键码,如 @keyup.enter 可保留,但 @keyup.13 不再支持)

    (4)移除 v-on.native 修饰符:

    子组件的 emits 数组中未指定的事件(如 click),会被识别为原生事件,父组件直接 @click 绑定即可,无需再使用 .native 修饰符:

    <!– 父组件中绑定事件 –>
    <my-component
    v-on:close="handleComponentEvent"
    v-on:click="handleNativeClickEvent"
    />

    // 子组件中声明自定义事件
    export default {
    emits: ['close'] // click 没在 emits 中指定,则代表原生事件
    }

    (5)移除过滤器(filter):

    过滤器虽然看起来很方便,但它需要一种自定义语法,打破了大括号内表达式"只是 JavaScript"的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

    // Vue2: {{ message | capitalize }}
    // Vue3 建议:
    // 方法调用:{{ capitalize(message) }}
    // 计算属性:computed(() => capitalize(message))

    (6)其他移除/变更(补充):

    • 移除实例方法 $on、$off、$once、$children(事件总线需借助 mitt 等第三方库);
    • v-if 与 v-for 同时用于同一元素时,v-if 的优先级更高(Vue2 中 v-for 优先);
    • 全局注册组件由 Vue.component 改为 app.component;
    • filters、keyCode、.native 等语法移除(如上)。

    8. 补充:Vue3 常用特性速览

    8.1 组件通信方式总结

    通信场景方式说明
    父 → 子 props 通过 defineProps 接收(语法糖)或 setup(props)
    子 → 父 自定义事件 emit 通过 defineEmits 声明并触发(语法糖)或 context.emit
    父 ↔ 子(双向) v-model 见 8.2,也可自定义 update:xxx 事件
    任意组件 全局事件总线 mitt(Vue3 移除了 $on/$off)
    祖 → 孙 provide / inject 跨层级通信,无需逐层传 props
    任意组件 Pinia 官方状态管理库,替代 Vuex
    页面间 vue-router 传参 query / params
    子组件向插槽传数据 作用域插槽 <slot :data="item"> + 父组件 v-slot="{data}"
    父组件访问子组件 ref + defineExpose 获取子组件实例及其暴露的数据/方法

    8.2 v-model 与 defineModel

    组件上使用 v-model(Vue3):

    Vue3 中 v-model 等价于 :modelValue + @update:modelValue:

    <!– 父组件 –>
    <Child v-model="count" />
    <!– 等价于 –>
    <Child :modelValue="count" @update:modelValue="count = $event" />

    <!– 子组件(Vue3.3 及以前) –>
    <script setup>
    const props = defineProps(['modelValue'])
    const emit = defineEmits(['update:modelValue'])
    </script>

    多个 v-model:

    <Child v-model:title="title" v-model:content="content" />

    Vue 3.4+ 的 defineModel() 宏(简化写法):

    <script setup>
    // 定义一个 v-model 绑定,返回一个 ref
    const model = defineModel()
    // 也可以带默认值与校验
    // const model = defineModel({ default: '' })
    </script>

    <template>
    <input v-model="model" />
    </template>

    8.3 模板语法与指令要点

    (1)v-if 与 v-for 优先级:Vue3 中同一元素上 v-if 优先于 v-for(与 Vue2 相反),因此不要在同一元素上同时使用,建议用 v-for 包裹一层或改用 v-show。

    (2)key 的使用:v-for 中务必使用 key;Vue3 中 key 不再自动生成在 v-bind 上。

    (3)事件修饰符:.stop、.prevent、.once、.capture、.self、.passive 等与 Vue2 一致;v-on 可直接缩写为 @。

    (4)v-model 修饰符:.lazy、.number、.trim 保留。

    (5)v-bind 合并规则变化:Vue3 中 class / style 的合并更智能;动态参数也支持 v-bind:[attrName]。

    (6)<script setup> 顶层 await:配合 Suspense 使用,组件可写成 async。

    8.4 其他新特性速览(补充)

    特性版本说明
    defineModel() Vue 3.4+ 简化组件 v-model 的双向绑定
    useTemplateRef() Vue 3.5+ 更简洁的模板引用声明方式
    watchPostEffect / watchSyncEffect Vue 3.5+ 指定时机执行的 watchEffect 变体
    onWatcherCleanup() Vue 3.5+ 在侦听器回调中注册清理函数
    useId() Vue 3.5+ 生成唯一 id(用于表单标签关联等)
    data() 选项 — Vue3 中仍然支持选项式 API,但组件须为函数
    Fragments(多根节点) — 模板不再强制要求单一根元素
    Teleport — 将内容传送到指定 DOM 位置(见 6.7)
    Suspense — 异步组件加载占位(见 6.9)

    赞(0)
    未经允许不得转载:171主机测评 » vue3笔记
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址