主题
组件库、生命周期与 pinia(二)
本页对应 组件库、生命周期与 pinia,共 7 段视频,约 50 分钟。
学习目标
- 能分别启动神领物流项目的 H5、小程序和 App 端,并说明 HBuilderX 启动 H5 时新开浏览器的现象,能将地址复制到常用浏览器中进行调试。
- 能安装网络请求模块,在
api目录创建统一请求实例文件,完成baseURL、loading、请求/响应拦截器等初始化配置。 - 能在页面中导入封装好的请求实例并发送请求,通过浏览器 Network 面板验证请求地址、loading 效果和 baseURL 配置是否生效。
- 能创建工具函数目录并封装轻提示方法,支持传入标题和图标参数,并将其挂载到
uni全局对象上直接调用。 - 能完成 Pinia 的安装、导入、
createPinia创建实例并通过app.use与 Vue 应用整合。 - 能使用
defineStore定义 Store,结合ref创建响应式状态并通过return暴露,能在页面中访问该状态。
知识点
登录状态为什么要先交给 Pinia 管理
神领物流的接口有一个前提:所有接口都要求登录后才能访问。转写稿里同学调接口看到 401,就是因为还没登录;这个 401 在當前阶段不影响我们测试前面的功能,但登录功能迟早要做。登录后要把 token 等登录状态记录下来,Vue2 项目常用 Vuex3,Vue3 项目则使用 Vuex4 或 Pinia。本节选择 Pinia。
为什么需要: 登录状态不能只放在某个页面里,否则其他页面、请求封装都拿不到 token。
怎么想: 把 Pinia 理解成 Vuex 的替代品:听到 Pinia,就联想到 Vuex;它们都负责集中管理状态。
怎么做: 先安装 Pinia,再在入口文件初始化,最后定义 store 保存状态。
易错点: 不要把 Vuex 的 mutation、modules 思路硬套到 Pinia;也不要因为接口 401 就误判 Pinia 配错了。
Pinia 的特点与使用定位
笔记列出 Pinia 的特点:API 更简单,去掉了 mutation;提供组合式风格 API;去掉 modules 概念,每个 store 都是独立模块;配合 TypeScript 更友好,类型推断可靠。
为什么需要: Vuex 在 Vue3 中逐渐被 Pinia 替代,新项目优先 Pinia 可以减少模板代码。
怎么想: 不要先背优点,而要先建立对应关系:Vuex 的 store 对应 Pinia 的 store,Vuex 的 state 对应组合式 store 里用 ref 定义的数据。
怎么做: 每个业务模块建一个独立 store 文件,例如 stores/counter.js。
易错点: “去掉了 modules”不等于所有状态都塞一个文件;每个 store 独立,但仍要按业务分文件,便于调试。
安装 Pinia
转写稿中安装时强调要在项目根目录执行。笔记里的安装代码块未展开,按转写稿整理为:
bash
npm install pinia为什么需要: 不安装就无法从 pinia 导入 createPinia 和 defineStore。
怎么想: 安装前先确认终端当前目录是不是神领物流项目根目录;内置终端不好用,可以用外置终端“使用命令行打开所在目录”。
怎么做: 在项目根目录执行安装命令,等安装完成后再启动项目。
易错点: 装错目录、没装完就运行、把 Vue2 的分支代码粘进 Vue3 项目,都会导致导入失败或运行报错。
在入口文件初始化 Pinia
转写稿中初始化 Pinia 分三步:导入 Pinia、创建 Pinia 实例、让 Vue 使用 Pinia。因为项目是 Vue3,转写稿还提醒把模板里“不是 Vue3”的条件分支删掉,避免粘错代码。整理后的核心代码是:
js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createSSRApp(App)
const pinia = createPinia()
app.use(pinia)
export function createApp() {
return { app }
}为什么需要: 只安装不初始化,Vue 应用并不知道要使用 Pinia。
怎么想: 记住三行三步骤:导入 createPinia、创建 pinia、app.use(pinia);顺序本质上不影响,但建议先导入模块再写业务。
怎么做: 在入口文件里完成上述三步,再启动项目验证是否报错。
易错点: 把 Vue2 的 Vue.use(PiniaVuePlugin) 写法粘进来;忘记调用 createPinia();导入路径或文件名写错。
定义第一个 Store:counter
转写稿先建 stores 目录,再建 counter.js,用 defineStore 定义 store。组合式写法中,第一个参数是 store 名字,第二个参数是函数;函数里用 ref 定义响应式数据,并 return 出去。
js
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
return { count }
})为什么需要: 登录状态最终要放进 store,所以先用 counter 这个小例子把定义流程跑通,再扩展更安全。
怎么想: store 就是“仓库”;组合式 store 里定义的数据要用 ref 才有响应式,外部要用就必须 return。
怎么做: 建 stores/counter.js,调用 defineStore('counter', () => { ... }),导出 useCounterStore。
易错点: store 的 id 建议和文件名一致,便于调试;函数里忘记 return,外部就拿不到数据;defineStore 是函数,不是组件;id 不要重复。转写稿也提醒:先写这一小段,不要一次写太多,否则排错困难。
本节目标与整体思路
【我的】模块里有 3 个独立接口:个人信息、任务数据、车辆信息。它们本身不难,难点在于养成统一习惯:接口方法先写到 apis/user.js,页面里只负责取数据和渲染。
- 为什么需要:如果每个页面直接写
uni.request,基地址、token、响应判断会散落各处;本节代码统一用已经封装好的uniFetch,页面只关心业务字段。 - 怎么想:先按接口文档定义方法,再在页面
onLoad中调用;拿到code === 200的data后赋值给ref,模板直接绑定。 - 怎么做:三个接口分别对应
profile()、task(year, month)、truck()。 - 易错点:不要漏掉
code !== 200的判断;未登录时接口可能返回 401,这不影响先验证请求是否发出;HBuilderX 启动 H5 时可能新开一个浏览器,调试时把地址复制到常用浏览器里,插件才可用。
3.1 个人信息查询
接口文档在这里。
- 为什么需要:页面要展示当前登录司机的头像、名称、编号,这些数据不能写死。
- 怎么想:先定义
profile(),再在pages/my/index.vue的onLoad里调用。 - 怎么做:
javascript
// apis/user.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
// 省略中间部分代码...
/**
* 个人资料
*/
profile() {
return uniFetch.get('/driver/users')
},
}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({})
onLoad(() => {
getUserProfile()
})
// 用户信息接口
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,不要凭直觉改;uni.utils.toast()是前面封装的全局轻提示,未登录或失败时会给出提示。
3.2 任务数据查询
接口文档在这里。
- 为什么需要:展示任务总量、完成任务量、运输里程;这些数据初始为 0,后续开发【任务】模块后会更新。
- 怎么想:接口需要年和月,所以
task(year, month)接收参数,页面里先按笔记传'2023'、'07'。 - 怎么做:
javascript
// apis/user.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
// 省略中间部分代码...
/**
* 任务数据
* @param {string} year - 任务数据的年份
* @param {string} month - 任务数据的月份
*/
task(year, month) {
return uniFetch.get('/driver/users/taskReport', { year, month })
},
}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 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="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>
</template>- 易错点:
task的参数顺序不要反;taskInfo初始给{},避免模板取值为undefined;onLoad里两个请求可以并列调用,但都要各自判断code。
3.3 车辆信息查询
接口文档在这里。
- 为什么需要:车辆信息页要展示车辆照片、车牌号、车型等。
- 怎么想:定义
truck(),在subpkg_user/truck/index.vue中调用;图片列表用swiper渲染。 - 怎么做:
javascript
// apis/user.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
// 省略中间部分代码...
/**
* 车辆信息
*/
truck() {
return uniFetch.get('/driver/users/truck')
},
}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>- 易错点:笔记里变量名是
truchInfo,模板和脚本必须保持一致;pictureList为空时swiper不显示图片,先确认接口是否返回;uni-list-item的right-text字段要和接口字段对应。
代码示例
示例 1:Pinia 初始化 + 定义 store(main.js / stores/counter.js)
javascript
// main.js
import { createSSRApp } from 'vue'
import App from './App.vue'
// 1. 导入 Pinia
import { createPinia } from 'pinia'
export function createApp() {
const app = createSSRApp(App)
// 2. 创建 Pinia 实例
const pinia = createPinia()
// 3. 与 Vue 整合(注册插件)
app.use(pinia)
return { app }
}javascript
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
// 调用 defineStore 定义仓库:第一个参数是仓库名(建议与文件名保持一致)
export default defineStore('counter', () => {
// 定义响应式数据
const count = ref(0)
// 必须 return,外部才能访问到 count
return { count }
})关键行与易错点
- 初始化只有三步:导入
createPinia→createPinia()创建实例 →app.use(pinia)与 Vue 整合,少任何一步 store 都用不了。 - 转写稿强调:项目已确定基于 Vue3 开发,
main.js里判断#ifndef VUE3的分支可以直接删掉,避免把 Pinia 写进 Vue2 的分支里导致不生效。 defineStore有两个参数:第一个是仓库名字符串,第二个是函数;名字建议与文件名(counter)一致,方便调试和排错。- 第二个参数里必须用
ref()包一层才是响应式数据,直接写const count = 0页面不会跟着变。 - 函数内部定义的数据一定要
return出去(这是函数不是对象),否则页面拿不到count。
示例 2:【我的】页面——个人信息 + 任务数据(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>配套接口定义(apis/user.js):
javascript
// 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 })
},
}关键行与易错点
- 接口方法统一收拢到
apis/user.js里,页面只调用userApi.xxx(),不要把 URL 散落在页面中。 if (code !== 200) return uni.utils.toast()是统一失败处理;转写稿里uni.utils.toast是挂在全局uni上的,不需要 import,因为uni本身就是全局对象(类似小程序的wx)。- 一定要
await+try/catch,否则请求异常会变成未捕获的 Promise 错误。 - 数据用
ref({})初始化为空对象,模板里{{ taskInfo.taskAmounts }}首次渲染为空,不要用ref(null),否则会报读取属性错误。 - 该接口目前未登录会返回 401,这是正常的(神领物流所有接口都要求登录态),后续做完登录才能拿到真实数据。
- 注意讲义里字段映射:司机编号用
userProfile.phone、手机号用userProfile.number,抄代码时不要凭感觉“纠正”,以接口返回为准。
示例 3:【车辆信息】页面(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>配套接口定义(apis/user.js 追加):
javascript
// apis/user.js
// 引入网络请求模块
import { uniFetch } from './uni-fetch'
export default {
// 省略中间部分代码...
/**
* 车辆信息
*/
truck() {
return uniFetch.get('/driver/users/truck')
},
}关键行与易错点
v-for的:key用picture.url(列表项的唯一值),不要用index。swiper需要indicator-dots才会显示轮播指示点,circular开启循环;图片用mode="aspectFill"保证不变形。- 变量名
truchInfo是讲义原样拼写(truck 少个 c),模板与脚本必须保持一致,改一处不改另一处就会渲染空白——这是本段最常见的坑。 - 该页面位于分包
subpkg_user,onLoad里发请求而不是onMounted,符合 uni-app 页面生命周期写法。 - 同样要判断
code !== 200并toast,不要直接把data赋给ref就完事。
常见错误
H5 调试时插件不生效、登录态对不上
- 现象:HBuilderX 启动 H5 后,浏览器没有常用插件,调试体验和平时不一致。
- 原因:HBuilderX 启动 H5 时新开了浏览器实例,不是日常浏览器的标签页,插件与登录态不在同一环境。
- 修法:启动后复制访问地址,粘贴到日常使用的浏览器中调试;不要直接在 HBuilderX 新开的浏览器里做插件相关排查。
安装依赖或导入
uni-fetch时报找不到模块- 现象:
import报错、npm install后仍无法使用,或路径提示异常。 - 原因:终端不在项目根目录执行安装,或导入路径写错、导出方式用错。
- 修法:先确认终端当前目录是项目根目录再安装;按项目约定从
@/apis/uni-fetch导入,并核对是默认导出还是具名导出。
- 现象:
请求地址拼接错误,接口发不出去或 404
- 现象:Network 中请求 URL 异常,可能重复基地址,或只写了域名没有业务路径。
- 原因:封装层已经配置基地址,业务调用时又写了完整 URL;或路径少写、多写斜杠。
- 修法:基地址只在网络请求初始化配置中维护,业务接口只写文档中的路径部分;用 Network 面板核对最终 URL。
Pinia store 定义了数据,页面却拿不到或不是响应式
- 现象:页面读取 store 数据为
undefined,或修改后视图不更新。 - 原因:store 内没有
return暴露数据,或暴露的不是ref创建的响应式数据,或页面解构方式不当。 - 修法:store 内用
ref定义状态并显式return { count };页面按 Pinia 用法消费,必要时用storeToRefs保持响应式。
- 现象:页面读取 store 数据为
uni.utils.toast调用时报undefined- 现象:轻提示不出现,控制台提示
uni.utils或toast不存在。 - 原因:工具函数未在入口挂载到全局,或导出/导入名称不一致,或调用处拼写错误。
- 修法:在应用入口完成一次全局挂载,确认
utils默认导出对象中包含toast,调用处统一使用uni.utils.toast(...)。
- 现象:轻提示不出现,控制台提示
未登录请求返回 401 被误判为网络封装失败
- 现象:任务列表等接口返回 401,误以为
uni-fetch配错。 - 原因:神领物流接口要求登录态,当前尚未登录或未携带 token。
- 修法:测试网络层时先只验证请求是否发出、URL 与 loading 是否正常;401 作为未登录预期结果记录,登录模块完成后再验证数据返回。
- 现象:任务列表等接口返回 401,误以为
练习
- 网络层最小验证:在任务页面调用一次任务列表接口,只写路径不写完整域名,打开 Network 检查最终 URL、请求是否发出、loading 是否出现;再故意把基地址改错一次,观察现象并恢复。
- 轻提示工具扩展:给
uni.utils.toast传入自定义标题和图标参数各调用一次,再分别不传标题、不传图标,验证默认值是否符合预期。 - Pinia 响应式验证:创建
stores/counter.js,用ref定义计数并在页面展示;页面提供按钮修改计数,确认视图同步更新,并检查 store 是否显式return。 - 我的模块接口联调:在
apis/user.js中补齐profile、task、truck三个方法,在“我的”页面onLoad中分别调用;要求每个请求独立处理code !== 200与异常,不让一个接口失败阻塞另外两个。 - 调试环境切换:用 HBuilderX 启动 H5 后,把地址复制到日常浏览器中,对比插件是否可用、Network 面板是否正常;记录一次“新开浏览器”和“日常浏览器”的差异。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 神领物流-项目启动 | 06:27 |
| 2 | 神领物流-网络请求-1 | 08:15 |
| 3 | 神领物流-网络请求-2 | 07:28 |
| 4 | 神领物流-轻提示 | 06:22 |
| 5 | 神领物流-Pinia初始化 | 11:11 |
| 6 | 关于uni.utils的答疑 | 03:40 |
| 7 | 神领物流-Pinia定义Store-1 | 06:50 |
小结
- 项目启动阶段先把至少一个端跑通,H5 调试优先用日常浏览器承接地址,能减少环境差异带来的无效排错。
- 网络层封装先验证“请求能发出、基地址正确、loading 生效”,再补拦截器和业务接口,排错顺序不要倒置。
- 401 在当前阶段是未登录的预期结果,不要把它当成
uni-fetch封装失败;登录/token 是后续模块的前置条件。 - Pinia 初始化完成后,store 必须显式暴露响应式状态;页面拿不到数据时先查
return和响应式,不要先怀疑页面模板。 - 工具函数集中到命名空间并全局挂载,可以减少重复导入;但挂载入口只做一次,调用名要保持一致。
- “我的”模块三个接口彼此独立,成功与失败都要分别处理;字段绑定以接口实际返回为准,不凭字段名猜含义。
- 每完成一层就做一次最小验证,避免后面接口全挂时无法判断问题在依赖、导入、基地址、登录态还是业务代码。