项目背景: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 核心问题:localStorage 在 file:// 协议下不可靠
这是三个问题的共同根因。
localStorage 的持久化行为依赖于浏览器的同源策略。在 http:// 或 https:// 协议下,localStorage 按 origin(协议+域名+端口)隔离并持久化。但在 file:// 协议下:
2.3 问题触发链路
APK 重启/切后台恢复
↓
WebView 重新加载 file:// 页面
↓
localStorage 被清除(部分设备)
↓
token / businessShopId / tokenExpireTime 丢失
↓
┌──────────────────────────────────────────┐
│ 路由守卫检测无 token → 跳转登录页 │
│ API 请求无 shopId → 返回空数据 │
│ topNav 检测无 shopId → 跳转登录页 │
│ visibilitychange 检测无 token → 跳转登录页 │
└──────────────────────────────────────────┘2.4 次要问题
在排查过程中还发现了以下问题:
clearWebViewCache()未保留tokenExpireTime— APK 更新清理缓存时,保留了 token 和 shopId,但遗漏了 tokenExpireTime,导致更新后 token 虽在但被判定为过期visibilitychange处理过于激进 — 每次应用恢复前台都检查 token,在 APK 环境下 localStorage 读取可能延迟,容易误触发退出登录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.vue 的 onMounted 中,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 的存储选型
4.2 排查思路
先确认问题范围 — 是所有设备还是特定设备?特定设备问题优先考虑 WebView 兼容性
检查存储可靠性 — 在 APK 环境中打印 localStorage 和 plus.storage 的值,对比是否一致
检查页面协议 —
file://协议下很多 Web API 行为与http://不同检查缓存清理逻辑 — 确认缓存清理时是否遗漏了关键数据
4.3 预防措施
统一存储入口 — 不要在业务代码中直接使用
localStorage,通过统一工具封装双写策略 — APK 环境同时写入
plus.storage和localStorage,读取优先plus.storage数据迁移 — 版本升级时提供从旧存储到新存储的迁移逻辑
缓存清理白名单 — 清理 WebView 缓存时,明确列出需要保留的 key
五、验证步骤
登录后关闭应用再打开 → 确认登录状态保持
登录后查看商品和分类 → 确认数据正常加载
点击首页导航 → 确认不会退出登录
切换应用到后台再恢复 → 确认不会退出登录
APK 更新后 → 确认 token 和 shopId 不丢失
评论区