[TOC] #### 1. 创建 Vue3 工程 --- 基于 Vite 创建 Vue3 工程 ```bash npm create vue@latest ``` 安装项目依赖并且运行项目 ```bash npm i && npm run dev ``` #### 2. 编写 APP 组件 --- vite 项目中,index.html 是项目的入口文件,在项目根目录下 加载 index.html 后,vite 解析 `<script type="module" src="xxx">` 指向的 JavaScript ```html <div id="app"></div> <script type="module" src="/src/main.js"></script> ``` main.js 文件内容:createApp 创建应用,根组件为 App,将应用挂载到 `#app` 上 ```javascript // 引入 createApp 用于创建应用 import { createApp } from 'vue' // 引入 App 根组件 import App from './App.vue' // 挂载应用 createApp(App).mount('#app') ``` #### 3. setup 概述 --- setup 是 vue3 中一个新的配置项,值是一个函数,它是组合式 API 表演的舞台,组件中所用到的:数据、方法、计算属性、侦听器等等,均配置在 setup 中。 特点如下: setup 函数返回的对象中的内容,可直接在模板中使用 setup 中访问 this 是 undefined,vue3 中已经弱化 this 了 setup 会在 beforeCreate 之前调用,它是领先所有钩子执行的 ```html <template> <div class="app"> <div>{{ a }}</div> <div>{{ age }}</div> <button @click="changeAge">修改年龄</button> </div> </template> <script> export default { name: "App", setup() { let name = '张三' let age = 18 function changeAge() { age += 1 console.log(age); } // 数据和方法交出去,模板中就可以使用 return { a: name, age, changeAge } }, } </script> ``` setup 会在 beforeCreate 之前调用,它是领先所有钩子执行的 ```javascript export default { name: "App", beforeCreate() { console.log('beforeCreate'); }, setup() { console.log('setup'); }, } ``` setup 的返回值也可以是一个渲染函数 ```javascript export default { name: "App", setup() { return () => '哈哈' }, } ``` #### 4. setup 与选项式 API --- setup 选项可以和选项式 API 的写法共存,选项式 API 中可以获取到 setup 中的数据,但是 setup 中不能读取到选项式 API 中的数据,因为 setup 是领先所有钩子执行的。虽然支持这种写法,但是不建议这样写 ```javascript export default { name: "App", data() { return { name: '张三', userAge: this.age } }, methods: { getAge() { console.log(this.age); } }, setup() { let age = 18 return { age } }, } ``` #### 5. setup 语法糖 --- 新增一个 script 标签,并且设置一个 setup 属性,就可以将 setup 选项中的内容放到该标签里面,在模板中可以直接使用该 script 标签中的数据,相当于 setup 选项自动 return,这也是官方推荐的写法 ```html <script> export default { name: "App", } </script> <script setup> let name = '张三' let age = 18 function changeAge() { age += 1 } </script> ``` 如果想要将组件名和 setup 合并到一个 script 标签,需要安装以下插件 ```bash npm i vite-plugin-vue-setup-extend -D ``` 修改 vite.config.js 配置文件,导入插件 ```javascript import VueSetupExtend from 'vite-plugin-vue-setup-extend'; export default defineConfig({ plugins: [ VueSetupExtend(), ], }) ``` 然后就可以通过给 `<script setup>` 增加 name 属性 指定组件名了 ```html <script setup name="Person"></script> ``` 补充:有个项目好像是因为我安装了这个插件,启动项目报错了,报错信息: ``` [ERROR] No loader is configured for ".node" files: node_modules/fsevents/fsevents.node ``` 解决方案1:找到项目中 `node_modules/fsevents/fsevents.js` 文件 ```javascript // 将以下内容 // const Native = require("./fsevents.node"); // 修改为 const Native = window.require("./fsevents.node"); ``` 解决方案2:排除 fsevents 依赖优化 在 vite.config.js 中配置 optimizeDeps.exclude 排除 fsevents 依赖(常见于 macOS 环境) ```javascript export default defineConfig({ optimizeDeps: { exclude: ["fsevents"] } }) ``` 清理缓存并重新安装依赖(排除 fsevents 依赖后项目仍无法启动执行此步骤) 删除 node_modules 和锁定文件后重新安装依赖,解决依赖安装不完整或版本冲突问题 ```bash rm -rf node_modules package-lock.json npm cache clean --force npm install ``` #### 6. ref 创建响应式数据 --- ref 用于定义响应式变量,返回一个 RefImpl 的实例对象,简称 ref 对象,ref 对象的 value 属性是响应式的 ```javascript import { ref } from 'vue'; let xxx = ref(初始值) ``` 注意点: JS 中操作数据需要 `xxx.value`,但模板中调用数据时不需要 `.value`,直接使用即可 对于 `let name = ref('张三')` 来说,name 不是响应式的,`name.value` 是响应式的 ref 创建基本类型的响应式数据 ```javascript let age = ref(18) function changeAge() { age.value += 1 } ``` ref 创建对象类型的响应式数据 ```javascript let car = ref({ brand: '奔驰', price: 100 }) let users = ref([ { id: 1, name: '张三' }, { id: 2, name: '李四' }, ]) function changePrice() { car.value.price += 10 } function changeFirstPrice() { users.value[0].name += '~' } ``` #### 7. reactive 创建响应式数据 --- reactive 创建对象类型的响应式数据 ```javascript import { reactive } from 'vue'; let car = reactive({ brand: '奔驰', price: 100 }) let users = reactive([ { id: 1, name: '张三' }, { id: 2, name: '李四' }, ]) console.log(car); // Proxy(Object) { ... } console.log(users); // Proxy(Object) { ... } function changePrice() { car.price += 10 } function changeFirstPrice() { users[0].name += '~' } ``` reactive 定义的对象响应式数据是深层次的 ```javascript let obj = reactive({ a: { b: { c: 1 } } }) function changeObj() { obj.a.b.c += 1 } ``` #### 8. ref 和 reactive 对比 --- ref 用来定义:基本类型数据、对象类型数据;reactive 用来定义:对象类型数据 一、区别: ref 创建的变量必须使用 `.value` 获取值,reactive 定义的变量则不需要 ```javascript let age = ref(10) let car = reactive({ brand: '奔驰', price: 100 }) function changePrice() { age.value += 1 car.price += 2 } ``` reactive 重新分配一个新对象,会失去响应式(可以使用 Object.assign 去整体替换) ```javascript let car = reactive({ brand: '奔驰', price: 100 }) function changePrice() { // 不具备响应式 // car = { brand: '宝马', price: 200 } // 这种写法具有响应式 Object.assign(car, { brand: '宝马', price: 200 }) } ``` 但是使用 ref 定义的对象类型数据,直接分配一个对象,也具有响应式 ```javascript let car = ref({ brand: '奔驰', price: 100 }) function changePrice() { car.value = { brand: '宝马', price: 200 } } ``` 二、使用原则: 若需要一个基本类型的响应式数据,必须使用 ref 若需要一个响应式对象,层级不深,ref、reactive 都可以 若需要一个响应式对象,且层级较深,推荐使用 reactive 三、ref 处理对象类型的响应式数据时,其 value 的值还是使用 reactive 处理的 ```javascript let age = ref(10) let car = reactive({ brand: '奔驰', price: 100 }) console.log(age); // RefImpl { ... } console.log(car); // Proxy(Object) { ... } ``` #### 9. toRefs 与 toRef --- 将一个响应式对象中的每一个属性,转换为 ref 对象。toRefs 与 toRef 功能一致,但 toRefs 可以批量转换 ```javascript import { ref, reactive, toRefs, toRef } from 'vue'; // ref 定义的对象类型 // let person = ref({ name: '张三', age: 18 }) // let { name, age } = toRefs(person.value) let person = reactive({ name: '张三', age: 18 }) let { name, age } = toRefs(person) // 批量转换 let n2 = toRef(person, 'name') // 单个转换 function changeName() { name.value += '~' } ``` #### 10. computed 计算属性 --- ```javascript import { ref, computed } from 'vue'; let firstName = ref('zhang') let lastName = ref('San') // 这么定义的计算属性,是只读的 // let fullName = computed(() => { // return firstName.value.toLocaleUpperCase() + ' ' + lastName.value.toLocaleLowerCase() // }) // 这么定义的计算属性,可读可写 let fullName = computed({ get() { return firstName.value.toLocaleUpperCase() + ' ' + lastName.value.toLocaleLowerCase() }, set(val) { const [str1, str2] = val.split('-') firstName.value = str1 lastName.value = str2 } }) function changFullName() { fullName.value = 'Li-Si' } ``` #### 11. watch 侦听器 --- watch 用于监视数据的变化,和 vue2 中的 watch 作用一致 特点:Vue3 中的 watch 只能监视以下四种数据: + ref 定义的数据 + reactive 定义的数据 + 函数返回一个值 + 一个包含上述内容的数组 情况一:监视 ref 定义的基本类型数据 ```javascript import { ref, watch } from 'vue'; let sum = ref(0) function changeSum() { sum.value += 1 } // 监视的时候不需要写 sum.value,直接写 sum 即可 const stopWatch = watch(sum, (newValue, oldValue) => { console.log('sum 变化了'); console.log({ newValue, oldValue }); // 当值大于 5 时,停止监视 if (newValue >= 5) { stopWatch() } }) ``` 情况二:监视 ref 定义的对象类型数据 直接写数据名,监视的是对象的地址值,若想要监视对象内部的数据,要手动开启深度监视 注意: + 若修改的是 ref 定义的对象中的属性,newValue 和 oldValue 都是新值,因为它们是同一个对象 + 若修改整个 ref 定义的对象,newValue 是新值,oldValue 是旧值,因为它们不是同一个对象了 ```javascript import { ref, watch } from 'vue'; let person = ref({ name: '张三', age: 18 }) function changeName() { // 修改对象中的属性,不会触发监视器1,会触发监视器2 person.value.name += '~' } function changePerson() { // 修改对象的地址值,既会触发监视器1,也会触发监视器2 person.value = { name: '李四', age: 20 } } // 监视器1:监视的是对象的地址值 watch(person, (newValue, oldValue) => { console.log('person 对象地址值 变化了'); console.log(newValue, oldValue); }) // 监视器2: 若想监视对象内部属性的变化,需要手动开启深度监视 deep: true watch(person, (newValue, oldValue) => { console.log('person 对象地址值或属性值 变化了'); console.log(newValue, oldValue); }, { deep: true }) // watch 的第一个参数: 被监视的数据 // watch 的第二个参数: 监视的回调 // watch 的第三个参数: 配置对象 // immediate: true,进入页面立即监视 watch(person, (newValue, oldValue) => { console.log('person 立即监视 变化了'); console.log(newValue, oldValue); }, { deep: true, immediate: true }) ``` 情况三:reactive 定义的对象类型数据 监视 reactive 定义的对象类型数据,且默认是开启深度监视的,无需手动开启,并且其深度监视是无法关闭的 ```javascript import { reactive, watch } from 'vue'; let person = reactive({ name: '张三', age: 18 }) function changeName() { person.name += '~' } function changePerson() { Object.assign(person, { name: '李四', age: 30 }) } watch(person, (newValue, oldValue) => { console.log('person 属性值 变化了'); console.log(newValue, oldValue); }) // 并且 深度监视 无法关闭的,deep: false 是无效的 // watch(person, (newValue, oldValue) => { }, { deep: false }) ``` 情况四:监视 ref 或 reactive 定义的对象类型数据中的某个属性,注意点如下: + 若该属性值不是对象类型,需要写成函数形式 + 若该属性值依然是对象类型,可以直接编写,也可以写成函数,不过建议写成函数 结论:监视对象里的属性,最好写函数式,默认监视对象的地址值;若是需要监视对象的属性值,手动开启深度监视即可 ```javascript let person = reactive({ name: '张三' }) function changeName() { person.name += '~' } // 错误写法 // watch(person.name, (val) => { // console.log('person.name 变化了'); // }) // 正确写法,属性值不是对象类型,需要写成函数形式 watch(() => person.name, (val) => { console.log('person.name 变化了'); }) ``` ```javascript let person = reactive({ name: '张三', car: { c1: '奔驰', c2: '宝马' } }) function changeC1() { // 场景一:修改对象中的属性值,可以触发监视器1,不能触发监视器2,可以触发监视器3 person.car.c1 += '奥迪' } function changeC2() { // 场景二:修改对象中的属性值,可以触发监视器1,不能触发监视器2,可以触发监视器3 person.car.c2 = '大众' } function changeCar() { // 场景三:修改对象的地址值,无法触发监视器1,可以触发监视器2,可以触发监视器3 person.car = { c1: '雅迪', c2: '爱玛' } // 场景四:这种写法没有修改对象的地址值,可以触发监视器1,不能触发监视器2,可以触发监视器3 // Object.assign(person.car, { c1: '雅迪', c2: '爱玛' }) } // 监视器1: 属性值依然是对象类型,可以直接编写 // watch(person.car, (val) => { // console.log('person.car 属性值变化了'); // }) // 监视器2: 给对象包一个函数,此时监视的就是这个对象的地址值 // watch(() => person.car, (val) => { // console.log('person.car 地址值变化了'); // }) // 监视器3: 给对象包一个函数,手动开启深度监视 watch(() => person.car, (val) => { console.log('person.car 属性值或地址值变化了【深度监视】'); }, { deep: true }) ``` 情况五:监视上述多个数据 ```javascript // 监视多个数据,以下两种写法都可以 watch([() => person.name, person.car], (newValue, oldValue) => { console.log(newValue, oldValue); }) watch([() => person.name, () => person.car.c1], (newValue, oldValue) => { console.log(newValue, oldValue); }) ``` #### 12. watchEffect --- 立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行该函数 watch 对比 watchEffect + 都能监听响应式数据的变化,不同的是监听数据变化的方式不同 + watch 要明确指出监视的数据 + watchEffect 不用明确指出监视的数据,函数中用到哪些属性,那就监视哪些属性 ```javascript import { ref, watch, watchEffect } from 'vue'; let temp = ref(0) let height = ref(0) function changeTemp() { temp.value += 1 } function changeHeight() { height.value += 2 } // 需求: 当 temp > 3 或 height > 5 时 给服务器发送请求 // watch([temp, height], (val) => { // let [temp, height] = val // if (temp > 3 || height > 5) { // console.log('给服务器发请求'); // } // }) watchEffect(() => { console.log('watchEffect 执行了'); if (temp.value > 3 || height.value > 5) { console.log('给服务器发请求'); } }) ``` #### 13. 标签的 ref 属性 --- 情况一:用在普通 DOM 标签上,获取的是 DOM 节点 ```html <template> <div class="app"> <h2 ref="title">北京</h2> <button @click="showLog">点我输出h2元素</button> </div> </template> <script setup> import { ref } from 'vue'; // 创建一个 title,用于存储 ref 标记的内容 let title = ref() function showLog() { console.log(title.value); // <h2 ref="title">北京</h2> } </script> ``` 情况二:用在组件标签上,获取的是组件实例对象 #### 14. 回顾 TS 接口、泛型 --- 创建一个接口,文件位置 `src/types/index.ts`,内容如下所示: ```javascript // 定义一个接口,用于限制 person 对象的具体属性 export interface PersonInter { id: number, name: string, age: number } // 一个自定义类型(两种写法都可以) // export type Persons = Array<PersonInter> export type Persons = PersonInter[] ``` 使用接口,必须遵守其规范 ```javascript import { type PersonInter, type Persons } from '@/types'; // 使用接口 let person: PersonInter = { id: 1, name: '张三', age: 18 } // 数组中的每个元素都遵循 PersonInter 接口 let personList: Array<PersonInter> = [ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, ] let personArray: Persons = [ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, ] ``` #### 15. props 的使用 --- reactive 定义的对象类型数据使用泛型 ```javascript import { reactive } from 'vue'; import { type Persons } from '@/types'; // 下面两种写法都可以 // 第一种:写在变量名后面 let personList1: Persons = reactive([ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, { id: 3, name: '王五', age: 20 }, ]) // 第二种:写在 reactive 后面 let personList2 = reactive<Persons>([ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, { id: 3, name: '王五', age: 20 }, ]) ``` 当前有父组件 App.vue,文件内容如下所示 ```html <template> <Person :list="personArray" /> </template> <script lang="ts" setup name="App"> import Person from './Person.vue'; import { reactive } from 'vue'; import { type Persons } from '@/types'; let personArray = reactive<Persons>([ { id: 1, name: '张三', age: 18 }, { id: 2, name: '李四', age: 20 }, { id: 3, name: '王五', age: 20 }, ]) </script> ``` 子组件 Person.vue 文件内容 ```html <template> <ul> <li v-for="item in list">{{ item.name }}</li> </ul> </template> <script lang="ts" setup> import { defineProps, withDefaults } from 'vue'; import { type Persons } from '@/types'; // 接受 list // defineProps(['list']) // 接受 list + 限制类型 // defineProps<{ list: Persons }>() // 接受 list + 限制类型 + 限制必要性(?: 父组件可以不传) // defineProps<{ list?: Persons }>() // 接受 list + 限制类型 + 限制必要性 + 指定默认值 let props = withDefaults(defineProps<{ list?: Persons }>(), { list: () => [{ id: 1, name: 'liang', age: 18 }] }) // 接受 list, 同时将 props 保存起来 // let props = defineProps(['list']) </script> ``` #### 16. Vue2 生命周期 --- Vue 组件实例在创建时要经历一系列的初始化步骤,在此过程中 Vue 会在合适的时机,调用特定的函数,从而让开发者有机会在特定阶段运行自己的代码,这些特定的函数统称为:生命周期钩子 生命周期整体分为四个阶段,每个阶段都有两个钩子,一前一后 + 创建(创建前,创建完毕) + 挂载(挂载前,挂载完毕) + 更新(更新前,更新完毕) + 销毁(销毁前,销毁完毕) 常用的钩子:挂载完毕,更新完毕、卸载之前 ```javascript export default { name: 'Person', data() { return { sum: 1 } }, methods: { add() { this.sum += 1 } }, // 创建前的构子 beforeCreate() { console.log('beforeCreate 创建前的构子'); }, // 创建完毕的构子 created() { console.log('created 创建完毕的钩子'); }, // 挂载前的构子 beforeMount() { console.log('beforeMount 挂载前的构子'); }, // 挂载完毕的构子 mounted() { console.log('mounted 挂载完毕的构子'); }, // 更新前的构子 beforeUpdate() { console.log('beforeUpdate 更新前的构子'); }, // 更新完毕的构子 updated() { console.log('updated 更新完毕的构子'); }, // 销毁前的构子 beforeDestroy() { console.log('beforeDestroy 销毁前的构子'); }, // 消耗完毕的构子 destroyed() { console.log('destroyed 消耗完毕的构子'); } } ``` #### 17. Vue3 生命周期 --- Vue3 的生命周期 + 创建(setup) + 挂载(挂载前,挂载完毕) + 更新(更新前,更新完毕) + 卸载(卸载前,卸载完毕) ```javascript import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'; let sum = ref(0) function add() { sum.value += 1 } // 挂载前 onBeforeMount(() => { console.log('挂载前'); }) // 挂载完毕 onMounted(() => { console.log('挂载完毕'); }) // 更新前 onBeforeUpdate(() => { console.log('更新前'); }) // 更新完毕 onUpdated(() => { console.log('更新完毕'); }) // 卸载前 onBeforeUnmount(() => { console.log('卸载前'); }) // 卸载完毕 onUnmounted(() => { console.log('卸载完毕'); }) ``` #### 18. 自定义 hooks --- 现有组件 Person.vue ```html <template> <h2>求和: {{ sum }}</h2> <button @click="add">点我sum+1</button> <br> <img v-for="item in dogList" :src="item"> <br> <button @click="getLog">获取小狗</button> </template> <script lang="ts" setup> import useSum from '@/hooks/useSum'; import useDog from '@/hooks/useDog'; const { sum, add } = useSum() const { dogList, getLog } = useDog() </script> ``` hooks 文件 useSum.ts (src/hooks/useSum.ts) ``` import { ref, onMounted } from 'vue'; export default function () { // 数据 let sum = ref(0) // 方法 function add() { sum.value += 1 } // 可以正常编写钩子 onMounted(() => { sum.value += 2 }) // 向外部提供东西 return { sum, add } } ``` hooks 文件 useDog.ts (src/hooks/useDog.ts) ``` import axios from 'axios'; import { reactive } from 'vue'; export default function () { // 数据 let dogList = reactive([ "https://images.dog.ceo/breeds/pembroke/n02113023_3324.jpg", ]) // 方法 async function getLog() { try { let result = await axios.get('https://dog.ceo/api/breed/pembroke/images/random') dogList.push(result.data.message) } catch (error) { alert(error) } } // 向外部提供东西 return { dogList, getLog } } ``` #### 19. 路由基本切换效果 --- 概念:路由就是一组 key-value 的对应关系,多个路由,需要经过路由器的管理 安装路由器 ```bash npm i vue-router ``` 创建路由器文件,src/router/index.ts ```javascript // 创建一个路由,并且暴露出去 // 第一步: 引入 createRouter import { createRouter, createWebHistory } from 'vue-router'; // 引入一个一个可能要呈现的组件 import Home from '@/components/Home.vue'; import News from '@/components/News.vue'; import About from '@/components/About.vue'; // 第二步: 创建路由器 const router = createRouter({ // 路由器的工作模式 history: createWebHistory(), // 路由规则 routes: [ { path: '/home', component: Home }, { path: '/news', component: News }, { path: '/about', component: About }, ] }) // 暴露出去 router export default router ``` 修改 main.js ```javascript import { createApp } from 'vue'; import App from './App.vue'; // 引入路由器 import router from './router'; // 将以下内容 createApp(App).mount("#app") // 修改为 const app = createApp(App) // 创建一个应用 app.use(router) // 使用路由器 app.mount("#app") // 挂载app应用到容器中 // 也可以连写 // app.use(router).mount("#app") ``` 根组件 App.vue ```html <template> <div class="app"> <h2 class="title">Vue 路由测试</h2> <!-- 导航区 --> <div class="navigate"> <!-- active-class 指定激活时的class值--> <RouterLink to="/home" active-class="active">首页</RouterLink> <RouterLink to="/news" active-class="active">新闻</RouterLink> <RouterLink to="/about" active-class="active">关于</RouterLink> </div> <!-- 展示区 --> <div class="content"> <RouterView></RouterView> </div> </div> </template> <script lang="ts" name="App"> import { RouterView, RouterLink } from 'vue-router'; </script> <style> .navigate a { margin-right: 20px; } .content { width: 600px; padding: 20px; margin-top: 20px; border: 1px solid red; } </style> ``` #### 20. 路由两个注意点 --- 路由两个注意点: + 路由组件通常存放在 pages 或 views 文件夹,一般组件通常存放在 components 文件夹 + 通过点击导航,视觉效果上 “消失”了的路由组件,默认是被卸载掉的,需要的时候再去挂载 路由组件:靠路由的规则渲染出来的 ```javascript routes: [{ path: '/home', component: Home }] ``` 一般组件:亲手写标签出来的 ```html <Person /> ``` 视觉效果上 “消失”了的路由组件,默认是被卸载掉的,需要的时候再去挂载 ```html <template> <div>News</div> </template> <script setup> import { onMounted, onUnmounted } from 'vue'; onMounted(() => { console.log('news onMounted'); }) onUnmounted(() => { console.log('news onUnmounted'); }) </script> ``` #### 21. 路由器的工作模式 --- 路由器的两种工作模式:history 模式、hash 模式 history 模式 + 优点: URL 更加美观,不带有 #,更接近传统网站的 URL + 缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会有404错误 hash 模式 + 优点:兼容性更好,因为不需要服务端处理路径 + 缺点:URL 带有 # 不太美观,且在 SEO 优化方面相对较差 使用示例: ```javascript import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'; const router = createRouter({ // 路由器的工作模式 history: createWebHistory(), // history 模式 // history: createWebHashHistory(), // hash 模式 // 路由规则 routes: [] }) ``` #### 22. 路由 to 的两种写法 --- 路由 to 的两种写法:字符串形式、对象形式(又分为通过路由名称和路由路径跳转) ```html <RouterLink to="/home">首页</RouterLink> <RouterLink :to="{ name: 'xinwen' }">新闻</RouterLink> <RouterLink :to="{ path: '/about' }">关于</RouterLink> ``` ```javascript const router = createRouter({ // 路由器的工作模式 history: createWebHistory(), // 路由规则 routes: [ { name: 'zhuye', path: '/home', component: Home }, { name: 'xinwen', path: '/news', component: News }, { name: '/guanyu', path: '/about', component: About }, ] }) ``` #### 23. 命名路由 --- 命令路由:可以简化路由跳转及传参 给路由规则命名,如下所示,命名为 xinwen ```javascript routes: [{ name: 'xinwen', path: '/news/list', component: News }] ``` 跳转路由: ```html <!-- 简化前,需要写完整的路由路径,现在只需写路由名称即可 --> <RouterLink :to="{ name: 'xinwen' }">新闻</RouterLink> ``` #### 24. 嵌套路由 --- 路由规则中使用 children 可定义嵌套路由 ```javascript routes: [ { path: '/news', component: News, children: [ { path: 'detail', component: Detail } ] }, ] ``` 那么在 News.vue 中,可以使用嵌套路由 ```html <template> <div>News</div> <!-- 导航区 --> <ul> <li v-for="item in newsList" :key="item.id"> <RouterLink to="/news/detail">{{ item.title }}</RouterLink> </li> </ul> <!-- 展示区 --> <div class="news-content"> <RouterView></RouterView> </div> </template> <script lang="ts" setup> import { RouterView, RouterLink } from 'vue-router'; import { reactive } from 'vue'; const newsList = reactive([ { id: 1, title: '春节活动', content: '这是春节活动内容' }, { id: 2, title: '国庆节活动', content: '这是国庆节活动内容' }, ]) </script> ``` #### 25. 路由 query 参数 --- 传递参数 ```html <li v-for="item in newsList" :key="item.id"> <!-- 第一种写法 --> <!-- <RouterLink :to="`/news/detail?id=${item.id}&title=${item.title}`">{{ item.title }}</RouterLink> --> <!-- 第二种写法 --> <RouterLink :to="{ path: '/news/detail', query: { id: item.id, title: item.title } }">{{ item.title }} </RouterLink> </li> ``` 接收参数 ```javascript import { useRoute } from 'vue-router'; // 打印 query 参数 const route = useRoute() ``` 使用示例: ```html <template> <ul> <li>ID:{{ route.query.id }}</li> <li>标题:{{ route.query.title }}</li> </ul> </template> <script lang="ts" setup> import { useRoute } from 'vue-router'; const route = useRoute() </script> ``` 也可以使用 toRefs ```html <template> <ul> <li>ID:{{ query.id }}</li> <li>标题:{{ query.title }}</li> </ul> </template> <script lang="ts" setup> import { toRefs } from 'vue'; import { useRoute } from 'vue-router'; const route = useRoute() let { query } = toRefs(route) </script> ``` #### 26. 路由 params 参数 --- 用法说明 + 传递 params 参数时,需要提前在路由规则中占位 + 传递 params 参数时,若使用 to 的对象写法,必须使用 name 配置项,不能用 path 修改路由规则:`detail/:id/:title` 参数占位,`?` 表示可选参数 ```javascript routes: [ { name: 'xinwen', path: '/news', component: News, children: [ { name: 'newDetail', path: 'detail/:id/:title?', component: Detail } ] } ] ``` 传递参数 ```html <li v-for="item in newsList" :key="item.id"> <!-- 第一种写法 --> <!-- <RouterLink :to="`/news/detail/${item.id}/${item.title}`">{{ item.title }}</RouterLink> --> <!-- 第二种写法,不能用 path,只能用 name --> <RouterLink :to="{ name: 'newDetail', params: { id: item.id, title: item.title, } }">{{ item.title }}</RouterLink> </li> ``` 接收参数 ```html <template> <ul> <li>ID:{{ route.params.id }}</li> <li>标题:{{ route.params.title }}</li> </ul> </template> <script lang="ts" setup> import { useRoute } from 'vue-router'; const route = useRoute() console.log(route); </script> ``` #### 27. 路由 props 配置 --- 作用:让路由组件更方便的收到参数(可以将路由参数作为 props 传给组件) ```javascript { // 第一种写法: 布尔值,作用:把收到的每一组 params 参数,作为 props 传给 detail 组件 props: true, // 第二种写法: 函数写法,作用:把返回的对象中每一组 key-value,作为 props 传给 detail 组件 props(route) { return route.query }, // 第三种写法: 对象写法,作用:把返回的对象中每一组 key-value,作为 props 传给 detail 组件 props: { id: '123', title: '标题' } } ``` 上述我们传递 params 参数的写法有点麻烦,可以使用 路由 props 配置进行优化 ```javascript routes: [ { path: '/news', component: News, children: [ { name: 'newDetail', path: 'detail/:id/:title', component: Detail, // 第一种写法: 布尔值,作用:把收到的每一组 params 参数,作为 props 传给 detail 组件 props: true } ] } ] ``` 如果传递的是 query 参数,可以定义 props 为一个函数,将 query 参数作为返回值 ```javascript routes: [ { path: '/news', component: News, children: [ { name: 'newDetail', path: 'detail', component: Detail, // 第二种写法: 函数写法,作用:把返回的对象中每一组 key-value,作为 props 传给 detail 组件 props(route) { return route.query } } ] } ] ``` 第三种写法: 对象写法,这种写法用的比较少 ```javascript routes: [ { path: '/news', component: News, children: [ { name: 'newDetail', path: 'detail', component: Detail, // 第三种写法: 对象写法,作用:把返回的对象中每一组 key-value,作为 props 传给 detail 组件 props: { id: '123', title: '标题' } } ] } ] ``` Detail.vue 组件接收数据就比较简单了,可接受上述三种写法传递的参数 ```html <template> <ul> <li>ID:{{ id }}</li> <li>标题:{{ title }}</li> </ul> </template> <script lang="ts" setup> import { defineProps } from 'vue'; defineProps(['id', 'title']) </script> ``` #### 28. 路由 replace 属性 --- 作用:控制路由跳转时操作浏览器历史记录的模式 浏览器的历史记录有两种写入方式:分别为 push 和 replace + push 是追加历史记录(默认值) + replace 是替换当前记录 开启 replace 模式 ```html <RouterLink replace ...>XXX</RouterLink> ``` #### 29. 编程式路由导航 --- 编程式路由导航: 脱离 RouterLink 标签实现路由跳转 路由组件的两个重要属性:`$route` 和 `$router` 变成了两个 `hooks` ```javascript import { onMounted } from 'vue'; import { useRouter } from 'vue-router'; const router = useRouter() onMounted(() => { // 挂载完毕3秒后跳转路由 setTimeout(() => { // router.push('/news') router.replace('/news') }, 3000) }) ``` 关于 router.push() 方法的参数,和 RouterLink 的 to 的写法相同 ```javascript function showNewsDetail(item: any) { router.push({ name: 'newDetail', query: { id: item.id } }) } ``` #### 30. 路由重定向 --- 作用:让指定的路径重定向到另一个路径 ```javascript routes: [ { path: '/', redirect: '/home' }, { path: '/home', component: Home }, ] ``` #### 31. 搭建 pinia 环境 --- 集中式状态管理工具:Vue2 中使用的是 VueX、Vue3 中使用的是 pinia 安装 pinia 依赖 ```bash npm i pinia ``` 使用 pinia ```javascript import { createApp } from 'vue'; import App from './App.vue'; // 第一步: 引入 pinia import { createPinia } from 'pinia'; const app = createApp(App) // 第二步: 创建 pinia const pinia = createPinia() // 第二步: 安装 pinia app.use(pinia) app.mount("#app") ``` #### 32. 存储 + 读取数据 --- Store 是一个保存:状态、业务逻辑的实体,每个组件都可以读取、写入它 它有三个概念:state、getter、action,相当于组件中的:data、computed 和 methods 具体编码:`src/store/count.ts` ```javascript import { defineStore } from 'pinia'; // 定义并暴露一个 store export const useCountStore = defineStore('count', { // 真正存储数据的地方 state() { return { sum: 2 } } }) ``` 读取数据 ```javascript // 引入 useCountStore import { useCountStore } from '@/store/count'; // 使用 useCountStore,得到一个专门保存 count 相关的 store const countStore = useCountStore() // 以下两种方式都可以拿到 state 中的数据 console.log(countStore.sum); // console.log(countStore.$state.sum); ``` #### 33. 修改数据的三种方式 --- 修改 pinia 数据的三种方式: ```javascript // 引入 useCountStore import { useCountStore } from '@/store/count'; // 使用 useCountStore,得到一个专门保存 count 相关的 store const countStore = useCountStore() function inc() { // 第一种方式:直接修改 countStore.sum += 1 countStore.title = '新标题' // 第二种方式:批量修改 countStore.$patch({ sum: 10, title: '这是标题', }) // 第三种方式:借助 actions 修改(actions 中可以编写一些业务逻辑) countStore.increment(10) } ``` useCountStore 编码:`src/store/count.ts` ```javascript import { defineStore } from 'pinia'; // 定义并暴露一个 store export const useCountStore = defineStore('count', { // actions 里面放置的是一个一个的方法,用于响应组件的动作 actions: { increment(value) { // 修改数据(this 是当前 store) this.sum += value } }, // 真正存储数据的地方 state() { return { sum: 1, title: '标题', } } }) ``` #### 34. storeToRefs --- 上面我们读取 pinia 的时候是这样的,好像不太方便,不够优雅 ```html <div>当前求和:{{ countStore.sum }}</div> <div>当前标题:{{ countStore.title }}</div> ``` 那么我们可能想到使用解构赋值,直接解构这种方式会使数据失去了响应式 ```javascript import { useCountStore } from '@/store/count'; const countStore = useCountStore() // 读取数据是可以的,但是失去了响应式 const { sum, title } = countStore ``` ```html <div>当前求和:{{ sum }}</div> <div>当前标题:{{ title }}</div> ``` 发现数据没有响应式,你可能会想到使用 toRefs 解构 ```javascript // 数据确实是响应式,但是不建议使用,代价比较大 const { sum, title } = toRefs(countStore) ``` 通过打印发现,将 store 中的所有属性和方法都变为了 ref 引用,而我们只需要把 store 中的数据变为引用 所以,虽然可以实现,但是不要用 toRefs 解构 store 的数据 ```javascript console.log(toRefs(countStore)); ``` pinia 也注意到了这个问题,提供了一个 storeToRefs 来解决这个事情 storeToRefs 只关注 store 中的数据,不会关注 store 中的方法,只将 store 中的数据变为 ref 引用 ```javascript import { storeToRefs } from 'pinia'; import { useCountStore } from '@/store/count'; const { sum, title } = storeToRefs(countStore) ``` #### 35. getters 的使用 --- 概念:当 state 中的数据,需要经过处理后再使用时,可以使用 getters 配置 ```javascript import { defineStore } from 'pinia'; // 定义并暴露一个 store export const useCountStore = defineStore('count', { // 真正存储数据的地方 state() { return { sum: 1, title: 'hello world', } }, getters: { // 第一种写法:使用 state bigSum(state) { return state.sum * 10 }, // 简写为箭头函数,反正不使用 this // bigSum: state => state.sum * 2, // 第二种写法: 使用 this upperTitle() { return this.title.toLocaleUpperCase() } } }) ``` 使用 getters 中的方法 和 使用 state 中的数据写法是一样的 ```javascript import { storeToRefs } from 'pinia'; import { useCountStore } from '@/store/count'; const countStore = useCountStore() const { sum, title, bigSum, upperTitle } = storeToRefs(countStore) ``` #### 36. $subscribe 监听 state --- 通过 store 的 `$subscribe` 方法监听 state 及其变化 ```javascript const countStore = useCountStore() const { sum, title } = storeToRefs(countStore) // 只要 state 中的数据有一个发生了变化就能监听到 countStore.$subscribe((mutate, state) => { // mutate 事件信息 state: store中的数据 console.log('countStore 数据发生变化了'); console.log(state); console.log(state.title); console.log(title.value); }) ``` #### 37. store 的组合式写法 --- 上面我们写的 store 都是选项式的写法,如下所示 ```javascript import { defineStore } from 'pinia'; export const useCountStore = defineStore('count', { actions: { increment(value) { } }, state() { return { sum: 1 } }, getters: { bigSum: state => state.sum * 2, } }) ``` store 也支持组合式的写法,如下所示 ```javascript import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; export const useCountStore = defineStore('count', () => { // state let sum = ref(1) // actions const increment = (val: Number) => { sum.value += 5 } // getters (使用 computed 计算属性即可) const bigSum = computed(() => { return sum.value * 100 }) // 暴露出去 return { sum, title, bigSum, increment } }) ``` #### 38. 组件通信 --- ##### 第一种方式:props 概述:props 是使用频率最高的一种通信方式,常用于:父子组件之间的传值 + 若父传子:属性值为非函数 + 若子传父:属性值是函数 现有父组件 Father.vue,子组件 Child.vue 父组件将数据传给子组件,示例如下,可以看出来属性值不是函数 ```html <h2>父组件</h2> <Child :vehicle="car"></Child> ``` ```javascript import { ref } from 'vue'; import Child from './Child.vue'; let car = ref('奔驰') ``` 子组件 Child.vue 接收父组件 Father.vue 传给的数据 ```html <h2>子组件</h2> <h3>父组件给的值:{{ vehicle }}</h3> ``` ``` // 声明接收 props defineProps(['vehicle']) ``` 子组件将数据传给父组件,示例如下,可以看到属性值是一个函数 父组件给子组件传递一个方法,子组件接收并且调用这个方法将数据传给父组件 ```html <template> <div class="father"> <h2>父组件</h2> <h3 v-show="toy">子组件数据:{{ toy }}</h3> <Child :sendToy="getToy"></Child> </div> </template> <script lang="ts" setup name="Father"> import { ref } from 'vue'; import Child from './Child.vue'; let toy = ref('') function getToy(value) { toy.value = value } </script> ``` ```html <template> <div class="child"> <h2>子组件</h2> <button @click="sendToy(toy)">给父组件数据</button> </div> </template> <script lang="ts" setup name="Child"> import { ref } from 'vue'; let toy = ref('奥特曼') defineProps(['sendToy']) </script> ``` ##### 第二种方式:自定义事件 先了解一下事件对象,调用方法但是没有传参数,接收参数默认是事件对象 ```html <template> <button @click="btnClick">点我</button> </template> <script setup> function btnClick(e) { console.log(e); // 事件对象 PointerEvent{...} } </script> ``` 传了参数,又想要获取事件对象。模板中有个特殊的占位符,可以理解为一个特殊的变量 `$event`,它就是事件对象 ```html <template> <button @click="btnClick(1, 2, $event)">点我</button> </template> <script setup> function btnClick(a, b, event) { console.log(a, b, event); } </script> ``` 自定义事件用法示例,子组件给父组件传递数据 ```html <template> <div class="father"> <h2>父组件</h2> <Child @send-toy="saveToy"></Child> </div> </template> <script setup> function saveToy(value) { console.log('父组件收到子组件传的数据:' + value); } </script> ``` ```html <template> <div class="child"> <h2>子组件</h2> <button @click="emit('send-toy', toy)">给父组件数据</button> </div> </template> <script setup> import { ref } from 'vue'; let toy = ref('奥特曼') // 声明事件 const emit = defineEmits(['send-toy']) </script> ``` ##### 第三种方式:mitt 使用 mitt 可以实现任意组件通信,用法如下: + 接收数据的:提前绑定事件(提前订阅消息) + 提供数据的:在合适的时候触发事件(发布消息) 安装 mitt ```bash npm i mitt ``` 编写代码:`src/utils/emitter.ts` ```javascript // 引入 mitt import mitt from 'mitt'; // 调用 mitt 得到 emitter,emitter 能:绑定事件、触发事件 const emitter = mitt() // 暴露 emitter export default emitter ``` 在 main.js 中引入 mitt,使其运行 ```javascript // 引入 emitter 使其运行 import emitter from '@/utils/emitter'; ``` mitt 基础语法 ```javascript const emitter = mitt() // 绑定事件 emitter.on('test1', () => { console.log('test1 被调用了'); }) emitter.on('test2', () => { console.log('test2 被调用了'); }) // 触发事件 setInterval(() => { emitter.emit('test1') emitter.emit('test2') }, 2000) // 解绑事件 setTimeout(() => { // 解绑指定事件 emitter.off('test1') // 解绑所有事件 // emitter.all.clear() }, 5000) ``` 使用示例: 在接收数据的组件中绑定一个事件 ```html <template> <h2>组件2</h2> <h2>收到的数据: {{ toy }}</h2> <button>给父组件数据</button> </template> <script lang="ts" setup> import { ref, onUnmounted } from 'vue'; import emitter from '@/utils/emitter'; let toy = ref('') // 给 emitter 绑定 send-toy 事件 emitter.on('send-toy', (value: any) => { toy.value = value }) // 在组件卸载时 解除绑定事件 onUnmounted(() => { emitter.off('send-toy') }) </script> ``` 在发送数据的组件中触发事件且传递参数 ```html <template> <h2>组件1</h2> <button @click="emitter.emit('send-toy', toy)">发送数据</button> </template> <script lang="ts" setup> import { ref } from 'vue'; import emitter from '@/utils/emitter'; let toy = ref('奥特曼') </script> ``` ##### 第四种方式:v-model #### 部署项目 --- Vue3 项目部署后刷新页面报错 服务器配置方案 nginx 配置 ``` location / { try_files $uri $uri/ /index.html; } ```