目录
[一、Vue Router路由](#一、Vue Router路由)
[attrs](#attrs)
本篇内容较多,查看目录学习自己需要的即可
一、Vue Router路由
概述
在 Vue3 里,路由(Vue Router) 就是页面跳转管理器
核心作用:
- 实现页面无刷新跳转 单页应用(SPA)不会整页刷新,只更新页面内容,体验更流畅
- 管理 URL 与组件的映射关系 哪个 URL 展示哪个页面,由路由统一配置
- 支持路由参数、嵌套、守卫等高级能力 满足跳转传参、权限控制、子页面等业务需求
总的来说,就是根据浏览器地址栏的 URL,匹配并展示对应的组件(页面),让单页应用(SPA)实现无刷新切换页面
路由基本流程
路由创建的index.js文件
TypeScript// 路由核心导入 import { createRouter, createWebHistory } from 'vue-router' // 页面组件导入 import Home from '@/pages/Home.vue' import About from '@/pages/About.vue' // 创建路由实例 const router = createRouter({ history: createWebHistory(), routes: [ { path: '/home', component: Home }, { path: '/about', component: About } ] }) export default router
- 首先进行导入:创建路由的方式和页面组件
- 然后进行创建路由
TypeScriptconst router = createRouter({...})
- 配置history 模式和路由表
TypeScripthistory: routes:
- 最后导出路由
TypeScriptexport default router
启动路由的main.js文件
TypeScriptimport './assets/main.css' import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app')导入不必多说
- 基于根组件
App.vue创建 Vue 应用实例
TypeScriptcreateApp(App)
- 全局安装路由插件 ,项目所有组件(App.vue)可用**
<router-link>、<router-view>**
TypeScript.use(router)
- 把
App.vue渲染到 html 中 id 为app的 DOM 盒子里,项目正式运行
TypeScript.mount(#app)
页面展示App.vue文件
TypeScript<template> <!-- 路由导航跳转 --> <nav> <router-link to="/home">首页</router-link> <span> | </span> <router-link to="/about">关于</router-link> </nav> <!-- 路由页面渲染出口,匹配的Home/About组件在这里显示 --> <router-view /> </template>
<router-link>:无刷新修改浏览器地址栏 URL,替代原生 a 标签
to="/home":点击后浏览器地址变成/home,路径必须和 router/index.js 里的 path 完全一致<router-view />(重要): 路由监测到地址变化,拿着当前 URL 去
router/index.js的**routes数组匹配**
main.js和index.js的关系index.js 是 "路由说明书",main.js 是 "Vue 总启动入口"
Vue 启动时,通过 main.js 把 index.js 里的路由 "安装" 到项目里
他们是挂载和被挂载的关系
注意点
- 路由组件一般放在views或者pages 文件夹下,一般组件放在components,养成好习惯
- 路由组件在视觉上消失时,是默认被卸载 了,而再次点击该组件展示内容是重新被挂载了
路由工作模式
知道怎么使用,优缺点了解即可
history模式
优点: URL 更加美观,不带有 # ,更接近传统的网站 URL
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会报错语法
TypeScriptconst router = createRouter({ history:createWebHistory(), //history模式 /******/ })
hash模式
优点:兼容性更好,因为不需要服务器端处理路径
缺点: URL 带有 # 不太美观,且在 SEO 优化方面相对较差
语法
TypeScriptconst router = createRouter({ history:createWebHashHistory(), //hash模式 /******/ })
App.vue下to的写法
TypeScript<!-- 第一种:to的字符串写法 --> <router-link active-class="active" to="/home">主页</router-link> <!-- 第二种:to的对象写法 --> <router-link active-class="active" :to="{path:'/home'}">Home</router-link>
- 字符串写法:简易跳转,不能传参,用于平时练习
- 对象写法:功能更强,支持跳转 + 携带参数,项目常用
命名路由
顾名思义:命名路由就是给路由起名字,用名字跳转,不用写长路径
TypeScriptconst routes = [ { path: '/home', name: 'Home', // 路由别名【命名】,名字唯一不能重复 component: Home },在路由的index.js中用name进行命名 ,App.vue上就可以根据name直接定位
TypeScript<router-link :to="{ name:'Home' }">首页</router-link>
嵌套路由
好处:
- 相比较普通路由,访问路径时,页面整页全部替换
- 而嵌套路由会父页面骨架保留,局部替换子内容
TypeScriptconst routes = [ { path:'/about', //父路由地址 name:'About', component:About, //父组件 //子路由数组 = 嵌套路由 children:[ // 空path:访问 /about 默认显示Tab1 { path:'', component:Tab1 }, // 子path不加/,自动拼接父路径 → /about/tab2 { path:'tab2', name:'Tab2', component:Tab2 } ] } ]上面写父组件路由,下面套子组件;
当访问子路由App.vue时
TypeScript<template> <!-- 1.一级导航,跳父路由 --> <router-link to="/about">去关于页</router-link> <!-- 2.一级路由渲染坑,放About整个页面 --> <router-view></router-view> </template>跳入父路由About.vue
TypeScript<!-- 子路由跳转 --> <router-link to="/about">Tab1</router-link> <router-link :to="{name:'Tab2'}">Tab2</router-link> <!-- 子页面渲染位置 --> <router-view/>
- App 只负责跳进
/about,- About 页面切换 tab1/tab2
路由传参
query参数
特点:? 拼接地址栏,刷新不丢失
传递过程
TypeScript<router-link :to="{path:'/detail', query:{id:1,name:'苹果'}}" >跳转</router-link>接收过程
TypeScript<script setup> import {useRoute} from 'vue-router' const route=useRoute() console.log(route.query.id) </script>
params参数
特点:路径拼接,/:id
传递过程
TypeScript{ path:'/detail/:id', // 提前声明占位 name:'Detail', component:Detail }接收过程
TypeScript<router-link :to="{name:'Detail', params:{id:66}}" >详情</router-link> // js router.push({name:'Detail',params:{id:66}})
而props可以让路由更方便的收到参数,所以最好在传参基础上使用props
路由的props配置
TypeScript{ path:'/detail/:id', name:'Detail', component:Detail, //写法1:props:true → 只接收params props:true, //写法2:对象 → 固定死数据,极少用 props:{a:100}, //写法3:函数 → params+query全能接【项目常用】 props:(route)=>({id:route.params.id,key:route.query.key}) }然后目标组件接收(用defineProps)
TypeScript<script setup> defineProps(['id']) // 直接使用{{id}} </script>
补充内容
replace属性(声明式导航)
作用
- 跳转时替换当前历史记录,不会新增浏览器历史栈
- 点击返回按钮无法回到上一页,适合登录、详情页跳转
TypeScript<!-- 写法:添加replace --> <router-link replace to="/detail">跳转</router-link> <!-- 搭配传参 --> <router-link replace :to="{path:'/detail', query:{id:1}}" >跳转</router-link>
编程式导航
依托
useRouter实现,有两种模式:
- push(入栈,默认):新增历史,可回退
TypeScriptimport {useRouter} from 'vue-router' const router = useRouter() // path+query router.push({path:'/detail',query:{id:1}}) // name+params router.push({name:'Detail',params:{id:1}})
- replace(替换栈,无返回):对应上面 replace 属性
TypeScriptrouter.replace({path:'/detail',query:{id:1}})
重定向redirect
访问 A 地址自动跳转至 B 地址,常用在默认首页、空路由、旧地址兼容
TypeScript// 基础写法 const routes=[ {path:'/',redirect:'/home'}, {path:'/about',component:About,children:[ {path:'',redirect:'tab1'}, {path:'tab1',component:Tab1} ]} ] // 对象写法 {path:'/',redirect:{name:'Home'}}
二、Pinia全局状态管理
搭建Pinia环境
在终端中运行
bashnpm install pinia在src/main.ts文件下,在原有代码基础上插入pinia
TypeScriptimport { 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')这样在F12中有pinia选项了
存储和读取数据
Store 是一个保存状态、业务逻辑的实体,组件都可以读写它
Store有三个概念:state、getter、action(相当于组件中的data、computed、methods)
定义Store
分别是Store文件夹下的count.ts和talk.ts文件(自己创建)
TypeScript// count.ts import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ sum: 6 }), // 数据 actions: {}, // 方法 getters: {} // 计算属性 }) // talk.ts import { defineStore } from 'pinia' export const useTalkStore = defineStore('talk', { state: () => ({ talkList: [ { id: '01', content: '你今天有点怪,怪好看的!' } ] }) })在组件中使用
组件引入后直接定义名.属性取值
TypeScript<template> <div>{{ countStore.sum }}</div> <li v-for="item in talkStore.talkList" :key="item.id"> {{ item.content }} </li> </template> <script setup> import { useCountStore } from '@/store/count' import { useTalkStore } from '@/store/talk' const countStore = useCountStore() const talkStore = useTalkStore() </script>
修改数据
三种方法
这是三方法的
TypeScript// store.ts import { defineStore } from 'pinia' export const useCountStore = defineStore('count', { state: () => ({ sum: 6 }), actions: { // 第三种:action 带逻辑修改 add(val: number) { this.sum += val } } })这是组件中的使用,一方法和二方法都是可以直接用的
TypeScript// 组件中 const countStore = useCountStore() // 1. 直接改 countStore.sum = 666 // 2. 批量改 countStore.$patch({ sum: 999 }) // 3. 调用 action 改(带逻辑/复用) countStore.add(10)
- 直接改:最简单,单个赋值
- $patch:批量改多个状态
- action:带逻辑 / 复用,最规范
补充方法和配置
storeToRefs
- 借助 storeToRefs 将 store 中的数据转为 ref 对象,方便在模板中使用
- 注意: pinia 提供的 storeToRefs 只会将数据做转换,而 Vue 的 toRefs 会转换 store 中数据
TypeScript<template> <div class="count"> <h2>当前求和为:{{ sum }}</h2> </div> </template> <script setup lang="ts" name="Count"> import { useCountStore } from '@/store/count' import { storeToRefs } from 'pinia' // 获取 store 实例 const countStore = useCountStore() // 解构出响应式数据 const { sum } = storeToRefs(countStore) </script>
getters
- 概念:当 state 中的数据,需要经过处理后再使用时,可以使用 getters 配置
- 追加 getters 配置
TypeScript// 引入defineStore用于创建store import { defineStore } from 'pinia' // 定义并暴露一个store export const useCountStore = defineStore('count', { // 动作 actions: { /************/ }, // 状态 state() { return { sum: 1, school: 'jdfs' } }, // 计算 getters: { bigSum: (state): number => state.sum * 10, upperSchool(): string { return this.school.toUpperCase() } } })
- 组件中读取数据
TypeScriptconst {increment,decrement} = countStore let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)
subscribe
通过 store 的 $subscribe() 方法侦听 state 及其变化
TypeScripttalkStore.$subscribe((mutate,state)=>{ console.log('LoveTalk',mutate,state) localStorage.setItem('talk',JSON.stringify(talkList.value)) })
store组合式写法
TypeScriptimport {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} })
三、组件通信
区别与通信概述
vue3区别于vue2
- 移出事件总线,使用 mitt 代替
- vuex 换成了 pinia
- 把 .sync 优化到了 v-model 里面了
- 把 listeners 所有的东西,合并到 attrs 中了
- $children 被砍掉了。
常见搭配形式
组件关系 传递方式 父传子 1. props 2. v-model 3. $refs 4. 默认插槽、具名插槽 子传父 1. props 2. 自定义事件 3. v-model 4. $parent 5. 作用域插槽 祖传孙、孙传祖 1. $attrs 2. provide、inject 兄弟间、任意组件间 1. mitt 2. pinia
props
使用频率最高的通信方式,父子相互通信
- 若 父传子 :属性值是 非函数
子组件
TypeScript<script setup> defineProps(['msg']) </script> <template> 子组件:{{ msg }} </template>父组件
TypeScript<script setup> import Child from './Child.vue' </script> <template> <Child msg="父组件数据" /> </template>
- 若 子传父 :属性值是 函数
子组件
TypeScript<script setup> const emit = defineEmits(['send']) const send = () => emit('send', '子组件数据') </script> <template> <button @click="send">传值</button> </template>父组件
TypeScript<script setup> import Child from './Child.vue' </script> <template> <Child @send="(val)=>console.log(val)" /> </template>
自定义事件
常用于子传父
要区别原生事件和自定义事件
原生事件:
- 事件名是特定的( click 、 mosueenter 等等)
- 事件对象 $event : 是包含事件相关信息的对象( pageX 、 pageY 、 target 、 keyCode )
自定义事件:
- 事件名是任意名称
- 事件对象 $event : 是调用 emit 时所提供的数据,可以是任意类型
TypeScript<!--在父组件中,给子组件绑定自定义事件:--> <Child @send-toy="toy = $event"/> <!--注意区分原生事件与自定义事件中的$event--> <button @click="toy = $event">测试</button> //子组件中,触发事件: this.$emit('send-toy', 具体数据)
补充用法
mitt
- 可以实现任意组件间通信
- mitt = 一个全局 Map 容器:key 是事件名,value 是回调函数数组
安装
TypeScriptnpm i mitt新建文件src/utils/emitter.ts
TypeScriptimport mitt from 'mitt' const emitter = mitt() export default emitter接收数据的组件
TypeScript<script setup> import emitter from '@/utils/emitter' import { onUnmounted } from 'vue' // 监听事件 const handler = (val: any) => { console.log('收到:', val) } emitter.on('send-toy', handler) // 组件销毁时解绑(重要,防止内存泄漏) onUnmounted(() => { emitter.off('send-toy', handler) }) </script>组件中使用
TypeScript<script setup> import emitter from '@/utils/emitter' // 随便什么时候触发 const send = () => { emitter.emit('send-toy', { name: '玩具车', price: 20 }) } </script> <template> <button @click="send">发送数据</button> </template>
v-model
- 实现 父与****子之间相互通信
- 本质是
:modelValue+@update:modelValue的组合子组件
TypeScript<!-- 子组件 Child.vue --> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>父组件
TypeScript<!-- 父组件 Parent.vue --> <script setup> import { ref } from 'vue' import Child from './Child.vue' const msg = ref('') </script> <template> <Child v-model="msg" /> <p>父组件:{{ msg }}</p> </template>
$attrs
- $attrs 用于实现 当前组件的父组件 ,向 当前组件的子组件 通信(祖孙之间)
- $attrs 是一个对象,包含所有父组件传入的标签属性
祖组件
TypeScript<!-- 祖组件 --> <template> <Child :name="name" :age="age" @click="handleClick" /> </template> <script setup> import Child from './Child.vue' const name = '张三' const age = 18 const handleClick = () => {} </script>子组件
TypeScript<!-- 子组件(中间层透传) --> <template> <!-- 把 attrs 透传给孙组件 --> <GrandChild v-bind="$attrs" /> </template> <script setup> import GrandChild from './GrandChild.vue' // 子组件不声明 props,所以 name/age/click 都在 $attrs 里 </script>孙组件
TypeScript<!-- 孙组件 --> <script setup> defineProps(['name', 'age']) </script> <template> <div>{{ name }} - {{ age }}</div> </template>
refs、parent(了解即可)
概述:
- $refs 用于 :父**→**子
- $parent 用于:子**→**父
|---------|----------------------------------|
| 属性 | 说明 |
| refs | 值为对象,包含所有被 ref 属性标识的 DOM 元素或组件实例 | | parent | 值为对象,当前组件的父组件实例对象 |
provide、inject
- 实现 祖孙组件 直接通信
- 在祖先组件中通过 provide 配置向后代组件提供数据
- 在后代组件中通过 inject 配置来声明接收数据
祖组件
TypeScript<!-- 祖组件 --> <script setup> import { provide, ref } from 'vue' const theme = ref('dark') // 提供数据 provide('theme', theme) </script>任意后代组件
TypeScript<!-- 任意后代组件 --> <script setup> import { inject } from 'vue' // 注入数据 const theme = inject('theme') console.log(theme.value) // 'dark' </script>
slot
父组件向子组件传递内容 / 模板,分为三个插槽
默认插槽
子组件
TypeScript<!-- 子组件 --> <template> <div class="card"> <slot /> <!-- 父组件传入的内容会被渲染在这里 --> </div> </template>父组件
TypeScript<!-- 父组件 --> <template> <Child> <p>这是父组件传入的内容</p> </Child> </template>
具名插槽
子组件
TypeScript<!-- 子组件 --> <template> <div class="card"> <header><slot name="header" /></header> <main><slot /></main> <footer><slot name="footer" /></footer> </div> </template>父组件
TypeScript<!-- 父组件 --> <template> <Child> <template #header>标题</template> <p>正文内容</p> <template #footer>底部信息</template> </Child> </template>
作用域插槽
子组件
TypeScript<!-- 子组件 --> <template> <div> <slot :user="user" /> <!-- 向父组件暴露子组件数据 --> </div> </template> <script setup> const user = { name: '张三', age: 18 } </script>父组件
TypeScript<!-- 父组件 --> <template> <Child v-slot="{ user }"> <p>{{ user.name }} - {{ user.age }}</p> </Child> </template>
pinia
(也是一种通信方式,结合之前关于pinia知识学习就行)