Skip to content

滚动容器与页面交互(二) ​

本页对应 滚动容器与页面交互,共 8 段视频,约 1 小时 5 分。

学习目标 ​

  • 能在获取验证码和提交登录表单时依据接口返回的 code === 10000 判断调用是否成功,失败时提示 message 并阻止后续逻辑执行。
  • 能创建 user Store,使用 ref 定义并暴露 token,在登录成功后写入接口返回的 token,并验证 Pinia 可直接赋值修改状态。
  • 能安装并配置 pinia-plugin-persistedstate,在 Pinia 实例上注册插件,并通过 persist: true 验证刷新页面后 token 仍保留在本地存储中。
  • 能使用 persist.pick 仅持久化 token 字段,并说明按需持久化对性能和存储合理性的作用。
  • 能调用 uni-forms 实例的 validateField(['mobile']) 对手机号进行部分表单校验,并在校验成功后再发送请求、失败时不发送请求。
  • 能实现协议勾选状态切换与登录前校验,并在请求拦截器中从 Pinia 读取 token,存在时添加 Authorization 请求头,通过测试请求验证 token 已随请求发送。

知识点 ​

先跑通主流程,再补结果验证 ​

为什么需要:登录功能里有两个关键接口:获取验证码、提交登录。转写稿里先让这两个接口“跑通”,因为主流程不通时,补细节容易把问题混在一起。
怎么想:先确认“能发请求、能拿到返回”,再回头检查“返回是否成功、失败怎么提示、成功后代码放哪”。
怎么做:先完成获取验证码与登录提交的基本调用,再在调用处补 code 判断。
易错点:接口通了就以为功能完成,忽略失败分支;或者一边调接口一边改交互,出了问题不知道是接口错还是逻辑错。

倒计时必须等验证码发送成功后再启动 ​

为什么需要:用户点“获取验证码”后,只有短信真的发送成功,才应该出现倒计时。否则接口失败也会倒计时,用户会以为已经发送。
怎么想:转写稿中发现原代码“一点按钮倒计时立马就变成 true”,于是提出:应该等 await 执行完,再决定是否显示倒计时。
怎么做:把启动倒计时的代码下移到获取验证码接口调用之后,并先判断结果:

js
// 示意代码:按转写稿逻辑整理
const res = await 获取验证码接口(手机号)
if (res.code !== 10000) {
  showToast(res.message)
  return
}
// 只有成功后才开始倒计时
startCountdown()

易错点:倒计时写在请求之前;忘记 await,请求还没回来就判断;失败后忘记 return,导致倒计时或后续逻辑继续执行。

用 code 判断接口是否调用成功 ​

为什么需要:调用接口不等于一定成功,可能发送验证码失败、登录失败。后端约定 code === 10000 表示成功,不等于 10000 表示失败。
怎么想:先判断 code,成功才走后续;失败时后端会返回 message,用它提示用户。
怎么做:在获取验证码和登录提交两处都加判断:

js
if (res.code !== 10000) {
  showToast(res.message)
  return
}

易错点:把 10000 写成 1000;只判断获取验证码,不判断登录提交;失败提示不用后端 message,自己乱写文案。

失败后要中断,不要继续执行 ​

为什么需要:验证码失败还启动倒计时、登录失败还保存 token,都会造成状态错误。
怎么想:失败分支提示完就结束当前流程,成功分支才继续。
怎么做:在失败判断里加 return。转写稿中反复强调“失败了,后面就没有必要执行了”。
易错点:只写 showToast 不写 return;return 位置放错,导致成功逻辑也被跳过。

调试过程:从现象反推代码位置 ​

转写稿里的调试很典型:先点验证码,发现倒计时立即出现,于是判断“倒计时位置不对”;把倒计时下移到接口调用之后,再补 code 判断。随后用手机号获取验证码、粘贴验证码并登录,观察登录成功返回中有 token。这说明接口验证通过后,才能进入“保存登录状态”的下一步。
易错点:调试时只看页面现象,不看网络请求返回;不确认 code 和 message 就改代码。

登录提交处同样要验证结果 ​

为什么需要:登录接口也可能失败,失败后不应继续保存 token 或跳转。
怎么想:获取验证码和登录提交是同一类问题:先判断结果,再执行成功后的动作。
怎么做:在登录提交的 await 后加同样的 code 判断,失败 showToast(res.message) 并 return;成功后再处理返回的 token。转写稿结尾也提到,登录成功返回 token 后,后续应把登录信息存到 Pinia。
易错点:登录失败仍保存 token;把 token 当成固定值;忘记失败提示,用户不知道错在哪。

先校验接口结果,再决定倒计时和后续流程 ​

为什么需要:获取验证码接口不一定成功,只有短信真正发送成功后才应出现倒计时;登录接口失败时也不应继续执行保存 token 等后续逻辑。
怎么想:一般后端约定 code === 10000 表示成功,不等于则失败,并用 message 提示原因。
怎么做:把倒计时放到接口成功之后,失败时 return:

js
const res = await getCode(mobile)
if (res.code !== 10000) {
  ElMessage.error(res.message)
  return
}
// 接口成功后再开始倒计时
startCountdown()

登录提交同理:

js
const res = await login(form)
if (res.code !== 10000) {
  ElMessage.error(res.message)
  return
}
userStore.token = res.data.token

易错点:倒计时写在请求前;失败后忘记 return;把 code、message 字段名写错;提示方法误写成 ElMessage 以外的名字。

用 Pinia 的 user store 记录 token ​

为什么需要:登录成功后接口会返回 token,它是后续请求识别登录状态的凭据,需要集中保存。
怎么想:用户相关数据单独建 store/user.js,与之前练习的 store 区分开。组合式写法中用 ref 定义响应式数据,并且必须 return。
怎么做:

js
// store/user.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const token = ref('')
  return { token }
}, {
  persist: true
})

组件中使用:

js
import { useUserStore } from '@/store/user'
const userStore = useUserStore()

// 登录成功后
userStore.token = res.data.token

易错点:忘记 return { token };组件中只导入不调用 useUserStore();Pinia 中直接赋值即可,不需要 Vuex 的 mutation,Pinia 也没有 mutation 这个概念。

持久化与部分持久化 ​

为什么需要:token 只放在 Pinia 内存中,一刷新页面就丢失,用户需要重新登录,体验很差。
怎么想:把需要长期保留的数据同步到本地存储,使用 Pinia 持久化插件;并非所有数据都值得持久化,只保留必要字段。
怎么做:安装 pinia-plugin-persistedstate 后注册插件:

js
// main.js
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)

只持久化 token:

js
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  return { token }
}, {
  persist: {
    paths: ['token']
  }
})

易错点:先 pinia.use(...) 再创建 pinia 的顺序错误;paths 写成 path;数组里写变量而不是字符串;把所有数据都持久化导致性能和无用数据问题。

表单局部校验与是否同意协议 ​

为什么需要:未填手机号就获取验证码不合理;未勾选协议就登录也不合理。
怎么想:获取验证码前只校验手机号字段,用 validateField(['mobile']);协议勾选用布尔变量控制,点击时取反,提交前判断。
怎么做:

js
try {
  await formRef.value.validateField(['mobile'])
  // 校验通过后再发请求
} catch (err) {
  // 校验失败,不继续
}

协议:

js
const isAgree = ref(false)

function toggleAgree() {
  isAgree.value = !isAgree.value
}

if (!isAgree.value) {
  ElMessage.error('请勾选协议')
  return
}

易错点:validateField 前漏 await;字段名与表单 prop 不一致;协议判断忘记取反;提示文案在公司项目中通常由产品规定,不能随意改。

请求拦截器统一携带 token ​

为什么需要:访问首页、个人中心等接口时通常都要带 token,每个请求手写太啰嗦,也容易漏。
怎么想:在请求拦截器里从 Pinia 取 token,统一放进请求头;没有 token 就不添加。响应拦截器再判断 401,提示未登录并跳转登录页。
怎么做:

js
import { useUserStore } from '@/store/user'

const userStore = useUserStore()
const defaultHeader = {}

if (userStore.token) {
  defaultHeader.Authorization = 'Bearer ' + userStore.token
}

config.headers = {
  ...config.headers,
  ...defaultHeader
}

易错点:在 .vue 组件外不要在顶层导入后立刻 use,应在拦截器实际执行处调用;Bearer 后要有空格;无 token 时不要强行拼接;不要每个接口单独手写请求头。

调试验证 ​

登录后查看 Network 中登录接口返回值是否含 token;在 Application 的 Local Storage 中确认 user 下是否有 token;刷新页面验证 token 是否还在;再发一次普通请求,查看请求头是否出现 Authorization。这样能把“记录登录状态、持久化、统一带 token”串成完整闭环。

先保证登录主流程可靠 ​

为什么需要:持久化保存的是登录后的 token,如果登录接口本身没跑通,后面存什么、取什么都不可靠。
怎么想:先验证接口结果,再更新界面状态。获取验证码时,要等接口成功后再启动倒计时;登录时也要判断 code 是否表示成功。
怎么做:转写稿中反复强调:倒计时代码要放到请求之后;若 code 不等于约定成功值,就用提示组件显示后端 message,并 return,不再执行后续逻辑。
易错点:一点按钮就立刻倒计时;失败后仍继续执行登录成功逻辑;忘记 return。

用独立 user store 保存 token ​

为什么需要:登录信息会被多个页面和拦截器使用,应该和练习 store 区分开。
怎么想:用户相关数据放到 user.js,用组合式写法定义 store,token 必须是响应式数据。
怎么做:按转写稿整理关键结构:

js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore('user', () => {
  const token = ref('')
  return { token }
})

登录成功后直接赋值:

js
user.token = res.data.token

易错点:组合式 store 忘记 return;token 不用 ref;忘记导出 useUserStore,导致导入时没有提示。

Pinia 中直接改状态 ​

为什么需要:Vuex 时代改状态要写 mutation,步骤多;Pinia 取消了这套写法。
怎么想:转写稿明确说,Pinia 里可以直接赋值,不需要再定义 mutation。
怎么做:在登录成功回调里拿到接口返回的 token,直接赋给 user.token。调试时打开 Vue 调试工具或大菠萝面板,确认 user 下的 token 已变化。
易错点:还按 Vuex 思路找 mutation;赋值对象路径写错;把测试值 12345678 留在代码里。

刷新丢失与持久化插件 ​

为什么需要:Pinia 数据在内存中,一刷新就丢,用户又得重新登录,体验很差。
怎么想:把需要长期保留的数据同步到本地存储,这就是持久化。转写稿中的方案是给 Pinia 配合持久化插件。
怎么做:先安装插件,再在 Pinia 实例上 use,最后在创建 store 时加第三个参数:

js
persist: true

验证方式:登录后打开浏览器 Application / Local Storage,能看到 user 下存了 token;刷新页面后数据仍在。
易错点:一定是先有 Pinia 实例,再 pinia.use(...),顺序不能反;组合式 store 的持久化配置要放在第三个参数;只配了插件但 store 没写 persist,不会生效。

只持久化 token:paths ​

为什么需要:不是所有数据都值得长期存,全量持久化会存多余数据,性能也不好。
怎么想:只把需要跨刷新保留的字段列出来。当前最需要的是 token。
怎么做:把 persist: true 改成对象,用 paths 指定字段:

js
persist: {
  paths: ['token']
}

转写稿解释过:插件内部拿到的 key 就是 store 名 user,value 里就是 store 中的数据;paths 里的 'token' 要写字符串,不要写成变量。
易错点:写成 paths: [token];需要多个字段时不会用数组追加;误以为所有数据都必须持久化。

持久化后的 token 如何被请求拦截器使用 ​

为什么需要:访问个人中心等接口要带登录状态,不能每个接口手写一遍 token。
怎么想:在请求拦截器里统一从 Pinia 取 token,放到请求头 Authorization 中。
怎么做:在拦截器使用处调用 useUserStore(),判断 token 是否存在,再追加头信息:

js
const user = useUserStore()
const defaultHeader = {}
if (user.token) {
  defaultHeader.Authorization = 'Bearer ' + user.token
}
config.headers = { ...config.headers, ...defaultHeader }

易错点:在 .js 文件中导入 store 后立刻 use;token 不存在仍强行拼接;固定前缀后的空格漏写。转写稿特别提醒:在 Vue 组件里常见“导入后立刻 use”,但在拦截器这类 .js 中应“哪儿用去哪儿 use”。

调试与验收 ​

登录后先看 Network 返回中是否有 token;再看 Application 的 Local Storage 是否出现 user 下的 token;刷新页面确认不丢;最后发一次请求,在请求头中确认出现 Authorization。这样才算把“Pinia 记录登录状态 + 数据持久化 + 请求携带 token”串通。

代码示例 ​

示例一:Pinia 定义用户 store + 数据持久化(stores/user.js + main.js) ​

js
// stores/user.js
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useUserStore = defineStore(
  'user',
  () => {
    // 记录用户登录状态(token)
    const token = ref('')

    return { token }
  },
  {
    // 第三个参数:持久化配置
    // persist: true 表示该 store 所有数据都存本地
    // persist: { pick: ['token'] } 只持久化指定数据,性能更好
    persist: {
      pick: ['token']
    }
  }
)
js
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
// 先创建 pinia,再 use 插件,顺序不能反
pinia.use(piniaPluginPersistedstate)

createApp(App).use(pinia).mount('#app')

关键行与易错点

  • defineStore('user', setup 函数, { persist }):本项目的 store 采用组合式写法(第二个参数是函数),持久化配置必须写在第三个参数里;写成对象式(选项式)时位置不同,容易写串行。
  • persist: true 会把该 store 的所有 state 都存进 localStorage,数据一多就浪费性能;用 pick: ['token'] 只存需要的字段。
  • pick 数组里必须是字符串(数据名),不要写成变量 [token],那是 ref 对象,不是字段名。
  • 插件 pinia.use(...) 一定要在 createPinia() 之后执行,否则不生效。
  • setup 里定义的 token 必须 return 出去,不 return 既取不到值,也不会有持久化。

示例二:登录页(获取验证码结果校验 + 倒计时位置 + 记录 token + 协议校验) ​

vue
<template>
  <van-form ref="iformRef" @submit="onFormSubmit">
    <van-field v-model="form.mobile" name="mobile" type="tel" placeholder="请输入手机号" />

    <van-field v-model="form.code" name="code" placeholder="请输入验证码">
      <template #button>
        <van-button size="small" type="primary" @click="onTextButtonClick">
          {{ timer > 0 ? `${timer}s后重发` : '获取验证码' }}
        </van-button>
      </template>
    </van-field>

    <div class="agree" @click="toggleAgree">
      <van-radio :checked="isAgree" />
      <span>我已阅读并同意《用户协议》</span>
    </div>

    <van-button type="primary" block native-type="submit">登录</van-button>
  </van-form>
</template>

<script setup>
import { ref, reactive } from 'vue'
import { showToast } from 'vant'
import { useUserStore } from '@/stores/user'
import { sendCode, login } from '@/api/user'

const iformRef = ref(null)
const form = reactive({ mobile: '', code: '' })
const timer = ref(0)
let timerId = null
const isAgree = ref(false) // 记录是否同意协议
const userStore = useUserStore() // 用户相关数据

// 切换是否同意协议
const toggleAgree = () => {
  isAgree.value = !isAgree.value
}

// 获取验证码
async function onTextButtonClick() {
  // 只校验手机号这一个字段
  try {
    await iformRef.value.validateField(['mobile'])
  } catch (err) {
    return // 校验失败,不发请求
  }

  const { data: res } = await sendCode({ mobile: form.mobile })

  // 验证接口是否调用成功
  if (res.code !== 10000) {
    showToast(res.message)
    return // 失败就不再往下执行
  }

  // 只有发送成功,才开始倒计时
  startCountdown()
}

function startCountdown() {
  timer.value = 60
  timerId = setInterval(() => {
    timer.value--
    if (timer.value <= 0) clearInterval(timerId)
  }, 1000)
}

// 提交表单(登录)
async function onFormSubmit() {
  if (!isAgree.value) {
    showToast('请先勾选协议')
    return
  }

  const { data: res } = await login(form)

  if (res.code !== 10000) {
    showToast(res.message)
    return
  }

  // 登录成功:直接给 store 的数据赋值即可(Pinia 无需 mutation)
  userStore.token = res.data.token
}
</script>

关键行与易错点

  • 倒计时代码必须写在接口调用之后:一开始就把 timer 置为 60 是常见瑕疵,短信没发出去也在倒计时;应放在 await sendCode() 成功之后。
  • res.code !== 10000 判定失败后一定要 return,否则失败也继续走倒计时/存 token。
  • validateField(['mobile']) 只校验数组里列出的字段(部分表单验证),返回 Promise,必须 await 并 try/catch,否则校验失败也照样发请求。
  • Pinia 中改数据直接赋值 userStore.token = res.data.token,不要照搬 Vuex 的 commit/mutation,Pinia 里根本没有 mutation 这个概念。
  • 协议用 van-radio 时,选中与否取决于 checked 属性绑定的值,所以要绑一个 ref 变量而不是写死 true/false,点击时取反切换。
  • 提示文案(如“请先勾选协议”)实际项目里由产品规定,标点都不能错。

示例三:请求拦截器统一携带 token(utils/request.js) ​

js
import axios from 'axios'
import { useUserStore } from '@/stores/user'

const instance = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 请求拦截器:统一把 token 带到请求头
instance.interceptors.request.use(
  (config) => {
    // 注意:useUserStore 必须写在拦截器函数内部,不能写在文件顶层
    const userStore = useUserStore()

    const defaultHeader = {} // 预留全局头信息,方便扩展
    if (userStore.token) {
      // Bearer 后面有一个空格,别丢
      defaultHeader.Authorization = 'Bearer ' + userStore.token
    }

    config.headers = { ...config.headers, ...defaultHeader }
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器:401 表示未登录/过期,跳转登录页
instance.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response && error.response.status === 401) {
      // 跳转登录
    }
    return Promise.reject(error)
  }
)

export default instance

关键行与易错点

  • useUserStore() 只能在拦截器回调内部调用(此时 pinia 已挂载);写在模块顶层会报 “getActivePinia was called with no active Pinia”。
  • 在 .js 文件里不要像组件中那样“导入即调用”,要“哪儿用去哪儿调”。
  • 先建 defaultHeader 空对象,再把局部 config.headers 展开合并进去,这样将来新增头信息直接往对象里加,扩展性好。
  • Authorization 拼固定前缀 'Bearer ',末尾空格不能少,写错或漏空格后端识别不到 token。
  • 用 if (userStore.token) 判断,没登录就不加这个头,避免带一个空 token 出去。

常见错误 ​

1. 一点"获取验证码",倒计时就启动了

  • 现象:手机号还没校验、短信也没真正发出去,按钮已经变成"60s 后重发",用户干等。
  • 原因:倒计时那段代码写在了 await getCode() 之前,或者写在了接口调用外面,跟请求结果无关。
  • 修法:把倒计时挪到请求成功之后;同时确认 res.data.code === 10000 才进入倒计时分支。

2. 接口失败了,后面的登录/跳转照样执行

  • 现象:验证码错误或短信没发出去,控制台照样打登录成功的日志,甚至带着空 token 跳页。
  • 原因:只弹了 ElMessage.error(...),没有中断流程——提示不等于拦截。
  • 修法:在失败分支补 return(或在拦截器里 Promise.reject),让"失败"真正意味着"后面不执行"。

3. 在 http.ts 顶部直接 const userStore = useUserStore() 报 "no active Pinia"

  • 现象:一刷新页面就白屏,控制台提示 getActivePinia / no active Pinia。
  • 原因:.ts 模块顶层代码在 app.use(pinia) 之前就执行了,此时 Pinia 还没装到应用上;而组件里之所以能这么写,是因为组件渲染时机晚于安装。
  • 修法:把 useUserStore() 移到真正使用它的地方——请求拦截器的回调函数内部(每次请求时再取)。

4. 刷新页面 token 就没了

  • 现象:登录后正常,F5 一下跳回登录页。
  • 原因:数据只活在 Pinia 内存里;或者装了持久化插件但 pinia.use(...) 写在 createPinia() 之前,插件没生效。
  • 修法:检查顺序为"先 createPinia() → 再 pinia.use(插件) → 最后 app.use(pinia)",并到 DevTools → Application → Local Storage 里确认有对应 key。

5. defineStore 写完,组件里拿不到任何数据

  • 现象:store.token 是 undefined,或者干脆报"没有导出 useUserStore"。
  • 原因:组合式写法的 store 忘了写 return { token };或者忘了 export。
  • 修法:setup store 里必须显式 return 要暴露的 state / 方法;文件顶部补 export const useUserStore = defineStore(...)。

6. Authorization 头带上了,但后端仍判未登录

  • 现象:Network 里能看到 Authorization,值形如 Bearerxxx,接口返回 401。
  • 原因:Bearer 与 token 之间漏了空格;或者把 Bearer ${token} 写成了 Bearer + 变量没拼进去。
  • 修法:统一用模板字符串 Bearer ${token},并顺手判断 token 不存在时整个头都不要加,避免发出 Bearer undefined。

7. 部分校验写了,但没拦住请求

  • 现象:手机号空着点"获取验证码",校验提示闪一下,请求还是发出去了。
  • 原因:validateField 忘了 await,Promise 还没 settle 就往下走了;或者传参写成了字符串 'mobile' 而不是数组 ['mobile']。
  • 修法:await formRef.value.validateField(['mobile']),并把发请求的代码放进 try 的成功路径,失败走 catch。

8. 协议勾选框点了没反应

  • 现象:isAgree 永远是 false,怎么点都不变,登录永远被拦。
  • 原因:只做了 :checked="isAgree" 的单向绑定,没有绑定点击事件去改值;或者用了 v-model 又同时手写 :checked 冲突。
  • 修法:@click 里 isAgree.value = !isAgree.value,保持"一个数据源"。

练习 ​

练习 1|让失败真的失败 把"获取验证码"和"提交登录"两处调用改成统一结构:try { await 接口 → 判 code → 成功逻辑 } catch { 提示 }。要求:故意把验证码填错,确认控制台不再打印登录成功的日志,也不发生跳转。

练习 2|持久化范围收窄 先让 store 里同时存在 token 和一个临时字段 loading,观察默认配置下两者是否都进了 Local Storage;然后用 pick 只保留 token,再对比一次。写出你观察到的差异(哪个 key、哪个字段消失)。

练习 3|拦截器边界测试 在请求拦截器里加一句临时 console.log,分别测试三种状态:未登录(无 token)、已登录(有 token)、登录后手动删掉 Local Storage 里的 token 再刷新页面。记录每次请求头里 Authorization 的实际值,并解释第三种情况下你的代码走到了哪个分支。

练习 4|把校验做全 给"获取验证码"补上手机号格式校验(不只是非空):先做 validateField,再自行判断是否为 11 位数字。要求两种失败给出不同提示文案,且都不发请求。

练习 5|迁移到短信登录页 用同样的四步(校验 → 提交 → 判 code → 存 store)把另一个不带验证码的登录页做完。做完后自问:哪几行是重复的、可以抽成公共函数?把你的抽法写出来。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医咨询-接口调用结果验证05:12
2优医咨询-Pinia记录登录状态09:27
3优医咨询-Pinia数据持久化09:51
4优医咨询-Pinia数据持久化进阶09:51
5优医咨询-Pinia部分数据持久化05:18
6优医咨询-uniForms部分表单数据验证07:11
7优医咨询-是否同意协议06:33
8优医咨询-权限验证发送token12:22

小结 ​

  1. 主流程跑通只是起点,真正的工程质量体现在"失败路径"上——每个 await 之后都要问一句"失败了怎么办"。
  2. ElMessage 是给人看的,return 是给代码看的,两者缺一不可。
  3. 倒计时、跳转、写 store 这类"副作用",一律放在接口确认成功之后。
  4. Pinia 的 store 用法在组件内和 .ts 模块里不一样:组件里可以顶层 use,模块里必须"哪儿用哪儿 use"。
  5. Pinia 没有 mutation,直接赋值即可;但"能直接改"不等于"随便改",改的地方应集中在动作触发点。
  6. 持久化不是"全存就好":默认全量存会带来性能与脏数据问题,用 pick 白名单才是常规做法。
  7. 插件生效有三个前置条件:装包、pinia.use、以及在 createPinia() 之后调用——顺序错了不报错但无效。
  8. 表单校验的价值在于"拦在请求之前":validateField + await + try/catch 三件套,缺一个就等于没校验。
  9. token 的传递与失效判断分居两个拦截器:请求拦截器负责"带上",响应拦截器负责"过期了就赶去登录"。
  10. 文案、提示语这类细节在真实项目里由产品定义,写代码时就要养成"不自己编词"的习惯。