← 全部文章
uni-app网络请求Pinia状态管理面试

uni-app 系列(四):网络请求封装与 Pinia 状态管理

2026年8月4日 · 17 分钟

一、uni.request:跨端统一的网络 API

uni-app 用 uni.request 统一各端网络请求,底层在微信端映射 wx.request、H5 端用 XHR/fetch、App 端走原生网络。基本用法基于回调:

uni.request({
  url: "https://api.example.com/items",
  method: "GET",
  data: { page: 1 },
  header: { "Content-Type": "application/json" },
  success: (res) => { console.log(res.statusCode, res.data); },
  fail: (err) => { console.error(err); },
});

裸用回调在业务里会写得很啰嗦,所以实际项目一定要封装

二、封装一个生产级请求层

目标:Promise 化 + 自动注入 token + 统一响应解包 + 统一错误处理 + 401 跳登录。

// utils/request.js
const BASE_URL = "https://api.example.com";

export function request({ url, method = "GET", data, auth = true }) {
  const header = { "Content-Type": "application/json" };
  // 自动注入 token
  if (auth) {
    const token = uni.getStorageSync("token");
    if (token) header.Authorization = "Bearer " + token;
  }

  return new Promise((resolve, reject) => {
    uni.request({
      url: BASE_URL + url,
      method,
      data,
      header,
      success: (res) => {
        // 统一处理登录失效
        if (res.statusCode === 401) {
          uni.removeStorageSync("token");
          uni.navigateTo({ url: "/pages/login/login" });
          return reject(new Error("未登录"));
        }
        // 后端统一响应体 { code, message, data }
        const body = res.data;
        if (body.code === 0) {
          resolve(body.data);       // 只把业务数据抛给调用方
        } else {
          uni.showToast({ title: body.message, icon: "none" });
          reject(new Error(body.message));
        }
      },
      fail: (err) => {
        uni.showToast({ title: "网络异常", icon: "none" });
        reject(err);
      },
    });
  });
}

// 语义化封装
export const http = {
  get: (url, data, auth) => request({ url, method: "GET", data, auth }),
  post: (url, data, auth) => request({ url, method: "POST", data, auth }),
};

业务里就非常清爽:

import { http } from "@/utils/request";
const list = await http.get("/items", { page: 1 });

关于"拦截器"

uni-app 提供 uni.addInterceptor 可对 requestnavigateTo 等 API 做拦截。但实践中,自己封装 request 函数(如上)比用 addInterceptor 更灵活可控,是主流做法。要点齐活:请求前注入 token、响应后统一解包、失败统一提示、401 统一跳登录。

三、本地存储

uni.setStorageSync("token", "abc");        // 同步存
const t = uni.getStorageSync("token");      // 同步取
uni.removeStorageSync("token");             // 删
uni.clearStorageSync();                     // 清空
// 也有异步版:uni.setStorage / getStorage

注意各端容量限制:微信小程序单 key 上限 1MB、总上限 10MB。别拿它当数据库,只存 token、少量用户配置、缓存标记等。

四、状态管理:为什么用 Pinia

跨页面共享的数据(登录态、购物车、全局配置)应放状态管理库。uni-app(Vue3)官方推荐 Pinia,原因:

  • Vue3 官方推荐,取代 Vuex,是 Vue 生态的新标准;
  • API 更简洁:没有 mutations,直接改 state / 用 actions;
  • 完整 TypeScript 支持,类型推导好;
  • 模块化天然:每个 store 独立,无需嵌套 modules。

定义一个 store

// stores/user.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";

export const useUserStore = defineStore("user", () => {
  const token = ref("");
  const profile = ref(null);
  const isLogin = computed(() => !!token.value);

  function setSession(session) {
    token.value = session.token;
    profile.value = session.user;
    uni.setStorageSync("token", session.token);
  }
  function logout() {
    token.value = "";
    profile.value = null;
    uni.removeStorageSync("token");
  }
  return { token, profile, isLogin, setSession, logout };
});

在 main.js 注册

import { createSSRApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

export function createApp() {
  const app = createSSRApp(App);
  app.use(createPinia());
  return { app };
}

在页面/组件里用

import { useUserStore } from "@/stores/user";
const user = useUserStore();
if (user.isLogin) { /* ... */ }
user.setSession(session);

持久化:Pinia 的 state 存在内存里,App/小程序被杀进程后会丢。登录态这类要持久的数据,需要在 setSession 里同步写 uni.setStorageSync,并在应用启动(App.vueonLaunch)时从 storage 读回来 restore。也可用持久化插件,但小程序端需注意兼容。

五、本阶段必须掌握的知识点

  • uni.request 基本用法,以及为什么/如何封装成 Promise 化请求层。
  • 请求层五要素:token 注入、响应解包、统一错误提示、401 跳登录、BASE_URL 管理。
  • 本地存储 API 与容量限制,明确它只存轻量数据。
  • Pinia 优于 Vuex 的理由、store 的组合式写法、在 main.js 注册、页面中使用。
  • Pinia 状态不持久,需配合 storage 做登录态持久化与启动 restore。

六、高频面试题

Q1:如何封装 uni-app 的网络请求?要考虑哪些点?

A:核心是把回调式的 uni.request 包成返回 Promise 的函数,并集中处理横切关注点:① 统一 BASE_URL 与请求头;② 请求前自动注入 token(从 storage 取);③ 响应后按后端约定的 {code,message,data} 统一解包,只把业务 data 交给调用方;④ 统一错误提示uni.showToast)与 reject;⑤ 对 401 登录失效统一清 token 并跳登录页。再对外暴露 get/post 等语义化方法,业务层就能 await http.get(...)

追问:uni-app 有 addInterceptor,为什么很多人还是手写封装?

A:uni.addInterceptor 能拦截 request/路由等 API,但它的能力和可读性不如自己封装函数灵活——手写封装能精确控制解包逻辑、按接口区分是否鉴权、做重试/取消/并发控制等,且调试直观。实际项目里以自封装 request 为主,addInterceptor 更多用于给路由跳转加全局埋点/登录守卫这类场景。


Q2:uni-app 项目里状态管理选 Vuex 还是 Pinia?为什么?

A:Vue3 项目推荐 Pinia。原因:① 它是 Vue 官方新推荐、取代 Vuex;② API 更简洁,去掉了 mutations,可直接修改 state 或通过 actions;③ TypeScript 支持完善,类型推导友好;④ 天生模块化,每个 store 独立,无 Vuex 的 modules 嵌套心智负担。老项目若已用 Vuex 可继续,但新项目用 Pinia。

追问:Pinia 的数据在小程序里会持久保存吗?怎么做登录态持久化?

A:不会。Pinia 的 state 在内存中,小程序/App 进程被回收或用户杀掉后就丢失。登录态要持久化,需在写入 store 的同时用 uni.setStorageSync 落盘,并在 App.vueonLaunch 里从 storage 读回来恢复到 store(restore)。也可用 pinia 持久化插件,但要注意小程序端 storage API 的兼容适配。


Q3:uni.setStorageSyncuni.setStorage 区别?存储有什么限制?

A:...Sync同步版本,直接返回/阻塞,代码顺序执行,适合存 token 这类小数据;不带 Sync 的是异步版本,通过 success/fail 回调。限制:以微信小程序为例,单个 key 上限 1MB、单个用户总上限 10MB,且是键值对存储,不能当数据库用。存大量结构化数据应走后端或本地数据库方案。

追问:同步存储会不会阻塞影响性能?

A:小数据(token、配置)用同步版几乎无感,方便;但大数据频繁同步读写会阻塞 JS 线程,导致卡顿。大数据或高频场景应改用异步版,或减少读写次数(如内存缓存 + 批量落盘)。

相关水晶