侧边栏壁纸
  • 累计撰写 75 篇文章
  • 累计创建 22 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

H5 套壳 APK 登录状态丢失问题排查与修复实录

七月流火
2026-06-01 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

项目背景:Vue 3 + Vite 构建的商家端 H5 应用,通过 HBuilderX 5+ Runtime 打包为 Android APK。部分安卓设备上出现登录后无法保持状态、商品分类加载失败、点击首页立即退出登录等问题。


一、问题现象

问题

表现

无法保持登录

登录成功后,关闭应用再打开,自动跳转到登录页

无法查询商品/分类

登录后进入店铺首页,商品列表和分类标签为空

点击首页退出登录

点击顶部导航「首页」按钮后,直接跳转登录页

三个问题在部分安卓设备上 100% 复现,在其他设备上正常。


二、根因分析

2.1 技术架构

项目采用 H5 套壳 APK 方案:

┌─────────────────────────────┐
│  Android APK (HBuilderX)    │
│  ┌───────────────────────┐  │
│  │  WebView (5+ Runtime) │  │
│  │  ┌─────────────────┐  │  │
│  │  │  Vue 3 H5 App   │  │  │
│  │  │  (file:// 协议)  │  │  │
│  │  └─────────────────┘  │  │
│  └───────────────────────┘  │
└─────────────────────────────┘

H5 页面通过 file:// 协议加载在 WebView 中运行。

2.2 核心问题:localStoragefile:// 协议下不可靠

这是三个问题的共同根因

localStorage 的持久化行为依赖于浏览器的同源策略。在 http://https:// 协议下,localStorage 按 origin(协议+域名+端口)隔离并持久化。但在 file:// 协议下:

问题

说明

origin 不确定

file:// 协议没有明确的 origin,不同浏览器/WebView 实现不一致

持久化不保证

部分 Android 版本/厂商 ROM 在应用关闭后清除 file:// 下的 localStorage

存储隔离问题

多个 file:// 页面可能共享同一个 localStorage,也可能不共享

2.3 问题触发链路

APK 重启/切后台恢复
    ↓
WebView 重新加载 file:// 页面
    ↓
localStorage 被清除(部分设备)
    ↓
token / businessShopId / tokenExpireTime 丢失
    ↓
┌──────────────────────────────────────────┐
│ 路由守卫检测无 token → 跳转登录页         │
│ API 请求无 shopId → 返回空数据            │
│ topNav 检测无 shopId → 跳转登录页         │
│ visibilitychange 检测无 token → 跳转登录页 │
└──────────────────────────────────────────┘

2.4 次要问题

在排查过程中还发现了以下问题:

  1. clearWebViewCache() 未保留 tokenExpireTime — APK 更新清理缓存时,保留了 token 和 shopId,但遗漏了 tokenExpireTime,导致更新后 token 虽在但被判定为过期

  2. visibilitychange 处理过于激进 — 每次应用恢复前台都检查 token,在 APK 环境下 localStorage 读取可能延迟,容易误触发退出登录

  3. pinia persist 使用原生 localStorage — store 持久化也受 file:// 协议影响


三、修复方案

3.1 核心方案:统一存储工具(plus.storage + localStorage 双写)

HBuilderX 5+ Runtime 提供了 plus.storage API,它基于 Android 原生 SharedPreferences 实现,不受 file:// 协议限制,数据可靠持久化。

设计统一存储工具 appStorage

┌─────────────────────────────────────┐
│           appStorage API            │
│  getItem / setItem / removeItem     │
├──────────────┬──────────────────────┤
│  APK 环境    │  浏览器环境           │
│  plus.storage│  localStorage        │
│  +           │                      │
│  localStorage│                      │
│  (双写)      │  (单写)              │
└──────────────┴──────────────────────┘

双写策略的原因:

  • plus.storage 保证 APK 环境下的持久化可靠性

  • localStorage 保证浏览器环境下的兼容性

  • 读取时 APK 环境优先从 plus.storage 读取(更可靠)

  • 写入时同时写两处,确保数据一致性

3.2 实现代码

// src/util/storage.js

function isAppEnvironment () {
  return typeof window.plus !== 'undefined' && window.plus.storage
}

function getItem (key) {
  if (isAppEnvironment()) {
    const val = window.plus.storage.getItem(key)
    return val
  }
  return localStorage.getItem(key)
}

function setItem (key, value) {
  if (isAppEnvironment()) {
    window.plus.storage.setItem(key, value)
  }
  localStorage.setItem(key, value) // 双写
}

function removeItem (key) {
  if (isAppEnvironment()) {
    window.plus.storage.removeItem(key)
  }
  localStorage.removeItem(key)
}

3.3 数据迁移

旧版 APK 使用 localStorage 存储了 token 等数据,升级后需要迁移到 plus.storage:

function migrateFromLocalStorage () {
  if (!isAppEnvironment()) return
  const keys = ['token', 'tokenExpireTime', 'businessShopId', 'tokenLastActive']
  keys.forEach(key => {
    const plusVal = window.plus.storage.getItem(key)
    if (plusVal) return // plus.storage 已有数据,跳过
    const lsVal = localStorage.getItem(key)
    if (lsVal) {
      window.plus.storage.setItem(key, lsVal)
    }
  })
}

App.vueonMounted 中,plusready 事件触发后调用迁移:

if (isAppEnvironment()) {
  appStorage.migrateFromLocalStorage()
  startAutoUpdate()
} else {
  document.addEventListener('plusready', () => {
    appStorage.migrateFromLocalStorage()
    startAutoUpdate()
  })
}

3.4 pinia persist 适配

pinia-plugin-persist 的 storage 字段需要实现 getItem/setItem/removeItem 接口。appStorage 已具备这些方法,可以直接替换:

// 修改前
persist: {
  enabled: true,
  strategies: [{
    storage: localStorage,
    paths: ['userInfo', 'headImage', 'areaCode', 'newAddress']
  }]
}

// 修改后
persist: {
  enabled: true,
  strategies: [{
    storage: appStorage,
    paths: ['userInfo', 'headImage', 'areaCode', 'newAddress']
  }]
}

3.5 修复 clearWebViewCache 遗漏

APK 更新时清理缓存,需要保留所有关键数据:

// 修改前:遗漏了 tokenExpireTime
const preserveKeys = ['token', 'businessShopId', 'tokenLastActive']

// 修改后
const preserveKeys = ['token', 'businessShopId', 'tokenLastActive', 'tokenExpireTime']


四、经验总结

4.1 H5 套壳 APK 的存储选型

存储方式

file:// 下可靠性

推荐场景

localStorage

❌ 不保证

仅浏览器环境

plus.storage

✅ 可靠

APK 环境(5+ Runtime)

plus.storage + localStorage 双写

✅ 最可靠

跨环境兼容

4.2 排查思路

  1. 先确认问题范围 — 是所有设备还是特定设备?特定设备问题优先考虑 WebView 兼容性

  2. 检查存储可靠性 — 在 APK 环境中打印 localStorage 和 plus.storage 的值,对比是否一致

  3. 检查页面协议file:// 协议下很多 Web API 行为与 http:// 不同

  4. 检查缓存清理逻辑 — 确认缓存清理时是否遗漏了关键数据

4.3 预防措施

  • 统一存储入口 — 不要在业务代码中直接使用 localStorage,通过统一工具封装

  • 双写策略 — APK 环境同时写入 plus.storagelocalStorage,读取优先 plus.storage

  • 数据迁移 — 版本升级时提供从旧存储到新存储的迁移逻辑

  • 缓存清理白名单 — 清理 WebView 缓存时,明确列出需要保留的 key


五、验证步骤

  1. 登录后关闭应用再打开 → 确认登录状态保持

  2. 登录后查看商品和分类 → 确认数据正常加载

  3. 点击首页导航 → 确认不会退出登录

  4. 切换应用到后台再恢复 → 确认不会退出登录

  5. APK 更新后 → 确认 token 和 shopId 不丢失

0

评论区