目录

一 学习前提

二 选项式与组合式

三 setup

四 ref与reactive

五 torefs与toref

六 computed

七 watch

八 watchEffect

九 标签的ref属性

十 Props

十一 生命周期

十二 Hooks

十三 路由

1 路由组件的区分

2 路由器的工作模式

3 params注意事项:

4 知识总结(gemini生成)

 5 路由交互

6 编程式导航

十四 pinia

1 批量修改

2 actions(修改数据)

3 storeToRefs(pinia解构)

4 getters (计算衍生值)

5 $suvscribe(监听)

6 组合式写法

十五 组件通信

1 props(父->子)*

2 Emit(子->父)*

3 v-model(双向绑定)*

4 pinia(全局状态)

5 slot(插槽)

6 $refs(命令式控制)

7 provide、inject(家族继承)

  8 $attrs(祖孙)


一 学习前提

首先需要将node/npm的环境给配置好

判断对应的版本

接下来就可以在指定的目录下创建一个vue项目了

创建结束后使用本地的编辑器打开

同时执行npm install安装所需的依赖

项目结构的简单入门

1 项目地基 index.html

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

2 启动入口-main.ts

// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App根组件
import App from './App.vue'
// 把创建好的App组件应用塞进index.html的#app元素中
createApp(App).mount('#app')

3 根组件-App.vue

<template>
  <!-- html组件结构 -->
  <div id="chaoge">超哥你好,这是一个vue页面</div>
</template>

<script lang="ts">
// js/ts
export default {
  name: 'App', //组件名称
}
</script>

<style>
/* css组件样式 */
#chaoge {
  color: blue;
  background-color: #ddd;
  box-shadow: 0 0 30px;
  border-radius: 10px;
  padding: 20px;
}
</style>

二 选项式与组合式

1 Vue2

选项式Api-Option Api

Vue2是规定好了格式,在data当中去写数据,在methods里面写方法,在computed里写计算属性。缺点,当开发了复杂的项目时,代码的实现逻辑较为分散,不利于维护。

这就好比你写 Java 类,强制规定:“所有的变量必须写在前 10 行,所有的逻辑必须写在后 10 行”。

2 Vue3

组合式Api-Option

vue3可以将一个功能的所有变量方法监听器写在一起,代码的复用性更强。

在 Vue 3 中,我们不再受 data/methods 的限制。我们可以像写 Java 的 Service 一样,把“搜索”相关的所有代码写在一起,把"排序”相关的所有代码写在一起。

代码示例:

vue2

<script>
export default {
  // 【痛点】:数据混在一起
  data() {
    return {
      users: [],          // 公共数据
      searchQuery: '',    // 🔍 搜索功能的数据
      sortType: 'asc'     // 🔃 排序功能的数据
    }
  },
  
  // 【痛点】:计算逻辑混在一起
  computed: {
    // 🔍 搜索功能的计算
    filteredUsers() {
      return this.users.filter(u => u.name.includes(this.searchQuery))
    },
    // 🔃 排序功能的计算 (依赖上面的结果)
    displayUsers() {
      return this.filteredUsers.sort((a,b) => /* 排序逻辑 */)
    }
  },

  // 【痛点】:方法混在一起
  methods: {
    // 🔍 搜索功能的方法
    updateSearch(val) {
      this.searchQuery = val
    },
    // 🔃 排序功能的方法
    toggleSort() {
      this.sortType = this.sortType === 'asc' ? 'desc' : 'asc'
    }
  }
}
</script>

vue3

<script setup>
import { ref, computed } from 'vue'

// 模拟原始数据
const users = ref([{ name: 'Jack', age: 20 }, { name: 'Tom', age: 18 }])

// =========================================
// 🟢 功能模块 A:搜索功能 (Search Logic)
// =========================================
// 变量、计算、方法都在这里,改搜索逻辑只看这就行
const searchQuery = ref('')

const filteredUsers = computed(() => {
  return users.value.filter(u => u.name.includes(searchQuery.value))
})

const updateSearch = (val) => {
  searchQuery.value = val
}

// =========================================
// 🔵 功能模块 B:排序功能 (Sort Logic)
// =========================================
// 变量、计算、方法都在这里,跟搜索逻辑物理隔离
const sortType = ref('asc')

const displayUsers = computed(() => {
  // 基于搜索结果进行排序
  return [...filteredUsers.value].sort((a, b) => {
    return sortType.value === 'asc' ? a.age - b.age : b.age - a.age
  })
})

const toggleSort = () => {
  sortType.value = sortType.value === 'asc' ? 'desc' : 'asc'
}
</script>

三 setup

在 Vue 3 中,选项式 API 和 setup 是可以共存的,并且选项式 API 中的变量和方法可以访问 setup 中的数据,但反过来,setup 中的数据和方法不能直接访问选项式 API 中的数据。这是因为 setup 会在 Vue 组件的实例化阶段之前执行,而选项式 API 的数据(如 datamethods)会在实例化后才被初始化,所以 setup 无法读取这些数据。

1.基础概念

setup是Vue的一个新的配置项,值是一个函数。

在 Vue 3 中,setup 是 Composition API 的一部分,并不是必须的。你可以选择是否使用它。但是想完全利用Vue的特性功能setup是必须声明的,

<script>
import { ref } from 'vue';

export default {
  setup() {
    const message = ref('Hello Vue 3!');
    const changeMessage = () => {
      message.value = 'You clicked the button!';
    };
    return {
      message,
      changeMessage
    };
  }
};
</script>

但是呢也可以使用对应的语法糖,可以在script标签上进行声明

<script setup>
import { ref } from 'vue';

const message = ref('Hello Vue 3!');
const changeMessage = () => {
  message.value = 'You clicked the button!';
};
</script>

四 ref与reactive

当前主流做法是二者并用,但以 ref 为主、reactive 为辅(推荐使用ref)

1 ref基础概念

ref 是 Vue 3 中 Composition API 的一部分,用来 创建响应式的数据。它是 Vue 3 中实现响应式状态管理的核心工具之一,特别适用于简单的原始数据(如数字、字符串、布尔值等)。

  • 基于 Object.defineProperty / Proxy 封装的值引用容器

  • 对任意类型(基本类型 / 对象 / 数组 / 函数)均可

  • 通过 .value 访问真实值

  • 在模板中会 自动解包(unref)

代码展示:

<script setup lang="ts">
// 导入Vue响应式API
import { ref } from 'vue'
// TS类型约束:补充电话字段
type PersonInfo = {
  name: string
  age: number
  tel: string
}
// 响应式数据:包含姓名、年龄和电话
const person = ref<PersonInfo>({
  name: '超哥',
  age: 18,
  tel: '13800138000', // 初始化电话
})
</script>

2 reactive

代码展示:

<script setup lang="ts">
import { reactive } from 'vue'

// TS 类型约束
type PersonInfo = {
  name: string
  age: number
  tel: string
}

// 使用 reactive 声明对象响应式
const person = reactive<PersonInfo>({
  name: '超哥',
  age: 18,
  tel: '13800138000'
})
</script>

对象不能直接修改(要借助object.assign)

五 torefs与toref

1 toRefs 的作用是:把一个 reactive 对象的每个属性,转换成“仍然保持响应式的 ref”,从而可以安全地解构使用。

toRefsreactive 的“安全解构工具”,用于在解构时不丢失响应式。

  • 只能作用于 reactive

  • 不能用于普通对象

  • 不能直接用于 ref

<script setup lang="ts" name="PersonScript">
import { reactive, ref, toRefs } from 'vue'

// TS 类型约束
type PersonInfo = {
  name: string
  age: number
  tel: string
}

// 使用 reactive 定义对象状态
const person = reactive<PersonInfo>({
  name: '超哥',
  age: 18,
  tel: '13800138000',
})

// 使用 toRefs 保持解构后的响应式
const { name, age, tel } = toRefs(person)

// 控制电话显示/隐藏
const showTelFlag = ref<boolean>(false)

// 查看电话
const showTel = () => {
  showTelFlag.value = true
}

// 修改姓名
const changeName = () => {
  name.value = `超哥-${Math.floor(Math.random() * 100)}号`
}

// 修改年龄
const changeAge = () => {
  age.value += 1
}
</script>

toref则是一个一个的去取

import { reactive, toRef } from 'vue'

const state = reactive({
  name: 'Tom',
  age: 18,
})

const nameRef = toRef(state, 'name')

六 computed

computed = “派生状态 + 自动依赖追踪 + 缓存”,适合从已有响应式状态推导新值”。

代码演示:

const count = ref(1)
const doubleCount = computed(() => count.value * 2)
  • 计算公式: count.value * 2

  • 依赖: count.value

  • 缓存规则:

    • 只要 count.value 没变化,多次访问 doubleCount.value 都不会重新执行计算函数

    • count.value 改变时,computed 才会重新计算

1 派生状态

  • 不是直接修改的状态,而是依赖已有状态的衍生值
  • 例子:doubleCount 来源于 count,但 doubleCount 本身不会改变 count

2 自动依赖追踪

  • Vue 会跟踪你依赖的响应式变量(这里是 count.value
  • 当它变化时,computed 自动更新

3 缓存

  • 如果依赖没变化,多次访问 .value 不会重复执行计算函数
  • 这是 computed 与普通函数的最大区别

核心特点:存在缓存,只会再修改时才会重新执行。

与这段代码做区分

function getDoubleCount() {
  return count.value * 2
}

代码展示:

我这里调用meth02,多次,但是在内部的属性(person.value.ago)不改变,我的这个meth01只会被计算一次,即使meth02被调用多次,但是meth01只会被调用1次。

// 计算属性
const meth01 = computed(() => {
  console.log('计算属性被触发了')
  return person.value.age * 3
})

// 普通方法
const meth02 = () => {
  console.log('普通方法被触发了')
  return meth01.value
}

计算属性默认是只读的,但是Vue允许通过一个Setter函数来创建一个可写的计算属性。

<script setup lang="ts" name="PersonScript">
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // 1. Get: 读取时触发
  // 负责将碎片数据组合成一个整体
  get() {
    return firstName.value + ' ' + lastName.value
  },

  // 2. Set: 修改时触发
  // val 是你赋给 fullName 的新值
  // 负责将整体数据拆解回碎片
  set(newValue) {
    // 假设新值是 "Jane Smith"
    const names = newValue.split(' ')
    firstName.value = names[0] // 更新源数据 firstName
    lastName.value = names[names.length - 1] // 更新源数据 lastName
  }
})

// 读取 (调用 get)
console.log(fullName.value) // "John Doe"

// 写入 (调用 set)
fullName.value = 'Jane Smith' 

// 验证源数据是否更新
console.log(firstName.value) // "Jane"
console.log(lastName.value) // "Smith"
</script>

七 watch

1 作用:监视数据的变化

2 监视种类:

  • 1 ref定义的数据/函数返回一个值。
  • 2 reactive定义的数据。
  • 3 一个包含上述内容的数组。

1 监视具体的属性

在实际开发中,直接监视具体属性借助函数 () => person.value.age 是性能最好且最简单做法。

// 如果你想拿到准确的 oldVal,可以精确监听某一个属性
watch(
  () => person.value.age, // 第一个参数写成 getter 函数
  (newAge, oldAge) => {
    console.log(`年龄从 ${oldAge} 变成了 ${newAge}`) // 这里能拿到不同的值
  }
)

这段代码当中的newVal与oldVal最终读取的值是相同的(要使用上面那种方法这里的watch应该放置在修改变量的位置的前面)

watch(person, (newVal, oldVal) => {
  console.log(newVal === oldVal) // true!它们完全是同一个东西
}, { deep: true })

关闭监听:

在 Vue.js 中,watch 函数返回一个用于停止监听的函数。这个返回的函数就是用来取消对数据的监听的,从而停止执行回调函数。

// watch监视函数(监视谁,变了之后的回调函数)
const stopWatch = watch(
  () => person.value.age,
  (newAge, oldAge) => {
    console.log(`年龄从 ${oldAge} 变成了 ${newAge}`)
    if (newAge >= 40) {
      stopWatch() // 停止监听
    }
  },
)

2 监视对象(ref)

若想监视对象内部属性的变化,需要手动开启深度监视。

// watch监视函数 - 监视对象
watch(
  person,
  (newPerson, oldPerson) => {
    console.log('person对象发生了变化 新值为', newPerson.age, '旧的数值', oldPerson.age)
  },
  { deep: true },
)

解释:当监视对象为对象时,在读取时读取的就是对象对应的地址,如果说只是修改内部的一些属性,在读取时,会读取对象地址,在不改变对象地址的前提下,这个方法并不会被调用(默认是浅监视),但是我是手动开启深度监视,此时修改了对象下的属性值也会被察觉到,但是监视记录的实际值还是对象的地址值,地址值是原先的,调用时这个地址值下的值已经被修改了,进而调用时都是新值。

就相当于你家房子一样,watch监视的是你家地址,虽然你把你家的门口或者里面的装饰换了,但是地址仍然不变,你要是过来找,找的只能说换过的。

3 监视对象(reactive)

在监视这方面的区别,简单理解(相当于只是多了一个自动开启深度监视的功能)

import { reactive, watch } from 'vue'

// 1. 定义 reactive 对象
const person = reactive({
  name: '超哥',
  age: 18,
  info: {
    salary: 20000 // 深层嵌套
  }
})

// 2. 监视整个对象
watch(person, (newVal, oldVal) => {
  console.log('Person 变了!')
  console.log('新旧值是否相等?', newVal === oldVal) // true
})

// 测试触发
const change = () => {
  person.age += 1        //  能触发(第一层)
  person.info.salary += 1000 //  能触发(深层),不需要加 { deep: true }
}

4 监视多个

将原有的一些属性放置到数组当中即可

watch([() => person.value.name, () => person.value.age], ([newName, newAge], [oldName, oldAge]) => {
  console.log([
    [newName, newAge],
    [oldName, oldAge],
  ])
})

八 watchEffect

立即允许一个函数,同时响应式的追踪其依赖,并在依赖更改时重新执行改函数。

watchEffect 是一个立即执行的函数。它的逻辑非常简单粗暴: “我立即执行一次你的代码,在执行过程中,我用到了哪个变量,我就自动盯着哪个变量。”

import { ref, watchEffect } from 'vue'

const water = ref(20) // 水温
const pressure = ref(100) // 水压

// 不需要告诉它监视谁,直接写逻辑
watchEffect(() => {
  // 因为这行代码里读取了 water.value 和 pressure.value
  // Vue 就会自动把 water 和 pressure 加入监视名单
  if (water.value > 60 || pressure.value > 200) {
    console.log('警告:锅炉异常!')
    console.log(`当前状态:水温${water.value},水压${pressure.value}`)
  }
})

九 标签的ref属性

  • ref 用于引用 DOM 元素或组件实例,使得你可以在 JavaScript 中访问它们。可以根据需要获取到一些信息,比如说元素的尺寸大小。

  • 在 Vue 3 中,ref 是响应式的,如果绑定的是 DOM 元素,它会返回该元素的引用;如果绑定的是组件,它会返回组件实例。

  • 动态 ref:你可以在 v-for 或类似的场景中使用动态的 ref 名称,Vue 会将这些引用存储为一个数组或对象。

<template>
  <div class="ax-container">
    <h2>这是一个新的组件</h2>
    <h3 ref="title2">北京天安门</h3>
    <button @click="showLog">点击输出元素</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 创建 ref 引用,直接与模板中的 ref 绑定
const title2 = ref()

// 输出该元素的 DOM 信息
const showLog = () => {
  console.log(title2.value) // 输出元素的 DOM 引用
}
</script>

十 Props

props 是组件对外暴露的输入接口,用于父组件向子组件传递数据

在 Vue 的组件模型中:

  • 父组件:数据的拥有者

  • 子组件:数据的使用者

这就是 Vue 的单向数据流(One-Way Data Flow)

理解:单向数据流 = 数据只能由“拥有者”修改,其他组件只能通过约定方式请求修改。

这是在父组件(传递的一个字符串属性的数值)

<template>
  <Person />
  <Ax a="超哥不错" />
  <Exercise />
</template>

对应的子组件

<template>
  <div class="ax-container">
    <h2>这是一个新的组件</h2>
    <h3 ref="title2">北京天安门</h3>
    <button @click="showLog">点击输出元素</button>
    <h4>{{ x.a }}</h4>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

// 接收并保存
const x = defineProps<{ a: string }>()
console.log(x)

// 创建 ref 引用,直接与模板中的 ref 绑定
const title2 = ref()

// 输出该元素的 DOM 信息
const showLog = () => {
  console.log(title2.value) // 输出元素的 DOM 引用
}
</script>

十一 生命周期

Vue 3 的生命周期可以理解为组件从创建挂载(渲染到页面)、更新销毁的整个过程。

在 Composition API (<script setup>) 中,我们主要关注以下几个阶段。注意:setup 语法糖本身就相当于创建阶段

阶段钩子函数 (Hook)作用与时机
1. 创建setup()最早执行。替代了 Vue2 的 created。此时数据已准备好,但 DOM 还没渲染。
2. 挂载onBeforeMountHTML 模板编译完成,即将挂载到页面。
onMounted最常用。DOM 挂载完成。操作 DOM、发请求都在这里。
3. 更新onBeforeUpdate数据变了,但视图还没更新。
onUpdated数据和视图都更新完成了。
4. 销毁onBeforeUnmount组件即将卸载,此时组件依然可用。
onUnmounted收尾工作。组件已卸载。用于清除定时器、解绑事件监听等。

1. 加载渲染过程 (Mounting)

口诀:父先开始,子先结束。

  1. setup (创建)

  2. onBeforeMount (准备挂载)

  3. --> setup (创建)

  4. --> onBeforeMount (准备挂载)

  5. --> onMounted (挂载完成!子组件就位)

  6. onMounted (挂载完成!父组件就位)

为什么? 因为父组件的模板里包含了子组件,父组件必须等子组件渲染完了,父组件才算真正的“渲染完成”。

2. 更新过程 (Updating)

口诀:父先发现,子先更新。

  1. onBeforeUpdate (父组件检测到变化)

  2. --> onBeforeUpdate (传递 props 给子组件,子组件准备更新)

  3. --> onUpdated (子组件更新完毕)

  4. onUpdated (父组件更新完毕)

3. 销毁过程 (Unmounting)

口诀:父先动手,子先消失。

  1. onBeforeUnmount (父组件准备销毁)

  2. --> onBeforeUnmount (子组件准备销毁)

  3. --> onUnmounted (子组件彻底没了)

  4. onUnmounted (父组件彻底没了)

总结

  • 单个组件setup -> onMounted -> onUpdated -> onUnmounted

  • 父子关系:父组件像一个括弧把子组件包在中间。父组件总是第一个开始,但是最后一个结束

十二 Hooks

hooks是将对应的功能代码(方法逻辑包含的变量方法)将其单独定义在了一个ts文件当中,如果说需要使用这个功能直接对应的导入即可,就不需要全部都放在组件的内部去声明调用,避免出现三个或者多个组件都需要使用某个方法但是出现了重复声明调用的问题.

当你发现两个组件写了类似的代码时,就是你创建 Hooks 的最佳时机。

代码展示:

1 组件

<template>
  <div class="card">
    <h2>这是一个Hooks 测试组件</h2>
    <p>当前数值:{{ sum }}</p>
    <p>放大十倍:{{ bigSum }}</p>
    <button @click="add">点我 +1</button>
  </div>
</template>

<script setup lang="ts">
// 1. 导入刚才定义的 Hook
import useSum from '@/hooks/useSum'

// 2. 调用 Hook,并解构出我们需要的数据和方法
// 就像买了个工具包,把里面的 锤子(sum)、扳手(add) 拿出来用
const { sum, add, bigSum } = useSum()

</script>

2 ts代码:

// 1. 引入 Vue 需要的 API
import { ref, computed, onMounted } from 'vue'

// 2. 导出组合式函数 (命名规范:useXxx)
export default function useSum() {
  // --- A. 定义数据 (State) ---
  const sum = ref(0)

  // --- B. 定义方法 (Actions) ---
  const add = () => {
    sum.value += 1
  }

  // --- C. 定义计算属性 (Computed) ---
  // Hooks 里面也可以包含计算属性!
  const bigSum = computed(() => {
    return sum.value * 10
  })

  // --- D. 定义生命周期 (Lifecycle) ---
  // 只要组件用了这个 Hook,组件挂载时就会自动触发这里
  onMounted(() => {
    console.log('useSum Hook 里的 onMounted 被触发了!')
  })

  // --- E. 把组件需要的东西交出去 (Return) ---
  return {
    sum,
    add,
    bigSum,
  }
}

十三 路由

1 路由组件的区分

1 是亲手写对应的标签展示出来的为一般组件

2 是路由的规则渲染出来的是路由组件

2 路由器的工作模式

维度HashHistoryMemory
URL 形式/#/xxx/xxx
刷新 404不会需要后端支持不存在
SEO
后端配置不需要必须不需要
使用环境浏览器浏览器非浏览器

1 Hash模式

http://example.com/#/user/profile

这种模式将路径当中的#后面的内容仅在前端进行解析,页面不会像服务器发起新的请求,刷新页面也不会出现404,无需后端进行配置但是url路径较为不美观

2 History模式

http://example.com/user/profile

没有#,更加接近传统网站,后端需进行路由配置。后端必须做“所有前端路由 → index.html”的重写/

3 Memory模式

通常用于非浏览器模式,路由状态保存在内存当中不依赖url

最常见的是:History 模式,其次是 Hash 模式,Memory 模式非常少见。

使用频率大致如下:

  1. History 模式 —— 生产环境主流

  2. Hash 模式 —— 开发 / 受限环境常用

  3. Memory 模式 —— 特殊场景专用

3 params注意事项:

<!-- 错误:用path+params的对象写法,params会被忽略 -->
<router-link :to="{ path: '/detail', params: { id: 1001 } }">点我跳转</router-link>
<!-- 正确:name+params的对象写法,参数能正常传递 -->
<router-link :to="{ name: 'Detail', params: { id: 1001 } }">点我跳转</router-link>
// 路由配置(用:id占位)
{ 
  path: '/detail/:id', // :id就是“占位”
  name: 'Detail', 
  component: Detail 
}
// 路由配置(没占位)
{ 
  path: '/detail', // 没有:id
  name: 'Detail', 
  component: Detail 
}

场景 1:只写 RouterLink 传 params(无路由 props 配置)

// 路由配置(未开启 props: true)
{ path: '/detail/:id', name: 'detail', component: Detail }
<!-- Detail 组件(只能通过路由对象拿 id) -->
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id) // ✅ 能拿到
console.log(props.id) // ❌ 报错,props 里没有 id
</script>

场景 2:开启路由 props 配置(params 映射成 props)

// 路由配置(开启 props: true)
{ path: '/detail/:id', name: 'detail', component: Detail, props: true }
<!-- Detail 组件(能通过 props 拿 id) -->
<script setup>
const props = defineProps({ id: String })
console.log(props.id) // ✅ 能拿到(路由系统帮你把 params 转成了 props)
</script>

场景 3:RouterView 直接传 props(和路由 params 无关)

<!-- 父组件 App.vue -->
<RouterView :id="1001" />
<!-- Detail 组件(直接通过 props 拿 id) -->
<script setup>
const props = defineProps({ id: String })
console.log(props.id) // ✅ 能拿到(父组件直接传的,不走路由系统)
</script>

4 知识总结(gemini生成)

这是一个非常经典的 Vue 3 “路由参数 + 组件通信” 混合模式。为了帮你快速理解,我将这段代码的核心逻辑梳理为一个清晰的架构,并提供思维导图。

核心架构:路由与组件的双重“握手”

这段代码的精髓在于:子组件的数据来源是“混合”的

  1. 数据的“身份ID”:由 路由 (URL) 决定(通过 params 传递 id)。

  2. 数据的“内容库”:由 父组件 决定(通过 RouterView 透传 newsList)。

一、 三步走架构 (快递投递模型)

基于你提供的“快递投递”心法,我们将代码逻辑拆解为三个标准步骤:

第一步:贴单子 (路由配置) -> 定义规则

这是规则制定层。我们需要告诉 Vue Router,当 URL 变化时,如何解析参数并把参数“喂”给组件。

  • 核心配置path: 'detail/:id'

    • :id 是占位符,表示这里会有一个动态的 ID。

  • 关键开关props: true

    • 作用:开启后,路由会把解析出来的 id 自动变成组件的 props 属性。

    • 好处:解耦。组件不需要写 useRoute(),直接用 props.id 接收。

第二步:发快递 (父组件) -> 触发动作

这是触发层。父组件做了两件事:导航和供货。

  1. 导航 (指路):使用 RouterLink

    • 必须用 Name:to="{ name: 'detail', params: { id: news.id } }"

    • 注意:如果用 pathparams 会被忽略,导致参数传不过去。

  2. 供货 (给数据)

    • 代码:<RouterView :news-list="newsList" />

    • 作用:父组件把整个 newsList 数组传给了当前渲染的子组件。这样子组件不仅有了 ID,还有了去哪里查 ID 的“数据库”。

第三步:收快递 (子组件) -> 处理逻辑

这是执行层。子组件是一个纯粹的数据消费者。

  1. 签收 (defineProps)

    • id: 来自路由(因为配置了 props: true)。

    • newsList: 来自父组件的 RouterView 透传。

  2. 拆包 (computed)

    • 利用 idnewsList 中查找具体的那一条数据:props.newsList.find(...)

    • 细节:路由传来的 id 可能是字符串,数据里的 id 是数字,需要 Number() 转换。

二、 避坑指南 (关键规则)

根据你提供的资料,这里有几个必须死记硬背的规则:

  1. Params 传参铁律

    • 错误path + params (参数会丢失)。

    • 正确name + params (参数正常传递)。

    • 理解path 是绝对路径,写了什么就是什么;name 是逻辑名称,Vue 才能根据名字去匹配参数。

  2. Props 的三种来源

    • 场景 1 (只路由):没开 props: true,组件只能用 useRoute().params.id 拿参数。

    • 场景 2 (路由转 Props):开了 props: true,组件可以用 defineProps(['id']) 拿参数(推荐,更清晰)。

    • 场景 3 (父组件直传)<RouterView :id="1001" />,这属于普通的父子组件传值,不走路由逻辑。

 5 路由交互

基础概念:根据浏览器地址栏 URL 的变化,自动在页面上挂载(显示)对应的组件,而不需要刷新整个页面。

  • 1 router包下创建index.ts指定路由规则
  • 2 使用RouterView(子路由页面渲染),Routerlink(页面转变交互)
  • 3 main.ts引用并使用user(router)

项目结构:

1 实现路由:

首先在src的根路径下创建一个router的文件夹,再创建一个index.ts,在里面声明对应的路由规则

// router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'

import Home from '@/views/Home.vue'
import News from '@/views/News.vue'
import About from '@/views/About.vue'
import Header from '@/components/Header.vue'
import Detail from '@/views/Detail.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    redirect: '/home',//路由重定向
  },
  {
    path: '/chushihua',
    name: 'Root',
    component: Header,
  },
  {
    path: '/home',
    name: 'zhuye',
    component: Home,
  },
  {
    path: '/news',
    name: 'xinwen',
    component: News,
    children: [
      {
        path: 'detail/:id',
        name: 'detail',
        component: Detail,
        props: true, // 关键
      },
    ],
  },
  {
    path: '/about',
    name: 'guanyu',
    component: About,
  },
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

export default router

2 src的views下的三个组件-Home,About,News

<template>
  <div>
    <h1>欢迎大家来到Home主页,三秒之后跳转新闻页面</h1>
  </div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 实现三秒后跳转
onMounted(() => {
  setTimeout(() => {
    router.push('/news')
  }, 3000)
})
</script>
<style scoped></style>
<template>
  <div>News 组件内容</div>
</template>
<script lang="ts" setup></script>
<style scoped></style>

components

<template>
  <div class="welcome-container">
    <div>你好,欢迎你学习 Vue 3 + TypeScript + Vite 全家桶开发的单页面应用!</div>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped></style>

3 接下来是App.vue

(需要导入RouterView与声明RouterView,Routerlink的标签)

<template>
  <div class="app">
    <h1>Welcome to My Vue App</h1>
    <!-- 导航区 -->
    <div class="navigate">
      <!-- 字符串写法 -->
      <RouterLink to="/home">首页</RouterLink>
      <!-- 对象写法(1 名字跳转 2 路径跳转) -->
      <RouterLink :to="{ name: 'xinwen' }">新闻</RouterLink>
      <RouterLink :to="{ path: '/about' }">关于</RouterLink>
    </div>
    <!-- 展示区 -->
    <div class="main-content">
      <!-- 初始路径(/)显示Header,其他路径隐藏 -->
      <Header v-if="route.path === '/'" />
      <!-- 路由页面:跳转后渲染对应组件 -->
      <RouterView v-else />
    </div>
  </div>
</template>
<script lang="ts" setup>
import { RouterView, RouterLink, useRoute } from 'vue-router'
import Header from './components/Header.vue'
const route = useRoute()
</script>
<template>
  <div>
    <h1>欢迎大家来到About页面</h1>
  </div>
</template>
<script lang="ts" setup></script>
<style scoped></style>
<template>
  <div class="news-container">
    <h2>新闻列表</h2>

    <div class="news-layout">
      <div class="news-list-wrap">
        <ul class="news-list">
          <li v-for="news in newsList" :key="news.id">
            <RouterLink :to="{ name: 'detail', params: { id: news.id } }">
              {{ news.title }}
            </RouterLink>
          </li>
        </ul>
      </div>

      <div class="news-detail-wrap">
        <RouterView :news-list="newsList" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { RouterView, RouterLink } from 'vue-router'

// 定义接口类型
export interface NewsInter {
  id: number
  title: string
  content: string
}

const newsList = ref<NewsInter[]>([
  { id: 1, title: '注意,未来几天寒潮来袭', content: '多穿衣服' },
  { id: 2, title: '冬天不能这样,否则吃胖十斤', content: '多跑步,少吃饭' },
  { id: 3, title: '震惊,万万没想到', content: '明天要上课' },
  { id: 4, title: '这是一个悲惨的故事', content: '今天是周一' },
])
</script>
<template>
  <div v-if="detail">
    <h3>{{ detail.title }}</h3>
    <h3>{{ detail.content }}</h3>
  </div>

  <div v-else class="empty">未找到对应新闻</div>
</template>

<script setup lang="ts">
import { computed } from 'vue'
// 导入 News.vue 中定义的接口(如果是单独文件定义更好,这里假设只是类型复用)
import type { NewsInter } from './News.vue'

/**
 * 接收 Props
 * 1. id: 来自路由参数 (因为 router 中配置了 props: true)
 * 2. newsList: 来自父组件 <RouterView :news-list="newsList" /> 的透传
 */
const props = defineProps<{
  id: string
  newsList: NewsInter[] // 接收父组件传来的整个数组
}>()

const detail = computed(() => {
  // 注意:路由传过来的 id 通常是 string,数据里的 id 是 number,需要转换对比
  return props.newsList.find((item) => item.id === Number(props.id))
})
</script>

4 最后是main.ts,使用路由器

// 引入createApp用于创建应用
import { createApp } from 'vue'
// 引入App根组件
import App from './App.vue'
// 引入路由器
import router from './router'
// 使用路由器并挂载路由器
createApp(App).use(router).mount('#app')

 总结:Vue 3 传参整体思路 (初学者心法)

你可以把从列表页点击进入详情页这整个过程,想象成一次“快递投递”:

第一步:贴单子 (Router 配置)

在路由配置文件里,设计好快递地址格式。

  • 关键点:用 :xxx 占位。

  • 代码path: '/detail/:id', props: true

第二步:发快递 (列表页跳转)

用户点击列表项,触发跳转动作。

  • 关键点:把具体的 ID 填进去,生成最终地址。

  • 代码

    <RouterLink :to="{ name: 'detail', params: { id: 999 } }">
      看详情
    </RouterLink>
    

第三步:收快递 (详情页组件)

组件并不关心数据是从哪来的(是路由传的,还是父组件传的),它只管拿个篮子在门口接。

  • 关键点:声明 defineProps,名字要和路由里的 :id 对上。

  • 代码

    // Detail.vue
    const props = defineProps(['id'])
    // 直接用,不需要 import useRoute,也不需要 route.params.id
    console.log(props.id) 
    

 为什么推荐这种“路由传参 + Props接收”的写法?

  1. 解耦:你的详情页组件 Detail.vue 变得很纯粹。它不依赖 vue-router 库。以后如果你想在一个弹窗里复用这个详情页,直接 <Detail id="999" /> 就能用,非常灵活。

  2. 清晰:在 defineProps 里一眼就能看到这个组件需要什么参数,像说明书一样。

6 编程式导航

如果说 <RouterLink> 是给用户点的菜单,那么编程式导航就是你手中的遥控器,让你能在代码里控制页面的跳转。

在Vue当中我们有两种方式切换页面:声明式与编程式

方法router.push()router.replace()
动作推入 (Push)替换 (Replace)
行为像叠盘子一样,在历史记录里新增一条记录。像抽桌布一样,销毁当前记录,用新的代替。
后果点击浏览器“后退”按钮,能回到上一个页面。点击浏览器“后退”按钮,回不到刚才的页面了(会回到更早之前)。
适用普通跳转(去列表、去详情)。登录成功后(不希望用户点后退又回到登录页)、404 页面。

声明式 (<RouterLink>) 优先:如果只是一个普通的超链接,没有任何逻辑判断,优先用标签写,代码更具可读性。

编程式 (useRouter) 兜底:一旦涉及到表单提交、权限判断、异步请求,必须使用编程式导航。

代码展示:

<template>
  <div>
    <h1>Home Page</h1>
    <button @click="goToCat">Go to Cat Page</button>
  </div>
</template>

<script setup lang="ts">
import { useRouter } from 'vue-router'

// 获取 router 实例
const router = useRouter()

function goToCat() {
  // 编程式导航
  router.push('/cat') // 使用路径跳转
  // 或者使用命名路由
  // router.push({ name: 'cat' })
}
</script>

十四 pinia

基础概念:它就是一个全局的组件,可以在任何地方被别人使用。

Pinia 概念对应 Vue 组件写法作用
Stateref()reactive()存数据(相当于 data)
Getterscomputed()算数据(相当于 computed)
Actionsfunction()改数据(相当于 methods)
  • Pinia 就是全局状态管理,用来解决“组件A的数据要给组件B用”这种麻烦事。

  • Setup 写法ref 是 state,computed 是 getter,function 是 action。

与Hooks进行区分

  • hooks:目的服务于组件的某个功能,可以被各个组件进行按需调用
  • pinia:目的维护数据的准确性与一致性,相当于一个存储共有变量的容器。
特性Hooks (Composables)Pinia (Store)
主要目的复用逻辑 (比如:抓取鼠标坐标、倒计时功能)管理全局数据 (比如:用户信息、购物车、全局主题)
数据独立性独立。每次调用 useHook() 都会产生一份新的数据。共享。无论在几个组件调用 useStore(),拿到的都是同一份数据。
调试工具使用常规 Vue Devtools 查看组件内部状态。专属 Devtools,支持时光旅行(Time Travel)、Action 追踪。
持久化刷新页面数据丢失(除非手动存 localStorage)。配合插件(pinia-plugin-persistedstate)可一键自动持久化。
SSR 支持需要手动处理。完美支持服务端渲染 (SSR)。
// Pinia (Store) 就像这种感觉
public class UserStore {
    // static:内存里只有一份,所有人共享
    public static String userName = "Jack"; 
}

环境前提:

1 先安装pinia

本地命令行执行npm i pinia

2 在main.ts当中

// Vue3 核心入口:仅创建并挂载Vue实例
import { createApp } from 'vue' // 仅导入创建Vue实例的核心方法
import App from './App.vue' // 导入根组件
// 1 引入pinia
import { createPinia } from 'pinia'
// 创建Vue实例 + 挂载到index.html的#app容器
const app = createApp(App)
// 2 创建pinia
const pinia = createPinia()
// 3 创建pinia
app.use(pinia).mount('#app')

3 定义一个单独的store包,在下面创建

import { defineStore } from 'pinia'

export const useCountStore = defineStore('Count', {
  // 真正存储数据的地方
  state() {
    return {
      sum: 6,
    }
  },
})

4 对应的组件当中的使用

// 导入
import { useCountStore } from '@/store/Count'
const countStore = useCountStore()
countStore.sum

技巧

1 批量修改

2 actions(修改数据)

在本地调用pinia当中定义的方法,进行修改,相当于一个封装了

3 storeToRefs(pinia解构)

将 Pinia store 中的响应式数据(state/getters)转换为独立的 ref 对象,确保解构后仍保留响应式特性。

import { defineStore } from 'pinia'
export const useCountStore = defineStore('Count', {
  // 真正存储数据的地方
  state() {
    return {
      sum: 6,
    }
  },
})
import { useCountStore } from '@/store/Count'
import { storeToRefs } from 'pinia'
const countStore = useCountStore()
const { sum } = storeToRefs(countStore)

4 getters (计算衍生值)

Pinia 的 getters 就是 全局 store 中的计算属性,用于根据 state 派生出新的数据,并且可复用、响应式和缓存优化。(类似于computed)

import { defineStore } from 'pinia'
export const useCountStore = defineStore('Count', {
  // 真正存储数据的地方
  state() {
    return {
      sum: 6,
    }
  },
  getters: {
    bigSum(state) {
      return state.sum * 10
    },
    smallSum(state) {
      return state.sum * 2
    },
  },
})

5 $suvscribe(监听)

$subscribe 是 Pinia 专门提供的一个实例方法,用来监听 Store 中 State 的变化

它是专门为 Pinia 定制的,比普通的 watch 更强大、更底层,也提供了更多的上下文信息。

基本语法结构

// 假设你有一个 store 实例
const store = useLoveStore()

// 调用 $subscribe
store.$subscribe((mutation, state) => {
  // 当 state 变化时,这里会被执行
}, { /* 可选配置对象 */ })

一个代码实现:数据变了就同步到浏览器存储

import { defineStore } from 'pinia'
import axios from 'axios'
import { nanoid } from 'nanoid'

export const useLoveStore = defineStore('Love', {
  // 真正存储数据的地方
  state: () => {
    // 1. 先尝试读取本地存储
    // JSON.parse 需要处理 null 的情况,所以用 || [] 做保底,但这里我们希望有默认土味情话
    const localData = localStorage.getItem('talkList')

    return {
      // 如果本地有数据,就转为对象使用;如果没有,就用后面的默认数组
      talkList: localData
        ? JSON.parse(localData)
        : [
            { id: '1', title: '导航说我已到达目的地,目的地是哪里?你的心里。' },
            { id: '2', title: '我觉得你今天有点怪,哪里怪?怪好看的。' },
            { id: '3', title: '我觉得你特别像一款游戏,什么游戏?我的世界。' },
            { id: '4', title: '你猜我什么星座?为你量身定做。' },
          ],
    }
  },

  actions: {
    async getLoveTalk() {
      try {
        const result = await axios.get('https://api.shadiao.pro/chp')
        const content = result.data?.data?.text

        if (content) {
          this.talkList.unshift({
            id: nanoid(),
            title: content,
          })
        }
      } catch (error) {
        console.error('API 请求失败:', error)
        alert('接口又挂了,请尝试方案二')
      }
    },
  },
})
<template>
  <div class="talk">
    <button @click="getLoveTalk">获取一句土味情话</button>
    <hr />
    <ul>
      <li v-for="talk in talkList" :key="talk.id">{{ talk.title }}</li>
    </ul>
  </div>
</template>
<script setup lang="ts">
import { useLoveStore } from '@/store/Love'
import { storeToRefs } from 'pinia'

const loveStore = useLoveStore()
const { talkList } = storeToRefs(loveStore)

// 订阅 store 的变化,保存到 localStorage
// 参数1:mutate (本次修改的信息,比如是谁改的,改了什么)
// 参数2:state (当前的最新数据)
loveStore.$subscribe((mutate, state) => {
  console.log('数据变化了,正在保存到本地...')
  localStorage.setItem('talkList', JSON.stringify(state.talkList))
})

const getLoveTalk = () => {
  loveStore.getLoveTalk()
}
</script>

6 组合式写法

import { defineStore } from 'pinia'
import axios from 'axios'
import { nanoid } from 'nanoid'
import { ref } from 'vue'

export const useLoveStore = defineStore('Love', () => {
  // ===== 原 state() =====
  const localData = localStorage.getItem('talkList')

  const talkList = ref(
    localData
      ? JSON.parse(localData)
      : [
          { id: '1', title: '导航说我已到达目的地,目的地是哪里?你的心里。' },
          { id: '2', title: '我觉得你今天有点怪,哪里怪?怪好看的。' },
          { id: '3', title: '我觉得你特别像一款游戏,什么游戏?我的世界。' },
          { id: '4', title: '你猜我什么星座?为你量身定做。' },
        ],
  )

  // ===== 原 actions.getLoveTalk =====
  const getLoveTalk = async () => {
    try {
      const result = await axios.get('https://api.shadiao.pro/chp')
      const content = result.data?.data?.text

      if (content) {
        talkList.value.unshift({
          id: nanoid(),
          title: content,
        })
      }
    } catch (error) {
      console.error('API 请求失败:', error)
      alert('接口又挂了,请尝试方案二')
    }
  }

  // ===== 暴露 =====
  return {
    talkList,
    getLoveTalk,
  }
})

十五 组件通信

1 props(父->子)*

基础概念:父传子,是最基础,最常用的通信方式,子组件通过defineProps接收,props是只读的,子组件不应直接修改他。

代码示例:(1 父 2 子)

父组件传递两个参数,一个title,一个count,子组件借助defineProps进行接收

<template>
  <Child title="任务列表" :count="10" />
</template>
// 接收:使用 TS 泛型定义,无需导入
const props = defineProps<{
  title: string
  count?: number // 可选属性
}>()

console.log(props.title) // 使用

2 Emit(子->父)*

基础概念:子传父,子组件通过emit触发一个事件并携带参数,父组件通过@事件名监听该事件并接收参数。

在子组件当中第一个参数定义了合法的事件名,delete-item,相当于是一个频道,父组件需要@delete-item进行调换。

  • 定义处(e: 'delete-item', ...):默认第一个参数写这个步骤的事件名字符串,进行区分

  • 发射处emit('delete-item', 99)

  • 父组件监听处@delete-item="handleDelete" => 当(@)子组件发来 delete-item 信号时,请运行我的 handleDelete 函数。”

  • 这里是相当于父接收了子的一个信息,出现delete-item就是代表我需要删除id=99的,但是逻辑不在子组件当中去进行操作,是让父组件进行操作。

代码示例(1父 2 子)

<template>
  <Child @delete-item="handleDelete" />
</template>

<script setup lang="ts">
const handleDelete = (id: number) => {
  console.log('子组件要删除:', id) // 父组件执行逻辑
}
</script>
// 1. 声明有哪些事件
const emit = defineEmits<{
  (e: 'delete-item', id: number): void
}>()

// 2. 触发事件 (例如在按钮点击时)
const handleClick = () => {
  emit('delete-item', 99) // 汇报:我要删掉 ID 99
}

3 v-model(双向绑定)*

基础概念:父子双向同步,本质上是props+emit的语法糖。

代码示例:(1 父 2 子)

defineModel 的运作流程:

  1. 声明const model = defineModel() 告诉 Vue:“我要建立一个双向通道”。

  2. 读取:你读 model.value,就是读父组件传来的 Props。

  3. 写入:你写 model.value = 200,Vue 自动帮你在背后 emit update 事件,父组件的数据瞬间同步更新。

<template>
  <h1>父组件的钱:{{ money }}</h1>
  <Child v-model="money" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
const money = ref(100)
</script>
<template>
  <div class="card">
    <p>子组件看到的钱:{{ model }}</p>
    <button @click="model += 10">加钱 (修改父组件)</button>
  </div>
</template>

<script setup lang="ts">
// 核心:一句话搞定接收和修改
// 这里 model 就像一个普通的 ref,但它直通父组件
const model = defineModel<number>() 
</script>

4 pinia(全局状态)

基础概念:全局状态管理,适用于共用变量的使用,相当于对共有数据的一个存储。

代码示例:

import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const name = ref('Alice')
  const login = () => { name.value = 'Bob' }
  
  return { name, login }
})
import { useUserStore } from '@/store/user'

const userStore = useUserStore()

console.log(userStore.name) // 读
userStore.login()           // 写 (调用 action)

5 slot(插槽)

基础概念:父传子,父组件将一段HTML结构传给子组件,子组件用<slot>标签占位显示。

分类

  1. 默认插槽:不用名字。

  2. 具名插槽:使用 name 属性,父组件用 v-slot:name (简写 #name)。

  3. 作用域插槽:子组件将数据回传给父组件,父组件决定如何渲染结构(数据在子,结构在父)。

代码示例:

<template>
  <Child>
    <p>这是一张风景照</p> 

    <template #footer>
      <button>点赞</button>
    </template>
  </Child>
</template>
<template>
  <div class="card">
    <slot></slot> 
    
    <slot name="footer"></slot>
  </div>
</template>

6 $refs(命令式控制)

基础概念:$refs 是 Vue 提供的一种 直接访问 DOM 元素或子组件实例的引用机制

代码示例:

<template>
  <Child ref="childRef" />
  <button @click="callChild">点我</button>
</template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childRef = ref(null)

const callChild = () => {
  childRef.value.sayHi()
}
</script>
<script setup>
const sayHi = () => {
  console.log('Hi from child')
}

defineExpose({ sayHi })
</script>

7 provide、inject(家族继承)

基础概念:祖先与后代,机制:依赖注入。祖先通过provide提供数据,无论层级多深,后代组件都可以通过inject拿到。

import { provide } from 'vue'
// 提供一个只读数据,或者一个修改函数
provide('theme-color', 'dark')
import { inject } from 'vue'
// 接收:第二个参数是默认值(防崩)
const theme = inject('theme-color', 'light')

  8 $attrs(祖孙)

基础概念:祖孙之间的传值,透传属性。

代码示例:

<Child :id="100" class="btn" msg="hello"/>
<template>
  <GrandChild v-bind="$attrs" />
</template>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs) // { id: 100, class: "btn", msg: "hello" }
</script>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐