1. Vue3简介2020年9月18日，Vue.js发布版3.0版本，代号：One Piece（n经历了：4800+次提交、40+个RFC、600+次PR、300+贡献者官方发版地址：Release v3.0.0 One Piece · vuejs/core截止2023年10月，最新的公开版本为：3.3.41.1. 【性能的提升】打包大小减少41%。初次渲染快55%, 更新渲染快133%。内存减少54%。1.2.【 源码的升级】使用Proxy代替defineProperty实现响应式。重写虚拟DOM的实现和Tree-Shaking。1.3. 【拥抱TypeScript】Vue3可以更好的支持TypeScript。1.4. 【新的特性】Composition API（组合API）：setupref与reactivecomputed与watch......新的内置组件：FragmentTeleportSuspense......其他改变：新的生命周期钩子data 选项应始终被声明为一个函数移除keyCode支持作为 v-on 的修饰符......2. 创建Vue3工程2.1. 【基于 vue-cli 创建】点击查看官方文档备注：目前vue-cli已处于维护模式，官方推荐基于 Vite 创建项目。## 查看@vue/cli版本，确保@vue/cli版本在4.5.0以上 vue --version ## 安装或者升级你的@vue/cli npm install -g @vue/cli ## 执行创建命令 vue create vue_test ## 随后选择3.x ## Choose a version of Vue.js that you want to start the project with (Use arrow keys) ## > 3.x ## 2.x ## 启动 cd vue_test npm run serve2.2. 【基于 vite 创建】(推荐)vite 是新一代前端构建工具，官网地址：https://vitejs.cn，vite的优势如下：轻量快速的热重载（HMR），能实现极速的服务启动。对 TypeScript、JSX、CSS 等支持开箱即用。真正的按需编译，不再等待整个应用编译完成。webpack构建 与 vite构建对比图如下： 具体操作如下（点击查看官方文档）## 1.创建命令 npm create vue@latest ## 2.具体配置 ## 配置项目名称 √ Project name: vue3_test ## 是否添加TypeScript支持 √ Add TypeScript? Yes ## 是否添加JSX支持 √ Add JSX Support? No ## 是否添加路由环境 √ Add Vue Router for Single Page Application development? No ## 是否添加pinia环境 √ Add Pinia for state management? No ## 是否添加单元测试 √ Add Vitest for Unit Testing? No ## 是否添加端到端测试方案 √ Add an End-to-End Testing Solution? » No ## 是否添加ESLint语法检查 √ Add ESLint for code quality? Yes ## 是否添加Prettiert代码格式化 √ Add Prettier for code formatting? No//vite创建自动映射 router路径 { "extends": "@vue/tsconfig/tsconfig.dom.json", "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { // Extra safety for array and object lookups, but may have false positives. "noUncheckedIndexedAccess": true, // Path mapping for cleaner imports. "paths": { "@/*": ["./src/*"] }, // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. // Specified here to keep it out of the root directory. "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" } }import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' // https://vite.dev/config/ export default defineConfig({ plugins: [ vue(), vueDevTools(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) }, }, })找不到文件“@vue/tsconfig/tsconfig.dom.json”。 方案一：重启 TypeScript 服务器（推荐首先尝试） 在 VS Code 中按 Cmd+Shift+P，输入 "TypeScript: Restart TS Server"，然后回车。 方案二：重新安装依赖 npm install自己动手编写一个App组件 你好啊！ 安装官方推荐的vscode插件： 总结：Vite 项目中，index.html 是项目的入口文件，在项目最外层。加载index.html后，Vite 解析 3. Vue3核心语法3.1. 【OptionsAPI 与 CompositionAPI】Vue2的API设计是Options（配置）风格的。Vue3的API设计是Composition（组合）风格的。Options API 的弊端Options类型的 API，数据、方法、计算属性等，是分散在：data、methods、computed中的，若想新增或者修改一个需求，就需要分别修改：data、methods、computed，不便于维护和复用。Composition API 的优势可以用函数的方式，更加优雅的组织代码，让相关功能的代码更加有序的组织在一起。说明：以上四张动图原创作者：大帅老猿3.2. 【拉开序幕的 setup】setup 概述setup是Vue3中一个新的配置项，值是一个函数，它是 Composition API “表演的舞台_”_，组件中所用到的：数据、方法、计算属性、监视......等等，均配置在setup中。特点如下：setup函数返回的对象中的内容，可直接在模板中使用。setup中访问this是undefined。setup函数会在beforeCreate之前调用，它是“领先”所有钩子执行的。 姓名：{{name}} 年龄：{{age}} 修改名字 年龄+1 点我查看联系方式 setup 的返回值若返回一个对象：则对象中的：属性、方法等，在模板中均可以直接使用（重点关注）。若返回一个函数：则可以自定义渲染内容，代码如下：setup(){ return ()=> '你好啊！' }setup 与 Options API 的关系Vue2 的配置（data、methos......）中可以访问到 setup中的属性、方法。但在setup中不能访问到Vue2的配置（data、methos......）。如果与Vue2冲突，则setup优先。setup 语法糖setup函数有一个语法糖，这个语法糖，可以让我们把setup独立出去，代码如下： 姓名：{{name}} 年龄：{{age}} 修改名字 年龄+1 点我查看联系方式 扩展：上述代码，还需要编写一个不写setup的script标签，去指定组件名字，比较麻烦，我们可以借助vite中的插件简化第一步：npm i vite-plugin-vue-setup-extend -D第二步：vite.config.tsimport { defineConfig } from 'vite' import VueSetupExtend from 'vite-plugin-vue-setup-extend' export default defineConfig({ plugins: [ VueSetupExtend() ] })第三步： 3.4. 【reactive 创建：对象类型的响应式数据】特性{{ }} 插值v-bind (:)v-model中文名插值语法属性绑定双向绑定数据流向单向：JS → 视图单向：JS → 视图双向：JS ⇄ 视图作用显示文本内容动态绑定 HTML 属性获取+显示表单值适用元素任何元素（标签之间）任何元素的任何属性表单元素（input/select/textarea）用户可修改不能不能可以简写无:无作用：定义一个响应式对象（基本类型不要用它，要用ref，否则报错）语法：let 响应式对象= reactive(源对象)。返回值：一个Proxy的实例对象，简称：响应式对象。注意点：reactive定义的响应式数据是“深层次”的。 汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万 游戏列表： {{ g.name }} 测试：{{obj.a.b.c.d}} 修改汽车价格 修改第一游戏 测试 3.5. 【ref 创建：对象类型的响应式数据】其实ref接收的数据可以是：基本类型、对象类型。若ref接收的是对象类型，内部其实也是调用了reactive函数。 汽车信息：一台{{ car.brand }}汽车，价值{{ car.price }}万 游戏列表： {{ g.name }} 测试：{{obj.a.b.c.d}} 修改汽车价格 修改第一游戏 测试 3.6. 【ref 对比 reactive】宏观角度看：ref用来定义：基本类型数据、对象类型数据；reactive用来定义：对象类型数据。区别：ref创建的变量必须使用.value（可以使用volar插件自动添加.value）。reactive重新分配一个新对象，会失去响应式（可以使用Object.assign去整体替换）。const car = reactive({ brand: '宝马', price: 100 })car = { brand: '奥拓', price: 1 } 失去响应式 Object.assign(car, { brand: '奥拓', price: 1 })使用原则：若需要一个基本类型的响应式数据，必须使用ref。若需要一个响应式对象，层级不深，ref、reactive都可以。若需要一个响应式对象，且层级较深，推荐使用reactive。3.7. 【toRefs 与 toRef】作用：将一个响应式对象中的每一个属性，转换为ref对象。并且改变解构的值，也会影响到原响应式对象的值。备注：toRefs与toRef功能一致，但toRefs可以批量转换。语法如下： 姓名：{{person.name}} 年龄：{{person.age}} 性别：{{person.gender}} 修改名字 修改年龄 修改性别 3.8. 【computed】作用：根据已有数据计算出新数据（和Vue2中的computed作用一致）。实现同样的功能，方法function没有缓存，模板调用几次，函数就执行几次；计算属性computed有缓存，模板调用多次，实际上只执行一次。计算属性实际上是一个ref响应式对象，因此赋值时候需要加上.value 姓： 名： 全名：{{fullName}} 全名：{{fullName}} 全名改为：li-si 3.9.【watch】作用：监视数据的变化（和Vue2中的watch作用一致）特点：Vue3中的watch只能监视以下四种数据：ref定义的数据。reactive定义的数据。函数返回一个值（getter函数）。一个包含上述内容的数组。我们在Vue3中使用watch的时候，通常会遇到以下几种情况：* 情况一监视ref定义的【基本类型】数据：直接写数据名即可，监视的是其value值的改变。 情况一：监视【ref】定义的【基本类型】数据 当前求和为：{{sum}} 点我sum+1 * 情况二监视ref定义的【对象类型】数据：直接写数据名，监视的是对象的【地址值】，若想监视对象内部的数据，要手动开启深度监视。注意：若修改的是ref定义的对象中的属性，newValue 和 oldValue 都是新值，因为它们是同一个对象。若修改整个ref定义的对象，newValue 是新值， oldValue 是旧值，因为不是同一个对象了。 情况二：监视【ref】定义的【对象类型】数据 姓名：{{ person.name }} 年龄：{{ person.age }} 修改名字 修改年龄 修改整个人 * 情况三监视reactive定义的【对象类型】数据，且默认开启了深度监视，且深层监视无法关闭。无法监视地址值，因为对象地址值没有改变，本质上assign在原对象上进行的是赋值。newValue和oldValue值相同，都是新值，还是因为对象地址值没有改变，本质上assign在原对象上进行的是赋值。 情况三：监视【reactive】定义的【对象类型】数据 姓名：{{ person.name }} 年龄：{{ person.age }} 修改名字 修改年龄 修改整个人 测试：{{obj.a.b.c}} 修改obj.a.b.c * 情况四监视ref或reactive定义的【对象类型】数据中的某个属性，注意点如下：若该属性值不是【对象类型】即【基本类型】，需要写成函数形式，此时oldValue是旧值，newValue是新值。若该属性值是依然是【对象类型】，可直接编，也可写成函数，建议写成函数。直接写：可以监视到对象内部属性a，b...的变化，但是监视不到整体的变化。整体改变时，对象地址值变化了，所以监视不到了。写函数（不开启深度监视）：监视不到对象内部属性a，b...的变化，但是可以监视到整体的变化，函数返回值监视的是对象的地址值，改变整体是产生一个新对象，所以能监视到，并且新值是新值，旧值是旧值。（不过对象内部属性a，b...的新旧值都是新值）写函数（开启深度监视）推荐：既能监视到对象内部属性a，b...的变化，也可以监视到整体的变化，函数返回值监视的是对象的地址值，改变整体是产生一个新对象，所以能监视到，并且新值是新值，旧值是旧值。（不过对象内部属性a，b...的新旧值都是新值）结论：监视的要是对象里的属性，那么最好写函数式。注意点：若是对象监视的是地址值，需要关注对象内部，需要手动开启深度监视。 情况四：监视【ref】或【reactive】定义的【对象类型】数据中的某个属性 姓名：{{ person.name }} 年龄：{{ person.age }} 汽车：{{ person.car.c1 }}、{{ person.car.c2 }} 修改名字 修改年龄 修改第一台车 修改第二台车 修改整个车 * 情况五监视上述的多个数据 情况五：监视上述的多个数据 姓名：{{ person.name }} 年龄：{{ person.age }} 汽车：{{ person.car.c1 }}、{{ person.car.c2 }} 修改名字 修改年龄 修改第一台车 修改第二台车 修改整个车 3.10. 【watchEffect】官网：立即运行一个函数，同时响应式地追踪其依赖，并在依赖更改时重新执行该函数。watch对比watchEffect都能监听响应式数据的变化，不同的是监听数据变化的方式不同watch：要明确指出监视的数据watchEffect：不用明确指出监视的数据（函数中用到哪些属性，那就监视哪些属性）。示例代码： 需求：水温达到50℃，或水位达到20cm，则联系服务器 水温：{{temp}} 水位：{{height}} 水温+1 水位+10 3.11. 【标签的 ref 属性】作用：用于注册模板引用。用在普通DOM标签上，获取的是DOM节点。用在组件标签上，获取的是组件实例对象。用在普通DOM标签上：名字相同自动关联 尚硅谷 前端 Vue 点我打印内容 用在组件标签上： <Person ref="ren"/> 测试 函数模板使用：可以动态获取ref绑定的元素对象ref绑定为一个函数，会在每次组件更新时都被调用，函数的第一个参数el为元素引用当绑定的元素被卸载时，函数也会被调用一次，此时的 el 参数会是 null { /* 将 el 赋值给一个数据属性或 ref 变量 */ }"> (refAttr[0] = el)">尚硅谷 (refAttr[1] = el)" type="text" /> 点我打印内容 销毁元素并打印内容 3.12. 【props】// 定义一个接口，限制每个Person对象的格式 export interface PersonInter { id:string, name:string, age:number } // 定义一个自定义类型Persons export type Persons = Array<PersonInter>App.vue中代码： <Person :list="persons"/> Person.vue中代码： {{item.name}}--{{item.age}} 3.13. 【生命周期】概念：Vue组件实例在创建时要经历一系列的初始化步骤，在此过程中Vue会在合适的时机，调用特定的函数，从而让开发者有机会在特定阶段运行自己的代码，这些特定的函数统称为：生命周期钩子规律：生命周期整体分为四个阶段，分别是：创建、挂载、更新、销毁，每个阶段都有两个钩子，一前一后。Vue2的生命周期创建阶段：beforeCreate、created挂载阶段：beforeMount、mounted更新阶段：beforeUpdate、updated销毁阶段：beforeDestroy、destroyedVue3的生命周期创建阶段：setup挂载阶段：onBeforeMount、onMounted更新阶段：onBeforeUpdate、onUpdated卸载阶段：onBeforeUnmount、onUnmounted常用的钩子：onMounted(挂载完毕)、onUpdated(更新完毕)、onBeforeUnmount(卸载之前)示例代码： 当前求和为：{{ sum }} 点我sum+1 3.14. 【自定义hook】什么是hook？—— 本质是一个函数，把setup函数中使用的Composition API进行了封装，类似于vue2.x中的mixin。自定义hook的优势：复用代码, 让setup中的逻辑更清楚易懂。示例代码：useSum.ts中内容如下：import {ref,onMounted} from 'vue' export default function(){ let sum = ref(0) const increment = ()=>{ sum.value += 1 } const decrement = ()=>{ sum.value -= 1 } onMounted(()=>{ increment() }) //向外部暴露数据 return {sum,increment,decrement} } useDog.ts中内容如下：import {reactive,onMounted} from 'vue' import axios,{AxiosError} from 'axios' export default function(){ let dogList = reactive<string[]>([]) // 方法 async function getDog(){ try { // 发请求 let {data} = await axios.get('https://dog.ceo/api/breed/pembroke/https://hzqaq.cn-nb1.rains3.com/assert3/random') // 维护数据 dogList.push(data.message) } catch (error) { // 处理错误 const err = <AxiosError>error console.log(err.message) } } // 挂载钩子 onMounted(()=>{ getDog() }) //向外部暴露数据 return {dogList,getDog} }组件中具体使用： 当前求和为：{{sum}} 点我+1 点我-1 加载中...... 再来一只狗 4. 路由4.1. 【对路由的理解】4.2. 【基本切换效果】Vue3中要使用vue-router的最新版本，目前是4版本。安装路由：npm i vue-router路由配置文件代码如下：import {createRouter,createWebHistory} from 'vue-router' import Home from '@/pages/Home.vue' import News from '@/pages/News.vue' import About from '@/pages/About.vue' const router = createRouter({ history:createWebHistory(), routes:[ { path:'/home', component:Home }, { path:'/about', component:About } ] }) export default routermain.ts代码如下：import router from './router/index' app.use(router) app.mount('#app')App.vue代码如下 Vue路由测试 <RouterLink to="/home" active-class="active">首页</RouterLink> <RouterLink to="/news" active-class="active">新闻</RouterLink> <RouterLink to="/about" active-class="active">关于</RouterLink> <RouterView></RouterView> 4.3. 【两个注意点】路由组件通常存放在pages 或 views文件夹，一般组件通常存放在components文件夹。通过点击导航，视觉效果上“消失” 了的路由组件，默认是被卸载掉的，需要的时候再去挂载。路由组件：靠路由规则渲染出来的。route:[{path:/demo,component:demo}]一般组件：亲手写出来的标签。<demo/>4.4.【路由器工作模式】history模式优点：URL更加美观，不带有#，更接近传统的网站URL。缺点：后期项目上线，需要服务端配合处理路径问题，否则刷新会有404错误。const router = createRouter({ history:createWebHistory(), //history模式 /******/ })各版本：vue2——mode:'history'vue3——history:createWebHistory()React——BrowserRouterhash模式优点：兼容性更好，因为不需要服务器端处理路径。缺点：URL带有#不太美观，且在SEO优化方面相对较差。const router = createRouter({ history:createWebHashHistory(), //hash模式 /******/ })4.5. 【to的两种写法】 <router-link active-class="active" to="/home">主页</router-link> <router-link active-class="active" :to="{path:'/home'}">Home</router-link>4.6. 【命名路由】作用：可以简化路由跳转及传参（后面就讲）。给路由规则命名：routes:[ { name:'zhuye', path:'/home', component:Home }, { name:'xinwen', path:'/news', component:News, }, { name:'guanyu', path:'/about', component:About } ]跳转路由： <router-link to="/news/detail">跳转</router-link> <router-link :to="{name:'guanyu'}">跳转</router-link> <router-link :to="{path:'/about'}">跳转</router-link>4.7. 【嵌套路由】编写News的子路由：Detail.vue配置路由规则，使用children配置项：const router = createRouter({ history:createWebHistory(), routes:[ { name:'zhuye', path:'/home', component:Home }, { name:'xinwen', path:'/news', component:News, children:[ { name:'xiang', path:'detail', component:Detail } ] }, { name:'guanyu', path:'/about', component:About } ] }) export default router跳转路由（记得要加完整路径）：<router-link to="/news/detail">xxxx</router-link> <router-link :to="{path:'/news/detail'}">xxxx</router-link>记得去Home组件中预留一个<router-view> <RouterLink v-for="news in newsList" :key="news.id" :to="{path:'/news/detail'}"> {{news.name}} </RouterLink> <RouterView/> 4.8. 【路由传参】query参数传递参数 <router-link to="/news/detail?a=1&b=2&content=欢迎你"> 跳转 </router-link> <RouterLink :to="{ //name:'xiang', //用name也可以跳转 path:'/news/detail', query:{ id:news.id, title:news.title, content:news.content } }" > {{news.title}} </RouterLink>接收参数：import {useRoute} from 'vue-router' const route = useRoute() // 打印query参数 console.log(route.query)params参数传递参数 <RouterLink :to="`/news/detail/001/新闻001/内容001`">{{news.title}}</RouterLink> <RouterLink :to="{ name:'xiang', //用name跳转 params:{ id:news.id, title:news.title, content:news.title } }" > {{news.title}} </RouterLink>接收参数：import {useRoute} from 'vue-router' const route = useRoute() // 打印params参数 console.log(route.params)备注1：传递params参数时，若使用to的对象写法，必须使用name配置项，不能用path。备注2：传递params参数时，需要提前在规则中占位。4.9. 【路由的props配置】作用：让路由组件更方便的收到参数（可以将路由参数作为props传给组件）{ name:'xiang', path:'detail/:id/:title/:content', component:Detail, // props的对象写法，作用：把对象中的每一组key-value作为props传给Detail组件 // props:{a:1,b:2,c:3}, // props的布尔值写法，作用：把收到了每一组params参数，作为props传给Detail组件 // props:true // props的函数写法，作用：把返回的对象中每一组key-value作为props传给Detail组件 props(route){ return route.query } }4.10. 【 replace属性】作用：控制路由跳转时操作浏览器历史记录的模式。浏览器的历史记录有两种写入方式：分别为push和replace：push是追加历史记录（默认值）。replace是替换当前记录。开启replace模式：<RouterLink replace .......>News</RouterLink>4.11. 【编程式导航】路由组件的两个重要的属性：$route和$router变成了两个hooksimport {useRoute,useRouter} from 'vue-router' const route = useRoute() const router = useRouter() console.log(route.query)//接收查询参数 console.log(route.parmas)//接收路径参数 console.log(router.push)//发起跳转 并且新增一条历史记录 const goDetail = () => { router.push({ name: 'xiang', params: { id: '001', title: '新闻001', content: '内容001' } }) } console.log(router.replace)//发现跳转 并且替换当前历史记录4.12. 【重定向】作用：将特定的路径，重新定向到已有路由。具体编码：{ path:'/', redirect:'/about' }5. pinia5.1【准备一个效果】5.2【搭建 pinia 环境】第一步：npm install pinia第二步：操作src/main.tsimport { createApp } from 'vue' import App from './App.vue' /* 引入createPinia，用于创建pinia */ import { createPinia } from 'pinia' /* 创建pinia */ const pinia = createPinia() const app = createApp(App) /* 使用插件 */{} app.use(pinia) app.mount('#app')此时开发者工具中已经有了pinia选项5.3【存储+读取数据】Store是一个保存：状态、业务逻辑 的实体，每个组件都可以读取、写入它。它有三个概念：state、getter、action，相当于组件中的： data、 computed 和 methods。具体编码：src/store/count.ts// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useCountStore = defineStore('count',{ // 动作 actions:{}, // 状态 state(){ return { sum:6 } }, // 计算 getters:{} })具体编码：src/store/talk.ts// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useTalkStore = defineStore('talk',{ // 动作 actions:{}, // 状态 state(){ return { talkList:[ {id:'yuysada01',content:'你今天有点怪，哪里怪？怪好看的！'}, {id:'yuysada02',content:'草莓、蓝莓、蔓越莓，你想我了没？'}, {id:'yuysada03',content:'心里给你留了一块地，我的死心塌地'} ] } }, // 计算 getters:{} })组件中使用state中的数据 当前求和为：{{ sumStore.sum }} {{ talk.content }} 5.4.【修改数据】(三种方式)第一种修改方式，直接修改countStore.sum = 666第二种修改方式：批量修改countStore.$patch({ sum:999, school:'atguigu' }) countStore.$patch((state) => { state.sum += 10 state.school = '北大' }) countStore.$patch((state) => { state.list.push({ id: 1, name: '新数据' }) })第三种修改方式：借助action修改（action中可以编写一些业务逻辑）import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { // 必须有 state: () => ({ sum: 0 }), actions: { // 加 increment(value: number) { if (this.sum < 10) { this.sum += value } }, // 减 decrement(value: number) { if (this.sum > 1) { this.sum -= value } } } })组件中调用action即可// 使用countStore const countStore = useCountStore() // 调用对应action countStore.increment(n.value)setup 写法defineStore('count', () => { const sum = ref(0) function increment() { sum.value++ } return { sum, increment } }) 当前值：{{ countStore.sum }} +1 5.5.【storeToRefs】借助storeToRefs将store中的数据转为ref对象，方便在模板中使用。注意：pinia提供的storeToRefs只会将数据做转换，而Vue的toRefs会转换store中数据。 当前求和为：{{sum}} 5.6.【getters】概念：当state中的数据，需要经过处理后再使用时，可以使用getters配置。追加getters配置。// 引入defineStore用于创建store import {defineStore} from 'pinia' // 定义并暴露一个store export const useCountStore = defineStore('count',{ // 动作 actions:{ /************/ }, // 状态 state(){ return { sum:1, school:'atguigu' } }, // 计算 getters:{ bigSum:(state):number => state.sum *10, upperSchool():string{ return this. school.toUpperCase() } } })组件中读取数据：const {increment,decrement} = countStore let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)5.7.【$subscribe】通过 store 的 $subscribe() 方法侦听 state 及其变化// stores/talk.ts import { defineStore } from 'pinia' import { ref } from 'vue' export const useTalkStore = defineStore('talk', () => { // 从本地读取（初始化） const talkList = ref(JSON.parse(localStorage.getItem('talk') || '[]')) function addTalk(talk: string) { talkList.value.push(talk) } function removeTalk(index: number) { talkList.value.splice(index, 1) } return { talkList, addTalk, removeTalk } }) 5.8. 【store组合式写法】import {defineStore} from 'pinia' import axios from 'axios' import {nanoid} from 'nanoid' import {reactive} from 'vue' export const useTalkStore = defineStore('talk',()=>{ // talkList就是state const talkList = reactive( JSON.parse(localStorage.getItem('talkList') as string) || [] ) // getATalk函数相当于action async function getATalk(){ // 发请求，下面这行的写法是：连续解构赋值+重命名 let {data:{content:title}} = await axios.get('https://api.uomg.com/api/rand.qinghua?format=json') // 把请求回来的字符串，包装成一个对象 let obj = {id:nanoid(),title} // 放到数组中 talkList.unshift(obj) } return {talkList,getATalk} })6. 组件通信Vue3组件通信和Vue2的区别：移出事件总线，使用mitt代替。vuex换成了pinia。把.sync优化到了v-model里面了。把$listeners所有的东西，合并到$attrs中了。$children被砍掉了。常见搭配形式：6.1. 【props】概述：props是使用频率最高的一种通信方式，常用与 ：父 ↔ 子。若 父传子：属性值是非函数。若 子传父：属性值是函数。父组件： 父组件， 我的车：{{ car }} 儿子给的玩具：{{ toy }} <Child :car="car" :getToy="getToy"/> 子组件 子组件 我的玩具：{{ toy }} 父给我的车：{{ car }} 玩具给父亲 6.2. 【自定义事件】概述：自定义事件常用于：子 => 父。注意区分好：原生事件、自定义事件。原生事件：事件名是特定的（click、mosueenter等等）事件对象$event: 是包含事件相关信息的对象（pageX、pageY、target、keyCode）自定义事件：事件名是任意名称事件对象$event: 是调用emit时所提供的数据，可以是任意类型！！！命名方式尽量不要驼峰式，而是采取keybab-case式，即send-toy示例： <Child @send-toy="saveToy"/> 测试 //父组件中，自定义事件被触发时所调用的函数： function saveToy(value:string){ console.log(value) } 子组件 发送玩具 6.3. 【mitt】概述：与消息订阅与发布（pubsub）功能类似，可以实现任意组件间通信。安装mittnpm i mitt新建文件：src\utils\emitter.tson 触发事件off 移除事件all.clear 移除全部事件// 引入mitt import mitt from "mitt"; // 创建emitter const emitter = mitt() /* // 绑定事件 emitter.on('abc',(value)=>{ console.log('abc事件被触发',value) }) emitter.on('xyz',(value)=>{ console.log('xyz事件被触发',value) }) setInterval(() => { // 触发事件 emitter.emit('abc',666) emitter.emit('xyz',777) }, 1000); setTimeout(() => { // 清理事件 emitter.all.clear() }, 3000); */ // 创建并暴露mitt export default emitter接收数据的组件中：绑定事件、同时在销毁前解绑事件：import emitter from "@/utils/emitter"; import { onUnmounted } from "vue"; // 绑定事件 emitter.on('send-toy',(value)=>{ console.log('send-toy事件被触发',value) }) onUnmounted(()=>{ // 解绑事件 emitter.off('send-toy') })【第三步】：提供数据的组件，在合适的时候触发事件import emitter from "@/utils/emitter"; function sendToy(){ // 触发事件 emitter.emit('send-toy',toy.value) }注意这个重要的内置关系，总线依赖着这个内置关系6.4.【v-model】概述：实现 父↔子 之间相互通信。前序知识 —— v-model的本质 <input type="text" :value="userName" @input="userName =(<HTMLInputElement>$event.target).value" >($event.target)这个是ts的类型断言，target一定是html元素而不为空数据到页面 :value="userName" 页面到数据 @input="userName =(<HTMLInputElement>$event.target).value" 组件标签上的v-model的本质：:moldeValue ＋ update:modelValue事件。 <AtguiguInput v-model="userName"/> <AtguiguInput :modelValue="userName" @update:model-value="userName = $event"/>在vue3中：数据到页面 :modelValue="userName" 页面到数据 @update:model-value="userName = $event" AtguiguInput组件中： 也可以更换value，例如改成abc <AtguiguInput v-model:abc="userName"/> <AtguiguInput :abc="userName" @update:abc="userName = $event"/>AtguiguInput组件中： 如果value可以更换，那么就可以在组件标签上多次使用v-model<AtguiguInput v-model:abc="userName" v-model:xyz="password"/>关于$event到底是什么？什么时候能够.target？对于原生事件，$event就是事件对象====>能.target对于自定义事件，$event就是触发事件时，所传递的对象====>不能.target6.5.【$attrs 】概述：$attrs用于实现当前组件的父组件，向当前组件的子组件通信（祖→孙）。具体说明：$attrs是一个对象，包含所有父组件传入的标签属性。注意：$attrs会自动排除props中声明的属性(可以认为声明过的 props 被子组件自己“消费”了)父组件： 父组件 <Child :a="a" :b="b" :c="c" :d="d" v-bind="{x:100,y:200}" :updateA="updateA"/> 子组件： 子组件 <GrandChild v-bind="$attrs"/> 孙组件： 孙组件 a：{{ a }} b：{{ b }} c：{{ c }} d：{{ d }} x：{{ x }} y：{{ y }} 点我更新A 6.6. 【$refs、$parent】概述：$refs用于 ：父→子。$parent用于：子→父。原理如下：属性说明$refs值为对象，包含所有被ref属性标识的DOM元素或组件实例。$parent值为对象，当前组件的父组件实例对象。子组件需要将数据暴露出来，父组件才能被允许使用；同样的，父组件把需要子组件操作的数据暴露出来，子组件才能拿到使用。// 宏函数把数据交给外部 defineExpose({ toy, book })6.7. 【provide、inject】概述：实现祖孙组件直接通信具体使用：在祖先组件中通过provide配置向后代组件提供数据在后代组件中通过inject配置来声明接收数据具体编码：【第一步】父组件中，使用provide提供数据 父组件 资产：{{ money }} 汽车：{{ car }} 资产+1 汽车价格+1 <Child/> 注意：子组件中不用编写任何东西，是不受到任何打扰的【第二步】孙组件中使用inject配置项接受数据。 我是孙组件 资产：{{ money }} 汽车：{{ car }} 点我 6.8. 【pinia】参考之前pinia部分的讲解6.9. 【slot】1. 默认插槽父组件中： <Category title="今日热门游戏"> {{ g.name }} </Category> 子组件中： {{ title }} <slot></slot> 2. 具名插槽父组件中： <Category title="今日热门游戏"> {{ g.name }} 更多 </Category> 子组件中： {{ title }} <slot name="s1"></slot> <slot name="s2"></slot> 3. 作用域插槽理解：数据在组件的自身（子组件），但根据数据生成的结构需要组件的使用者（父组件）来决定。（新闻数据在News组件中，但使用数据所遍历出来的结构由App组件决定）具体编码：<Game v-slot="params"> <Game v-slot:default="params"> <Game #default="params"> 父组件中： <Game v-slot="params"> <!-- <Game v-slot:default="params"> --> <!-- <Game #default="params"> --> {{ g.name }} </Game> 子组件中： 今日游戏榜单 <slot :games="games" a="哈哈"></slot> 7. 其它 API7.1.【shallowRef 与 shallowReactive 】shallowRef作用：创建一个响应式数据，但只对顶层属性进行响应式处理。用法：let myVar = shallowRef(initialValue);特点：只跟踪引用值的变化，不关心值内部的属性变化。shallowReactive作用：创建一个浅层响应式对象，只会使对象的最顶层属性变成响应式的，对象内部的嵌套属性则不会变成响应式的用法：const myObj = shallowReactive({ ... });特点：对象的顶层属性是响应式的，但嵌套对象的属性不是。总结通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的，对所有深层的对象不会做任何处理，避免了对每一个内部属性做响应式所带来的性能成本，这使得属性的访问变得更快，可提升性能。7.2.【readonly 与 shallowReadonly】readonly作用：用于创建一个对象的深只读副本。用法：const original = reactive({ ... }); const readOnlyCopy = readonly(original);特点：对象的所有嵌套属性都将变为只读。任何尝试修改这个对象的操作都会被阻止（在开发模式下，还会在控制台中发出警告）。应用场景：创建不可变的状态快照。保护全局状态或配置不被修改。shallowReadonly作用：与 readonly 类似，但只作用于对象的顶层属性。用法：const original = reactive({ ... }); const shallowReadOnlyCopy = shallowReadonly(original);特点：只将对象的顶层属性设置为只读，对象内部的嵌套属性仍然是可变的。适用于只需保护对象顶层属性的场景。7.3.【toRaw 与 markRaw】toRaw作用：用于获取一个响应式对象的原始对象， toRaw 返回的对象不再是响应式的，不会触发视图更新。官网描述：这是一个可以用于临时读取而不引起代理访问/跟踪开销，或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用，请谨慎使用。何时使用？ —— 在需要将响应式对象传递给非 Vue 的库或外部系统时，使用 toRaw 可以确保它们收到的是普通对象具体编码：import { reactive,toRaw,markRaw,isReactive } from "vue"; /* toRaw */ // 响应式对象 let person = reactive({name:'tony',age:18}) // 原始对象 let rawPerson = toRaw(person) /* markRaw */ let citysd = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败，因为citys被markRaw标记了 let citys2 = reactive(citys) console.log(isReactive(person)) console.log(isReactive(rawPerson)) console.log(isReactive(citys)) console.log(isReactive(citys2))markRaw作用：标记一个对象，使其永远不会变成响应式的。例如使用mockjs时，为了防止误把mockjs变为响应式对象，可以使用 markRaw 去标记mockjs编码：/* markRaw */ let citys = markRaw([ {id:'asdda01',name:'北京'}, {id:'asdda02',name:'上海'}, {id:'asdda03',name:'天津'}, {id:'asdda04',name:'重庆'} ]) // 根据原始对象citys去创建响应式对象citys2 —— 创建失败，因为citys被markRaw标记了 let citys2 = reactive(citys)7.4.【customRef】作用：创建一个自定义的ref，并对其依赖项跟踪和更新触发进行逻辑控制。实现防抖效果（useSumRef.ts）封装在hooks里面：import {customRef } from "vue"; export default function(initValue:string,delay:number){ //track（跟踪），trigger（触发） let msg = customRef((track,trigger)=>{ let timer:number return { //get何时调用？——msg被读取时候 get(){ track() // 告诉Vue数据msg很重要，要对msg持续关注，一旦变化就更新 return initValue }, //set何时调用？——msg被修改时候 set(value){ clearTimeout(timer) timer = setTimeout(() => { initValue = value trigger() //通知Vue数据msg变化了 }, delay); } } }) return {msg} }组件中使用： {{ msg }} 8. Vue3新组件8.1. 【Teleport】什么是Teleport？—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。<teleport to='body' > 我是一个弹窗 我是弹窗中的一些内容 关闭弹窗 </teleport>8.2. 【Suspense】等待异步组件时渲染一些额外内容，让应用有更好的用户体验使用步骤：异步引入组件使用Suspense包裹组件，并配置好default 与 fallbackimport { defineAsyncComponent,Suspense } from "vue"; const Child = defineAsyncComponent(()=>import('./Child.vue'))<Child/>是一个包含了异步请求的子组件 我是Child组件 当前求和为：{{ sum }} 我是App组件 <Suspense> <Child/> 加载中....... </Suspense> 8.3.【全局API转移到应用对象】app.component 注册全局组件app.config 配置对象app.directive 注册全局指令app.mountapp.unmountapp.use8.4.【其他】过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。keyCode 作为 v-on 修饰符的支持。v-model 指令在组件上的使用已经被重新设计，替换掉了 v-bind.sync。v-if 和 v-for 在同一个元素身上使用时的优先级发生了变化。移除了$on、$off 和 $once 实例方法。移除了过滤器 filter。移除了$children 实例 propert。......9.补充9.1静态转发export default defineConfig({ plugins: [ vue(), VueSetupExtend() ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, server:{ port: 5173, proxy: { '/api': { // 前缀替换成代理地址： 5173 -> 8080 后端tomcat服务器端口号 target: 'http://localhost:8080/admin', ws: false, secure: false, //open: true → Vite 会自动打开你的默认浏览器，并访问 http://localhost:5174 false 则为不打开 替换路径 changeOrigin: true, // /api去掉，变成空串，因为它只是一个标识而已，并不是路径 rewrite: (path) => path.replace(/^\/api/, '') } } } })