主题
组件库、生命周期与 pinia(三)
本页对应 组件库、生命周期与 pinia,共 6 段视频,约 57 分钟。
学习目标
- 能够使用 Pinia 组合式 API 定义包含 state 和 actions 的 store,并在 Vue 3
<script setup>页面中导入、调用,验证数据响应式更新。 - 能够使用
storeToRefs解构 Pinia store 中的 state,并直接解构 actions,验证页面加减交互正常。 - 能够安装并注册
pinia-plugin-persistedstate,通过persist配置实现指定数据持久化,并刷新页面验证数据不丢失。 - 能够使用
createPersistedState自定义storage的setItem/getItem,基于uni.setStorageSync/getStorageSync使持久化兼容 H5 与小程序。 - 能够通过
key配置持久化键名前缀,并通过paths仅持久化指定字段,验证本地存储键名与存储内容符合预期。
知识点
一、先看【我的】模块的整体思路
为什么需要:【我的】页面要展示司机个人信息、本月任务数据,车辆信息则在独立页面展示。三个数据来源不同,必须分别调用接口,不能靠静态写死。
怎么想:按“接口层 → 页面层”拆。先在 apis/user.js 里定义 profile()、task()、truck() 三个方法,再在页面 onLoad 中请求,拿到 data 后赋给 ref 渲染。转写稿里反复强调“先导出,再导入;保存后刷新;控制台打印看数据有没有出来”,这套调试习惯同样适用于本节接口联调。
怎么做:先写接口方法,再写页面请求函数,最后在模板绑定字段。每个请求都要判断 code !== 200 并 uni.utils.toast()。
易错点:导入路径、方法名拼写、接口字段名要对照文档;不要直接把响应整体塞进模板,要取 data。
二、个人信息查询
为什么需要:页面要显示头像、名称、司机编号、手机号码,这些是登录司机的动态资料。
怎么想:接口只负责取数据,页面只负责渲染。请求放在 onLoad 中,保证进入页面就加载。
怎么做:在 apis/user.js 增加 profile(),页面中请求并赋值。
javascript
// apis/user.js
import { uniFetch } from './uni-fetch'
export default {
profile() {
return uniFetch.get('/driver/users')
},
task(year, month) {
return uniFetch.get('/driver/users/taskReport', { year, month })
},
truck() {
return uniFetch.get('/driver/users/truck')
},
}vue
<!-- pages/my/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import userApi from '@/apis/user'
const userProfile = ref({})
const taskInfo = ref({})
onLoad(() => {
getUserProfile()
getTaskInfo()
})
async function getUserProfile() {
try {
const { code, data } = await userApi.profile()
if (code !== 200) return uni.utils.toast()
userProfile.value = data
} catch (err) {
console.log(err)
}
}
</script>
<template>
<view class="page-container">
<view class="user-profile">
<image class="avatar" :src="userProfile.avatar" mode=""></image>
<text class="username">{{ userProfile.name }}</text>
<text class="no">司机编号:{{ userProfile.phone }}</text>
<text class="mobile">手机号码:{{ userProfile.number }}</text>
</view>
</view>
</template>易错点:笔记模板中“司机编号”绑 phone、“手机号码”绑 number,联调时要以接口文档字段为准核对,不要只凭字段名猜;请求失败要 toast,不要静默。
三、任务数据查询
为什么需要:展示本月任务总量、完成任务量、运输里程。笔记注明这些数据初始为 0,后续【任务】模块完成后会更新。
怎么想:任务数据依赖年月参数,所以接口方法要接收 year、month,页面调用时传示例值。
怎么做:在页面增加 getTaskInfo(),在 onLoad 中调用。
javascript
async function getTaskInfo() {
try {
const { code, data } = await userApi.task('2023', '07')
if (code !== 200) return uni.utils.toast()
taskInfo.value = data
} catch (err) {
console.log(err)
}
}vue
<view class="month-overview">
<view class="title">本月任务</view>
<view class="content">
<view class="item">
<text class="volumn">{{ taskInfo.taskAmounts }}</text>
<text class="label">任务总量</text>
</view>
<view class="item">
<text class="volumn">{{ taskInfo.completedAmounts }}</text>
<text class="label">完成任务量</text>
</view>
<view class="item">
<text class="volumn">{{ taskInfo.transportMileage }}</text>
<text class="label">运输里程(km)</text>
</view>
</view>
</view>易错点:年月参数格式要与文档一致;初始值显示 0 是正常现象,不要误判接口失败;字段名 taskAmounts、completedAmounts、transportMileage 不要写错。
四、车辆信息查询
为什么需要:车辆照片、车牌号、车型、所属机构、载重等信息在独立页面展示,方便司机查看当前车辆。
怎么想:图片用 swiper 轮播 pictureList,文本信息用 uni-list 展示。请求逻辑与前面一致。
怎么做:
vue
<!-- subpkg_user/truck/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import userApi from '@/apis/user'
const truckInfo = ref({})
onLoad(() => {
getTruckInfo()
})
async function getTruckInfo() {
try {
const { code, data } = await userApi.truck()
if (code !== 200) return uni.utils.toast()
truckInfo.value = data
} catch (err) {
console.log(err)
}
}
</script>
<template>
<view class="page-container">
<swiper class="truck-pictures" indicator-active-color="#fff" circular indicator-dots>
<swiper-item v-for="picture in truckInfo.pictureList" :key="picture.url">
<image class="picture" mode="aspectFill" :src="picture.url"></image>
</swiper-item>
</swiper>
<view class="truck-meta">
<uni-list :border="false">
<uni-list-item :border="false" title="车辆编号" :right-text="truckInfo.id" />
<uni-list-item :border="false" title="车辆号牌" :right-text="truckInfo.licensePlate" />
<uni-list-item :border="false" title="车型" :right-text="truckInfo.truckType" />
<uni-list-item :border="false" title="所属机构" :right-text="truckInfo.currentOrganName" />
<uni-list-item :border="false" title="载重" :right-text="truckInfo.allowableLoad" />
</uni-list>
</view>
</view>
</template>易错点:笔记原代码中 truchInfo 是拼写笔误,建议统一改为 truckInfo,否则模板取值容易出错;v-for 的 :key 用 picture.url;图片模式用 aspectFill 避免变形。转写稿里“拼亚/皮尼”即 Pinia,本节不展开,但“命名要语义化、保存后刷新验证、控制台打印排查”的调试思路可以沿用。
本节作业要做什么
本节笔记只有一句核心要求:独立完成第3章节3个接口的调用。转写稿里没有出现这3个接口的名称、请求地址、请求参数和返回结构,因此这里不能替你编造三个接口,也不应凭空写死 URL。作业的正确目标是:按第3章讲义或接口文档,把3个接口逐个调通,能拿到返回数据,能在页面或Pinia store中正确使用,并做好基本异常处理。
为什么需要先明确这一点?因为“接口调用”不是只写三行 import,而是要完成“请求—返回—渲染/存储—异常”的闭环。怎么想?先把3个接口列成清单:接口名、请求方法、URL、入参、返回字段、使用页面。怎么做?每完成一个就在清单上打勾,避免最后只调了一个却以为完成了三个。易错点:把示例代码当成真实接口;只写请求不处理返回;参数名按记忆写而不对照文档。
先纠正常见同音字和API名
转写稿里出现了不少口头同音字,写代码时要纠正:Pinia不是“拼命/P尼亚/P命”;store不是“死道/此道”;use不是“柚子”;create不是“可瑞特”;paths不是“pass”;getItem/setItem不是“盖子”。为什么需要纠正?因为这些名字一旦写错,编辑器会直接报错,调试时间都耗在拼写上。怎么想?以官方文档和项目现有代码为准。怎么做?导入具名导出时加大括号;store 导出的是函数,页面里要调用后才能拿到实例。易错点:转写稿明确提醒过,import 时不要漏大括号;useXxxStore() 不要只写 useXxxStore。
用Pinia组织3个接口的调用
转写稿演示了在 store 中定义数据和方法,并全部导出,页面再导入使用。这个思路可以迁移到作业:每个接口对应一个 action,页面只负责触发和展示。为什么需要?因为接口结果如果散落在页面里,三个接口容易重复、难维护。怎么想?先定义 store,再导出 state 和 action,最后在页面调用。怎么做?可参考下面模板,但接口名、参数、URL必须按第3章文档替换,不能照抄:
js
// store/xxx.js:示意模板,不是第3章真实接口
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useXxxStore = defineStore('xxx', () => {
const list = ref([])
async function fetchOne(params) {
// 按第3章文档填写请求方法、地址、参数
// const res = await request(...)
// list.value = res.data
}
return { list, fetchOne }
})页面中:
js
import { useXxxStore } from '@/store/xxx'
const xxxStore = useXxxStore()
// 按文档调用对应 action易错点:只定义 action 不导出;页面导入路径写错;三个接口共用一个 action 却传错参数;把 storeToRefs 用在方法上。转写稿说得很清楚:storeToRefs 主要用于 state 数据,方法直接解构即可。
调试顺序:先打印,再渲染,再交互
转写稿的调试过程很有参考价值:先 console.log(store) 看 store 里有没有数据和方法,再点按钮验证响应式,最后刷新页面看持久化。作业也按这个顺序:先单独调通一个接口,打印返回结果;确认字段后再绑定页面;三个接口逐个过,不要一次全接。为什么需要?因为三个接口同时接,报错时很难判断是请求、字段还是渲染问题。怎么做?每调一个接口就看一次 Network 和控制台;返回字段对不上时先打印完整响应,不要猜。易错点:只看页面没变化就认定接口失败;刷新后数据丢失误判为接口失败;控制台有报错却继续接下一个接口。
持久化与解构的作业级注意
转写稿补充了持久化:刷新页面后内存数据会丢失,token 这类数据需要长期保存;插件默认可能用 localStorage,小程序环境不适用,所以要自定义 storage;key 加前缀便于识别;paths 只存必要数据,避免 persist: true 把所有数据都存下来造成性能损耗。作业中如果某个接口结果需要跨刷新保留,只持久化必要字段。易错点:把整个 store 全量持久化;key 与项目其他 store 冲突;解构 state 后丢失响应式,却误以为接口没返回。
验收清单
交作业前逐项检查:3个接口是否都真实调用;请求方法、URL、参数是否来自第3章文档;返回数据是否被使用;页面是否有成功/失败反馈;控制台是否无报错;是否清理了多余 console.log;刷新后必要数据是否仍在。转写稿最后也说明,这部分内容是留给学生独立完成的作业,因此本文只给完成路径和调试方法,不代写三个接口的具体实现。
代码示例
示例一:apis/user.js —— 三个接口的统一封装
js
// apis/user.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
/**
* 个人资料
*/
profile() {
return uniFetch.get('/driver/users')
},
/**
* 任务数据
* @param {string} year - 任务数据的年份
* @param {string} month - 任务数据的月份
*/
task(year, month) {
return uniFetch.get('/driver/users/taskReport', { year, month })
},
/**
* 车辆信息
*/
truck() {
return uniFetch.get('/driver/users/truck')
},
}关键行与易错点
- 三个方法都返回
uniFetch.get(...)的返回值(Promise),必须return,否则页面里await userApi.profile()拿到的是undefined。 task(year, month)的第二个参数是查询参数对象{ year, month },不是拼在 URL 上,交给uni-fetch统一处理。- 统一用
export default { ... }导出对象,页面里才能import userApi from '@/apis/user'后以userApi.profile()调用。 - 注意
import路径写相对路径./uni-fetch,不要写成@/apis/uni-fetch,两者指向不同。
示例二:pages/my/index.vue —— 个人信息 + 任务数据
vue
<!-- pages/my/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import userApi from '@/apis/user'
// 用户信息
const userProfile = ref({})
// 任务数据
const taskInfo = ref({})
onLoad(() => {
getUserProfile()
getTaskInfo()
})
// 用户信息接口
async function getUserProfile() {
try {
const { code, data } = await userApi.profile()
// 检测接口是否调用成功
if (code !== 200) return uni.utils.toast()
// 渲染请求来的数据
userProfile.value = data
} catch (err) {
console.log(err)
}
}
// 任务数据
async function getTaskInfo() {
try {
const { code, data } = await userApi.task('2023', '07')
// 检测接口是否调用成功
if (code !== 200) return uni.utils.toast()
// 渲染请求来的数据
taskInfo.value = data
} catch (err) {
console.log(err)
}
}
</script>
<template>
<view class="page-container">
<!-- 个人信息 -->
<view class="user-profile">
<image class="avatar" :src="userProfile.avatar" mode=""></image>
<text class="username">{{ userProfile.name }}</text>
<text class="no">司机编号:{{ userProfile.phone }}</text>
<text class="mobile">手机号码:{{ userProfile.number }}</text>
</view>
<!-- 本月任务 -->
<view class="month-overview">
<view class="title">本月任务</view>
<view class="content">
<view class="item">
<text class="volumn">{{ taskInfo.taskAmounts }}</text>
<text class="label">任务总量</text>
</view>
<view class="item">
<text class="volumn">{{ taskInfo.completedAmounts }}</text>
<text class="label">完成任务量</text>
</view>
<view class="item">
<text class="volumn">{{ taskInfo.transportMileage }}</text>
<text class="label">运输里程(km)</text>
</view>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.page-container {
padding: 30rpx;
}
.user-profile {
display: flex;
flex-direction: column;
align-items: center;
padding: 40rpx 0;
.avatar {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
}
.username {
margin-top: 20rpx;
font-size: 36rpx;
font-weight: bold;
}
.no,
.mobile {
margin-top: 10rpx;
color: #818181;
font-size: 26rpx;
}
}
.month-overview {
margin-top: 30rpx;
background-color: #fff;
border-radius: 12rpx;
padding: 24rpx;
.title {
font-weight: bold;
margin-bottom: 20rpx;
}
.content {
display: flex;
.item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.volumn {
font-size: 40rpx;
font-weight: bold;
}
.label {
margin-top: 8rpx;
color: #818181;
font-size: 24rpx;
}
}
}
}
</style>关键行与易错点
onLoad必须从@dcloudio/uni-app引入,不是从vue;两个接口在onLoad里并列调用,互不阻塞。await userApi.profile()的结果用解构const { code, data } = ...,先判code !== 200再赋值,return uni.utils.toast()表示失败即中断,不渲染脏数据。- 一定用
userProfile.value = data,不能写userProfile = data,否则ref变量被整体覆盖,响应式丢失。 - 模板里字段名(
name/phone/number/taskAmounts/completedAmounts/transportMileage)必须与接口返回一一对应,建议先在控制台打印data核对,再落到模板。 taskInfo初始是空对象{},接口未返回前页面显示为空白;若希望初始显示 0,可把初始值写成ref({ taskAmounts: 0, completedAmounts: 0, transportMileage: 0 })。
示例三:subpkg_user/truck/index.vue —— 车辆信息查询
vue
<!-- subpkg_user/truck/index.vue -->
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import userApi from '@/apis/user'
// 车辆信息
const truchInfo = ref({})
onLoad(() => {
getTruckInfo()
})
// 获取车辆信息
async function getTruckInfo() {
try {
const { code, data } = await userApi.truck()
// 检测接口是否调用成功
if (code !== 200) return uni.utils.toast()
// 渲染请求来的数据
truchInfo.value = data
} catch (err) {
console.log(err)
}
}
</script>
<template>
<view class="page-container">
<!-- 车辆照片轮播 -->
<swiper
class="truck-pictures"
indicator-active-color="#fff"
circular
indicator-dots
>
<swiper-item v-for="picture in truchInfo.pictureList" :key="picture.url">
<image class="picture" mode="aspectFill" :src="picture.url"></image>
</swiper-item>
</swiper>
<!-- 车辆元信息 -->
<view class="truck-meta">
<uni-list :border="false">
<uni-list-item
:border="false"
title="车辆编号"
:right-text="truchInfo.id"
/>
<uni-list-item
:border="false"
title="车辆号牌"
:right-text="truchInfo.licensePlate"
/>
<uni-list-item
:border="false"
title="车型"
:right-text="truchInfo.truckType"
/>
<uni-list-item
:border="false"
title="所属机构"
:right-text="truchInfo.currentOrganName"
/>
<uni-list-item
:border="false"
title="载重"
:right-text="truchInfo.allowableLoad"
/>
</uni-list>
</view>
</view>
</template>
<style lang="scss" scoped>
.truck-pictures {
width: 750rpx;
height: 380rpx;
.picture {
width: 100%;
height: 100%;
}
}
.truck-meta {
margin-top: 20rpx;
background-color: #fff;
}
</style>关键行与易错点
swiper循环的是truchInfo.pictureList,ref({})初始为空对象,pictureList为undefined时v-for不会报错但轮播为空,接口回来后才渲染,属正常现象。:key="picture.url"用图片地址做 key,避免用index。<image>用mode="aspectFill"保证图片铺满不变形;swiper上的indicator-dots/circular/indicator-active-color都是布尔/字符串属性,直接写即可。uni-list-item用:right-text绑定右侧文字,title是左侧标题,别把两者写反;right-text前有冒号表示绑定变量,不加冒号就变成纯字符串字面量。- 页面在
subpkg_user分包下,import userApi from '@/apis/user'依然用@别名,不要写成../../apis/user,避免层级算错。 - 变量名
truchInfo(笔记中即如此拼写)在script与template中必须保持一致,改名时记得全局替换,否则模板渲染为空白。
常见错误
现象:页面加载后头像、姓名、编号全空白,控制台无报错。 原因:
ref({})写成普通对象const userProfile = {},异步赋值后视图不更新。 修法:状态一律用ref/reactive包裹;渲染前用console.log(data)确认字段真实存在再绑模板。现象:司机编号和手机号显示反了(编号位置出现 11 位手机号)。 原因:凭字段名猜语义,
number/phone未与接口文档逐个比对。 修法:以接口文档字段说明为准绑定,别按单词字面意思猜;联调时把返回值贴到模板旁对照一遍。现象:任务总量、完成量、里程恒为 0,或换月后数据仍是上月。 原因:
userApi.task('2023', '07')年月写死。 修法:用dayjs().year()/dayjs().format('MM')动态取值,并注意接口要求月份是两位数还是数字。现象:接口失败时页面一片空白,用户不知道发生了什么。 原因:
if (code !== 200) return uni.utils.toast()未传文案,catch里只console.log。 修法:toast(msg)必须带提示语,catch中同样给出兜底提示;关键页面再加空状态/骨架屏。现象:车辆照片轮播区域高度为 0,图片不显示或严重变形。 原因:
swiper/image未设宽高,或mode用了默认scaleToFill拉伸。 修法:给.truck-pictures和.picture显式设置高度与width:100%,image用aspectFill。现象:Pinia 中解构出的 count 改了不生效,或对方法误用
storeToRefs报错。 原因:直接解构 state 丢失响应性;storeToRefs只处理 state/getters。 修法:state 用storeToRefs(store),actions 直接从 store 上普通解构。现象:H5 端持久化正常,小程序端刷新后数据丢失。 原因:插件默认走
localStorage,小程序无此 API。 修法:用createPersistedState({ storage: { setItem: uni.setStorageSync, getItem: uni.getStorageSync } })自定义存储。现象:持久化后本地缓存体积异常大,页面变卡。 原因:第三个参数直接传
true,store 内所有字段全量落盘。 修法:改用{ paths: ['userProfile'] }白名单,只存必要字段(如 token、用户信息)。
练习
三接口串联:独立完成
profile/task/truck三个方法的封装与页面渲染,要求每个页面先打印完整响应再绑模板,提交时截图对比"接口返回字段"与"模板绑定字段"一一对应。动态年月:把任务数据的年月改为动态获取,并在页面上支持"上一月/下一月"切换,切换后重新拉取数据(注意
onLoad只执行一次,切换需自己写方法调用)。统一请求处理:把"判断 code → toast → 赋值 → catch"这套逻辑抽成一个可复用的封装(如
useRequest或uni.utils.request),三个页面改用它,要求失败时有文案、成功时返回 data。异常兜底:给车辆信息页加空状态与图片加载失败兜底(
image的@error),并给三个接口加uni.showLoading/hideLoading。Store + 持久化:把用户信息存入 Pinia,用
storeToRefs解构渲染,配置持久化paths: ['userProfile']与自定义uni.setStorageSync存储,分别在 H5 和小程序端刷新验证数据不丢失。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 神领物流-Pinia定义Store-2 | 12:40 |
| 2 | 神领物流-pinia数据解构 | 04:27 |
| 3 | 神领物流-pinia数据持久化-1 | 08:48 |
| 4 | 神领物流-pinia数据持久化-2 | 08:33 |
| 5 | 神领物流-pinia数据持久化-3 | 14:52 |
| 6 | 神领物流-pinia数据持久化-4 | 08:08 |
小结
- 三个接口的开发套路是固定的:定义方法 → 页面
onLoad调用 → 校验code→ 赋值渲染,没有例外。 - 字段以接口文档为准,不能凭单词字面猜语义,
number/phone这类易混字段必须逐一核对。 - 状态必须用
ref/reactive包裹,否则异步赋值后视图不会更新,且这种错误往往不报错、最难查。 - 请求失败必须有用户可见的提示,只写
console.log等于把问题留给用户。 - 写死参数(年月、ID)在本地能跑通,但换月/换人即失效,属于必须提前消除的隐患。
storeToRefs只解决 state/getters 的响应性丢失,actions 直接解构即可。- 持久化在小程序端必须自定义 storage,否则默认
localStorage方案静默失效。 - 持久化用
paths白名单而非true,存得越少越稳、越快。