vue3笔记

Vue3 笔记


1. 项目的创建与运行

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

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

1.2 基于 Vite 构建工具 ------ Vite

bash 复制代码
# 创建项目
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 脚手架【推荐】

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

2. 目录结构

2.1 main.js 入口文件

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') */

注:

  1. Vue3 中创建应用使用 createApp() 工厂函数,不再需要 new Vue();
  2. 其他文件与 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。
js 复制代码
<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 参数

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

html 复制代码
<!-- 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>
html 复制代码
<!-- 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>

注:

  1. props 会被包装成响应式的 Proxy 对象,不需要 return 出去即可在模板中使用;
  2. props 是只读的,子组件不应修改 props(数据流是单向的);
  3. 对 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() 触发事件,第一个参数为父组件在子组件标签上绑定的事件名,后续参数为父组件事件处理函数所需的参数。

html 复制代码
<!-- 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>
html 复制代码
<!-- 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

js 复制代码
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(模板会自动解包)。

js 复制代码
<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 实现响应式,嵌套对象都有响应式。
js 复制代码
// 对象类型会被包装成 RefImpl,其 .value 是 reactive 产生的 Proxy 代理对象
let job = ref({
  type: '前端工程师',
  salary: '30K'
})
// 修改时:
job.value.type = 'UI设计师'
job.value.salary = '60K'
js 复制代码
// 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 对象。

js 复制代码
<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 重写了数组方法使其具有响应式)。
js 复制代码
// 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):

js 复制代码
// 源数据
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 的响应式原理:

js 复制代码
// 模拟 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() 即可;新增/删除属性、数组下标修改都具备响应式,嵌套对象的数据也都有响应式。

控制台验证效果:

js 复制代码
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
对比维度 ref reactive
定义数据 用来定义基本类型数据;也可以定义对象/数组(内部会自动通过 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 写法;若计算属性会被修改,请写成完整写法。

js 复制代码
<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 功能相同,但写法上有一些细节差异。

js 复制代码
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 响应式的单个基本类型数据

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

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

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

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

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

注意:

  1. 此处无法正确获取旧值,目前获取到的新旧值相同;
  2. 默认开启深度监视 ,deep: false 无效。

(4)侦听 reactive 对象的某个基本类型属性

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

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

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

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

js 复制代码
// 写法一:回调函数,返回 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 响应式的对象:

js 复制代码
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 配置无效 */

小细节汇总:

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

3.6 watchEffect() 监听某些数据

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

js 复制代码
watchEffect(() => {
  const x1 = sum.value
  const x2 = person.job.j1.salary
  console.log('watchEffect所指定的回调执行了')
})
3.6.1 与 watch() 的区别
对比 watch watchEffect
套路 既要指明监视的属性 ,也要指明监视的回调 不用指明监视哪个属性,回调中用到的属性即被监视
类似 computed --- 有点像 computed,但 computed 注重计算出来的值 (必须有返回值);watchEffect 更注重过程(函数体),不用写返回值
立即执行 需配置 immediate: true 默认立即执行一次

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

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

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

js 复制代码
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 的区别:该函数传入响应式对象作为参数,可以将响应式对象的所有属性与之联动并形成响应式,常配合解构展开使用。

js 复制代码
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 传递)。

html 复制代码
<!-- 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>
html 复制代码
<!-- Son.vue(底层组件 / 孙) -->
<script>
import { inject } from 'vue'
export default {
  name: 'Son',
  setup() {
    // inject(key):参数需与 provide 的第一个参数对应,用变量接收返回值
    let car = inject('car')
    return { car }
  }
}
</script>
html 复制代码
<template>
  <h3>我是Son组件(孙),{{car.name}}--{{car.price}}</h3>
</template>

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

js 复制代码
// 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)
js 复制代码
// 2. 底层组件接收数据 / 响应式数据 / 方法
const roomData = inject('data-key')
const countData = inject('count-key')
const setCount = inject('setCount-key')
html 复制代码
<button @click="setCount">修改顶层组件的数据</button>

注:

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

3.10 判断响应式类型

js 复制代码
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()。

js 复制代码
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()。

js 复制代码
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 安装

bash 复制代码
npm i mitt

5.2 创建事件总线文件

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

js 复制代码
import mitt from 'mitt'
export default mitt()

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

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

html 复制代码
<!-- 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>
html 复制代码
<!-- 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 解绑事件

js 复制代码
// 解绑全部事件
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 类似,但更灵活、更清晰。

使用步骤:

  1. 在 src/hooks/ 目录下新建 js 文件,以该函数功能命名,例如 sum.js:
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 }
}
  1. 在组件中导入并调用:
html 复制代码
<!-- 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 效率更高。
js 复制代码
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 只支持第一层的响应式,深层嵌套的对象没有响应式。

js 复制代码
import { shallowReactive } from 'vue'
export default {
  name: 'App',
  setup() {
    let data = shallowReactive({
      counter2: 1000,      // 第一层,有响应式
      a: {                 // 深层嵌套
        b: {
          c: { counter: 1 }  // 无响应式,修改不会触发更新
        }
      }
    })
    return { data }
  }
}
html 复制代码
<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 保护数据。

js 复制代码
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 标记某个对象,让这个对象永远都不具备响应式。比如集成第三方库时,有一个巨大的只读列表,不让其具备响应式是一种性能优化
js 复制代码
// 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 }
  }
}
html 复制代码
<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)的时机。

js 复制代码
// 自定义一个防抖 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)。

html 复制代码
<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。

js 复制代码
<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 插槽)。

html 复制代码
<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>)

原版写计数器:

vue 复制代码
<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 语法糖写计数器:

vue 复制代码
<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 参数:

vue 复制代码
<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 对象,存储父组件传过来的属性名和值,使用时直接写属性名即可。

vue 复制代码
<!-- 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>
vue 复制代码
<!-- 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>

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

vue 复制代码
<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 在子组件中调用父组件传来的函数,需要传递的数据以参数形式发送到父组件。

vue 复制代码
<!-- 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>
vue 复制代码
<!-- 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 元素或子组件实例。

vue 复制代码
<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({...}) 将需要暴露的值写上。

vue 复制代码
<!-- 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 一起使用提供可靠的类型推断。

安装与配置:

bash 复制代码
# 创建项目、安装依赖(npm i)后:
npm install pinia
js 复制代码
// 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:

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 }
})

在组件中使用:

vue 复制代码
<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:

js 复制代码
// 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
}
vue 复制代码
<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:

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

vue 复制代码
<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 参数
js 复制代码
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 参数

动态路由配置:

js 复制代码
{
  path: '/user/:id',
  component: UserComponent
}

组件中获取参数:

js 复制代码
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 属性绑定的值。
html 复制代码
<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>

数据对象与校验规则:

js 复制代码
// 初始化表单 数据对象
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' },
  ]
})

登录提交:

js 复制代码
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 中)、响应拦截器(对返回的状态码进行对应的处理)。

js 复制代码
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 中封装对应后端接口的请求方法。

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()。
js 复制代码
// 路由守卫(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();
    }
  }
})
js 复制代码
// 将后端返回的权限数据转成路由配置
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;
}
js 复制代码
// store/index.js(Pinia 缓存权限数据)
import { defineStore } from 'pinia'

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

【4】退出登录

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

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

6.14 插槽(Slot)

6.14.1 默认插槽
vue 复制代码
<!-- A.vue(子组件) -->
<template>
  <div>
    <h1>子:A组件</h1>
    <header>
      <div>头部</div>
      <slot></slot>      <!-- 默认插槽(匿名插槽) -->
    </header>
    <footer>
      <div>底部</div>
      <slot></slot>
    </footer>
  </div>
</template>
vue 复制代码
<!-- Home.vue(父组件) -->
<script setup>
import A from '../components/A.vue'
</script>

<template>
  <div>
    <!-- 标签体内容会填充到子组件的 <slot></slot> 位置 -->
    <A>这是xxxx数据</A>
  </div>
</template>
6.14.2 具名插槽
vue 复制代码
<!-- 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>
vue 复制代码
<!-- 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

vue 复制代码
<template>
  <div>
    <A>
      <template #xxx>这是xxxx数据</template>
      <template #yyy>这是yyyyy数据</template>
    </A>
  </div>
</template>
6.14.3 作用域插槽

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

vue 复制代码
<!-- 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>
vue 复制代码
<!-- 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}'(显式指名默认插槽):

vue 复制代码
<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 动态插槽

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

vue 复制代码
<!-- 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 文件中定义插件:

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 中引入并使用该插件(可传入参数):

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')

③ 在组件中使用:

js 复制代码
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 文件中导出一个函数:

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

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

js 复制代码
import { getCurrentInstance } from 'vue'
// 插件
import { plugFn } from '../plugin_libs/plug_2'

export default {
  setup() {
    plugFn('我是老尚')   // 直接调用
  }
}
6.15.3 写法 3:通过 provide 和 inject

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

js 复制代码
// 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:

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

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

js 复制代码
// children.vue(子组件)
import { injectFn } from '../plugin_libs/plug_3'
export default {
  name: 'children',
  setup() {
    let msg = '子组件';
    // 所有的子组件,都能拿到 _fn 方法
    const fns = injectFn();
    fns('第三个插件,我是老尚')
    return { msg }
  }
}
html 复制代码
<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)过渡类名的更改:

css 复制代码
/* Vue2.x 写法 */
.v-enter,
.v-leave-to {
  opacity: 0;
}
.v-leave,
.v-enter-to {
  opacity: 1;
}
css 复制代码
/* 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 修饰符:

html 复制代码
<!-- 父组件中绑定事件 -->
<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
js 复制代码
// 子组件中声明自定义事件
export default {
  emits: ['close']   // click 没在 emits 中指定,则代表原生事件
}

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

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

js 复制代码
// 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:

vue 复制代码
<!-- 父组件 -->
<Child v-model="count" />
<!-- 等价于 -->
<Child :modelValue="count" @update:modelValue="count = $event" />
vue 复制代码
<!-- 子组件(Vue3.3 及以前) -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>

多个 v-model:

vue 复制代码
<Child v-model:title="title" v-model:content="content" />

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

vue 复制代码
<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)

相关推荐
object not found2 小时前
WooCommerce 接入 KPay 后订单完成页不显示订单信息:returnUrl 动态参数问题解决
前端·wordpress·独立站支付
yume_sibai2 小时前
04-Nginx部署实战完全指南(前端部署 + 后端代理 + 监控告警 + 故障排查)
运维·前端·nginx
data analyse 4562 小时前
数据合规的同意记录怎么留存?
前端·人工智能·数据分析
linux_cfan2 小时前
videojs v10 源代码系列解读:35 · HLS 解析器:multivariant 与 media playlist
前端·javascript·音视频
IT_陈寒2 小时前
Redis键过期失效?这个坑我踩得明明白白
前端·人工智能·后端
web打印社区2 小时前
汽修 / 4S 店:维修工单、结算单网页怎么静默出纸
开发语言·前端·javascript·chrome·ecmascript
liangshanbo12152 小时前
Webpack的分包策略面试题
前端·webpack·node.js
ttwuai2 小时前
Golang Web后台管理框架推荐:Gin、GoFrame与数据面板项目怎么分
前端·golang·gin