Skip to content

组件库、生命周期与 pinia(二) ​

本页对应 组件库、生命周期与 pinia,共 7 段视频,约 50 分钟。

学习目标 ​

  1. 能分别启动神领物流项目的 H5、小程序和 App 端,并说明 HBuilderX 启动 H5 时新开浏览器的现象,能将地址复制到常用浏览器中进行调试。
  2. 能安装网络请求模块,在 api 目录创建统一请求实例文件,完成 baseURL、loading、请求/响应拦截器等初始化配置。
  3. 能在页面中导入封装好的请求实例并发送请求,通过浏览器 Network 面板验证请求地址、loading 效果和 baseURL 配置是否生效。
  4. 能创建工具函数目录并封装轻提示方法,支持传入标题和图标参数,并将其挂载到 uni 全局对象上直接调用。
  5. 能完成 Pinia 的安装、导入、createPinia 创建实例并通过 app.use 与 Vue 应用整合。
  6. 能使用 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({}) 初始化为空对象,模板里 &#123;&#123; taskInfo.taskAmounts &#125;&#125; 首次渲染为空,不要用 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 就完事。

常见错误 ​

  1. H5 调试时插件不生效、登录态对不上

    • 现象:HBuilderX 启动 H5 后,浏览器没有常用插件,调试体验和平时不一致。
    • 原因:HBuilderX 启动 H5 时新开了浏览器实例,不是日常浏览器的标签页,插件与登录态不在同一环境。
    • 修法:启动后复制访问地址,粘贴到日常使用的浏览器中调试;不要直接在 HBuilderX 新开的浏览器里做插件相关排查。
  2. 安装依赖或导入 uni-fetch 时报找不到模块

    • 现象:import 报错、npm install 后仍无法使用,或路径提示异常。
    • 原因:终端不在项目根目录执行安装,或导入路径写错、导出方式用错。
    • 修法:先确认终端当前目录是项目根目录再安装;按项目约定从 @/apis/uni-fetch 导入,并核对是默认导出还是具名导出。
  3. 请求地址拼接错误,接口发不出去或 404

    • 现象:Network 中请求 URL 异常,可能重复基地址,或只写了域名没有业务路径。
    • 原因:封装层已经配置基地址,业务调用时又写了完整 URL;或路径少写、多写斜杠。
    • 修法:基地址只在网络请求初始化配置中维护,业务接口只写文档中的路径部分;用 Network 面板核对最终 URL。
  4. Pinia store 定义了数据,页面却拿不到或不是响应式

    • 现象:页面读取 store 数据为 undefined,或修改后视图不更新。
    • 原因:store 内没有 return 暴露数据,或暴露的不是 ref 创建的响应式数据,或页面解构方式不当。
    • 修法:store 内用 ref 定义状态并显式 return { count };页面按 Pinia 用法消费,必要时用 storeToRefs 保持响应式。
  5. uni.utils.toast 调用时报 undefined

    • 现象:轻提示不出现,控制台提示 uni.utils 或 toast 不存在。
    • 原因:工具函数未在入口挂载到全局,或导出/导入名称不一致,或调用处拼写错误。
    • 修法:在应用入口完成一次全局挂载,确认 utils 默认导出对象中包含 toast,调用处统一使用 uni.utils.toast(...)。
  6. 未登录请求返回 401 被误判为网络封装失败

    • 现象:任务列表等接口返回 401,误以为 uni-fetch 配错。
    • 原因:神领物流接口要求登录态,当前尚未登录或未携带 token。
    • 修法:测试网络层时先只验证请求是否发出、URL 与 loading 是否正常;401 作为未登录预期结果记录,登录模块完成后再验证数据返回。

练习 ​

  1. 网络层最小验证:在任务页面调用一次任务列表接口,只写路径不写完整域名,打开 Network 检查最终 URL、请求是否发出、loading 是否出现;再故意把基地址改错一次,观察现象并恢复。
  2. 轻提示工具扩展:给 uni.utils.toast 传入自定义标题和图标参数各调用一次,再分别不传标题、不传图标,验证默认值是否符合预期。
  3. Pinia 响应式验证:创建 stores/counter.js,用 ref 定义计数并在页面展示;页面提供按钮修改计数,确认视图同步更新,并检查 store 是否显式 return。
  4. 我的模块接口联调:在 apis/user.js 中补齐 profile、task、truck 三个方法,在“我的”页面 onLoad 中分别调用;要求每个请求独立处理 code !== 200 与异常,不让一个接口失败阻塞另外两个。
  5. 调试环境切换:用 HBuilderX 启动 H5 后,把地址复制到日常浏览器中,对比插件是否可用、Network 面板是否正常;记录一次“新开浏览器”和“日常浏览器”的差异。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1神领物流-项目启动06:27
2神领物流-网络请求-108:15
3神领物流-网络请求-207:28
4神领物流-轻提示06:22
5神领物流-Pinia初始化11:11
6关于uni.utils的答疑03:40
7神领物流-Pinia定义Store-106:50

小结 ​

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