Vue的两大生态以及组件通信

目录

[一、Vue Router路由](#一、Vue Router路由)

概述

路由基本流程

路由创建的index.js文件

启动路由的main.js文件

页面展示App.vue文件

注意点

路由工作模式

history模式

hash模式

App.vue下to的写法

命名路由

嵌套路由

路由传参

query参数

params参数

路由的props配置

补充内容

replace属性(声明式导航)

编程式导航

重定向redirect

二、Pinia全局状态管理

搭建Pinia环境

存储和读取数据

定义Store

在组件中使用

修改数据

补充方法和配置

storeToRefs

getters

subscribe

store组合式写法

三、组件通信

区别与通信概述

vue3区别于vue2

常见搭配形式

props

自定义事件

补充用法

mitt

v-model

[attrs](#attrs)

refs、parent(了解即可)

provide、inject

slot

默认插槽

具名插槽

作用域插槽

pinia


本篇内容较多,查看目录学习自己需要的即可

一、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
  • 首先进行导入:创建路由的方式和页面组件
  • 然后进行创建路由
TypeScript 复制代码
const router = createRouter({...})
  • 配置history 模式和路由表
TypeScript 复制代码
history:
routes:
  • 最后导出路由
TypeScript 复制代码
export default router

启动路由的main.js文件

TypeScript 复制代码
import './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 应用实例
TypeScript 复制代码
createApp(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
缺点:后期项目上线,需要服务端配合处理路径问题,否则刷新会报错

语法

TypeScript 复制代码
const router = createRouter({
history:createWebHistory(), //history模式
/******/
})

hash模式

优点:兼容性更好,因为不需要服务器端处理路径
缺点: URL 带有 # 不太美观,且在 SEO 优化方面相对较差
语法

TypeScript 复制代码
const 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>
  • 字符串写法:简易跳转,不能传参,用于平时练习
  • 对象写法:功能更强,支持跳转 + 携带参数,项目常用

命名路由

顾名思义:命名路由就是给路由起名字,用名字跳转,不用写长路径

TypeScript 复制代码
const routes = [
  {
    path: '/home',
    name: 'Home', // 路由别名【命名】,名字唯一不能重复
    component: Home
  },

在路由的index.js中用name进行命名 ,App.vue上就可以根据name直接定位

TypeScript 复制代码
<router-link :to="{ name:'Home' }">首页</router-link>

嵌套路由

好处:

  • 相比较普通路由,访问路径时,页面整页全部替换
  • 而嵌套路由会父页面骨架保留,局部替换子内容
TypeScript 复制代码
const 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(入栈,默认):新增历史,可回退
TypeScript 复制代码
import {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 属性
TypeScript 复制代码
router.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环境

在终端中运行

bash 复制代码
npm install pinia

在src/main.ts文件下,在原有代码基础上插入pinia

TypeScript 复制代码
import { 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()
    }
  }
})
  • 组件中读取数据
TypeScript 复制代码
const {increment,decrement} = countStore
let {sum,school,bigSum,upperSchool} = storeToRefs(countStore)

subscribe

通过 store 的 $subscribe() 方法侦听 state 及其变化

TypeScript 复制代码
talkStore.$subscribe((mutate,state)=>{
    console.log('LoveTalk',mutate,state)
    localStorage.setItem('talk',JSON.stringify(talkList.value))
})

store组合式写法

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

三、组件通信

区别与通信概述

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 是回调函数数组

安装

TypeScript 复制代码
npm i mitt

新建文件src/utils/emitter.ts

TypeScript 复制代码
import 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知识学习就行)

相关推荐
甜汤圆1 小时前
Python 里**自定义数据单元**
前端
cidy_981 小时前
将 Figma 接入 Codex MCP:从 `/plugins` 到本地插件配置的完整教程
前端
vivo互联网技术1 小时前
动效开发不踩坑:几种动效实现方案对比与实战选型
前端·性能优化·动效
Csvn1 小时前
【Vue3】Composition API vs Options API —— 什么场景该选哪个
前端
Csvn1 小时前
Vue3 迁移血泪史:v-model 的 .sync 陷阱,90% 升级项目都会踩
前端·vue.js
光影少年1 小时前
js单线程,为什在node环境下的js可以处理高并发请求?
前端·javascript·掘金·金石计划
vim怎么退出2 小时前
Dive into React——事件系统
前端·react.js·源码阅读
moMo2 小时前
# JavaScript 的“等等我”:聊聊同步与异步
javascript
KaMeidebaby2 小时前
卡梅德生物技术快报|重组蛋白的表达和纯化:工艺调试全记录:大肠杆菌体系重组蛋白的表达和纯化参数标定(肠激酶轻链案例)
前端·人工智能·算法·数据挖掘·数据分析