Vue+JS Day3

JS + Vue 学习笔记(三):从路由切换到 Pinia 共享状态与构建预览

前言

前两天,我补了 JavaScript 基础、Vue 模板与响应式,练习了组件通信、异步请求和本地持久化。第三天的重点,是把页面组织起来:用路由切换内容,用 Pinia 管理共享状态,再做表单校验、自测和构建预览。

今天让我印象最深的,不是多记住了几个 API,而是开始分清三件事:内容属于哪个组件,数据由谁保存,页面运行的是源码还是构建产物。

这篇文章按实际学习过程整理。示例以 Vue 3 的 <script setup lang="ts"> 为背景,不同小节的代码是独立片段,不需要全部重复粘贴。现有项目已经准备了部分路由页和 store,我主要通过阅读、修改和运行验证理解它们,并不是今天从零搭建了所有文件。

一、路由:地址、组件和显示位置要连起来看

项目中已经有以下对应关系:

ts 复制代码
{ path: '/users', component: UserListView },
{ path: '/users/:id', component: UserDetailView },
{ path: '/todos', component: TodoView },

访问 /users 显示用户列表,访问 /users/2 显示用户详情,访问 /todos 显示待办页。

但只有路由配置还不够。入口组件需要提供导航和路由内容的显示位置:

vue 复制代码
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>

<template>
  <header>
    <h1>第三天:路由练习</h1>
    <nav>
      <RouterLink to="/users">用户列表</RouterLink>
      <RouterLink to="/todos">我的待办</RouterLink>
    </nav>
  </header>
  <p>下面是当前地址对应的页面:</p>
  <RouterView />
</template>

我把它理解为三个分工:

text 复制代码
RouterLink → 跳到哪个地址
路由配置   → 地址对应哪个组件
RouterView → 匹配到的组件显示在哪里

如果删掉 RouterView,导航仍可以改变地址,但对应的路由页没有地方显示。

我还加了一句固定文字,再切换列表页和待办页。结果是文字一直保留,下方的内容发生变化。这样就直观看到了:页面外壳和路由内容不是同一层。

二、按钮为什么只在用户详情页出现

练习时,我把"查看 2 号用户"的按钮写在用户详情组件中,却疑惑为什么访问 /users 时看不到它。

原因不是"没有 id,所以按钮不显示",而是:

  • /users 匹配的是用户列表组件。
  • /users/2 匹配的是用户详情组件。
  • 按钮写在详情组件中,自然随详情组件出现。
text 复制代码
App.vue
├── 固定标题、导航、说明文字
└── RouterView
    ├── /users   → 用户列表组件
    ├── /users/2 → 用户详情组件
    └── /todos   → 待办组件

<header> 也只是 HTML 标签,不会因为叫 header 就自动成为全站公共区域。它是否一直显示,取决于放在哪个组件、什么位置。

我的需求最终是让按钮出现在 /users,因此把按钮和跳转函数一起放到了用户列表组件中,而不是放到 App.vue。

这次还留下过一个错误:函数移走了,详情页模板里的旧按钮却没有删除,导致类型检查提示找不到 goToUser。清理残留绑定后,检查才通过。

移动功能时,不能只移动函数,还要检查模板中是否留下了对它的调用。

三、useRoute 读取信息,useRouter 执行导航

这两个名字很像,但职责不同:

ts 复制代码
const route = useRoute()
const router = useRouter()

1. 读取参数

对于当前这条 /users/:id 路由,访问 /users/2 时:

ts 复制代码
route.params.id        // '2'
typeof route.params.id // 'string'

我最初以为它是数字,后来才明确:地址里看起来像数字的这一段,读取到的仍然是字符串。

ts 复制代码
'8' + 1         // '81'
Number('8') + 1 // 9

需要数字运算时,要先转换;只是拼接请求地址时,则可以直接使用字符串。

项目中还用计算属性读取当前参数:

ts 复制代码
const userId = computed(() => String(route.params.id))

这样当前参数变化后,读取 userId 时可以得到对应的新值。

2. 执行跳转

ts 复制代码
function goToUser() {
  router.push('/users/2')
}
vue 复制代码
<button @click="goToUser">查看 2 号用户</button>

我曾把路径写成 /user/2,少了一个 s。项目配置的是 /users/:id,两者不匹配,会进入兜底的找不到页面路由。

记忆方式是:route 负责读,router 负责跳;路径要和配置对应。

四、共享计数:为什么引入 Pinia

如果在两个页面中分别写:

ts 复制代码
const count = ref(0)

它们是两份独立数据。变量名字相同,不代表保存的是同一份状态。

但这并不意味着只有 Pinia 才能共享数据。我当时提出了一个问题:把计数放在 App.vue 中,不也可以吗?

答案是可以,不过要分清需求:

  • 只是所有页面上方都能看到同一个计数器,放在 App.vue 的路由内容区外就可以。
  • 如果页面组件内部也要读取、修改它,可以通过 props 和事件连接父子组件。
  • 当多个组件都需要使用同一份状态时,也可以用 Pinia 把它集中管理。

Pinia 不是共享状态的唯一办法,而是另一种组织共享数据的方式。

1. state、action 和 getter

现有计数 store 的核心结构如下,我把加 1 修改成了加 2:

ts 复制代码
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
  }),
  getters: {
    doubleCount: state => state.count * 2,
  },
  actions: {
    increment() {
      this.count += 2
    },
    reset() {
      this.count = 0
    },
  },
})
  • state 保存共享数据。
  • action 定义操作数据的方法。
  • getter 根据状态计算派生结果,例如双倍计数。

组件中取得 store 后,可以直接使用:

ts 复制代码
const counter = useCounterStore()
vue 复制代码
<p>{{ counter.count }}</p>
<button @click="counter.increment()">+2</button>

这里读取的是 counter.count,不写成 counter.count.value。

2. 我实际验证了什么

我修改了 action 和按钮文字,观察 A、B 两个组件读取同一份计数。接着从只看 A 切换到只看 B,B 显示了更新后的 4,控制台没有报错。

需要说明:/counter/a 和 /counter/b 在当前配置中使用的是同一个路由组件,只是在内部按参数切换不同面板;这不是两个独立路由页的完整实现。

后续"在列表页修改、在待办页显示同一 store"的方案,我确认理解后选择不再重复动手,因此这里只记录设计思路,不把它写成已完成的跨页实操。

另外,切换路由时保留状态,不等于刷新后也能保留。 当前计数 store 没有接入本地持久化;待办刷新后保留,是因为它另外使用了 localStorage。

五、表单校验:computed 算错误,v-if 控制显示

表单使用 reactive 保存用户名和邮箱,使用计算属性生成错误信息:

ts 复制代码
const form = reactive({ username: '', email: '' })
const showValidation = ref(false)

const usernameError = computed(() => {
  return form.username.trim() === '' ? '用户名不能为空' : ''
})

输入为空或全是空格,返回错误文字;合法时返回空字符串。

模板负责决定是否展示:

vue 复制代码
<small v-if="showValidation && usernameError">
  {{ usernameError }}
</small>

提交时才开启 showValidation,所以刚打开页面不会立即出现一堆错误。提交失败后再输入合法用户名,提示会自动消失:

text 复制代码
用户名变化
→ computed 重新计算错误信息
→ 合法时结果为 ''
→ v-if 条件不成立
→ 错误提示消失

这里具体依赖的是 form.username,不是表单的任何字段变化都要重新计算用户名错误。

邮箱则使用一个基础格式规则:

ts 复制代码
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

这只是练习中的基础格式检查,不能证明邮箱真实存在或可以收信。

实际操作中,我验证了空提交提示必填、错误邮箱提示格式问题、输入合法后提示消失,以及合法提交显示成功文字。这里的"提交成功"只是本地页面提示,没有向服务器保存数据。

六、十道自测暴露出的两个卡点

自测覆盖了引用、回调、数组方法、解构、异步,以及响应式、计算属性、组件通信、侦听器和路由参数。

结果是:8 题完全正确,1 题部分正确,1 题错误。 两个需要回看的地方是下面这些。

1. ref 和 reactive 不能套用同一种访问方式

ts 复制代码
const count = ref(0)
const user = reactive({ age: 20 })

count.value++ // 正确
user.age++    // 正确

我在选择题中误选了 user.value.age++。当前这个 user 是响应式对象,应直接访问 age,它没有用于包装整个对象的 value 属性。

复习时,我正确补出了 name.value 和 person.age。

2. immediate 首次执行时没有旧值

ts 复制代码
const count = ref(10)
watch(count, (newValue, oldValue) => {
  console.log(newValue, oldValue)
}, { immediate: true })

立即执行的第一次打印:

text 复制代码
10 undefined

我最初忘记 immediate: true,补题时又把初始旧值理解成了和新值相同。实际上,创建时还没有发生一次数据变化,因此这次没有旧值。

如果之后把 count.value 改成 12,对应的变化回调才会收到 12 和 10。最后我通过了补题,但仍然需要后续练习巩固,不能把一次答对当成已经熟练掌握。

七、dev、build、preview:源码和成品不是一回事

这是今天收尾时又确认的一组区别:

命令 用途
npm run dev 开发时使用,修改源码后通常通过热更新看到变化
npm run build 执行当前项目的类型检查与构建,生成 dist
npm run preview 在本地预览已有的 dist 构建产物

我问过:运行 preview 后再改代码,页面会不会同步?

不会自动把源码更新重新打包进去。 preview 读取的是上一次构建的文件。改了源码后,需要重新执行 npm run build,构建成功再刷新预览页面;如果预览服务已停止,再启动它。

powershell 复制代码
# 平时边写边看
npm run dev

# 验收打包后的版本
npm run build
npm run preview

这就像源文件和导出成品:修改源文件,不代表之前导出的成品会自动变化。

本次构建与预览结果

最后由助手直接执行构建,并使用独立浏览器会话检查:

  • 类型检查和构建通过。
  • 用户列表成功加载接口数据。
  • 导航切换到待办页正常。
  • 添加临时待办后刷新,数据仍然保留。
  • 直接请求 /todos 返回 HTTP 200,浏览器刷新没有出现 404。
  • 刷新后的控制台没有错误和警告。

临时待办已删除,测试浏览器和临时预览服务已关闭,没有修改我原来浏览器中的待办数据。

这只说明本地构建预览通过,不代表网站已上线。 真实部署使用 history 路由时,还要检查托管服务器的 SPA fallback:访问 /todos 等前端路由时,应能返回应用入口,再交给前端路由处理,而不是直接报告文件不存在。

总结

第三天,我开始把"组件写在哪里""状态由谁保存"和"地址如何对应页面"联系起来理解。按钮不显示时,先检查当前路由究竟渲染哪个组件;数据不同步时,先判断两个组件是否使用同一份状态,而不是只看变量名。

三天计划完成的是一轮主题覆盖和主要功能练习,不是三天精通 Vue。部分现有代码通过阅读和修改理解,部分重复实操与总结复述选择跳过,这些都应该和真正运行验证过的内容分开记录。

接下来,我想减少对现成代码的依赖,自己重新做一个包含列表、详情、待办和共享计数的小项目,把这些概念真正写熟。

相关推荐
我是大猴子1 小时前
py how claude work项目分析
运维·服务器·windows·1024程序员节
码完就睡1 小时前
Linux——IP协议与路由选择
linux·网络·tcp/ip·1024程序员节
远航计算机1 小时前
收录从分钟级变 3 到 7 天,抢首发为什么失效了
大数据·人工智能·爬虫·chatgpt·aigc·1024程序员节
iDao技术魔方1 小时前
Bun v1.4.4 实战:用 bun fmt 替掉 prettier 的 5 个步骤
typescript·1024程序员节
箓维1 小时前
UDP Socket的实现
网络·1024程序员节
AIyy8661 小时前
个人在线知识库怎么搭?从三种知识形态说起
1024程序员节
会飞的大可1 小时前
AI 新闻周报 | 2026年10月19日 — 10月24日
1024程序员节
艸肅1 小时前
Docker 国内镜像源配置
linux·ubuntu·docker·1024程序员节