跳转至正文

Capacitor 中的键值存储:Preferences 与 SQLite 的对比

对比 Capacitor 中的键值存储:Preferences、SQLite 键值存储与 Keychain 存储。了解哪种方案更适合存放设置、缓存和机密信息。

Capgo 中键值存储方案对比的 Capacitor 标志

在 Capacitor 中进行键值存储,请使用 @capacitor/preferences 少量小型设置用 Preferences,更大或更多的数据用基于 SQLite 的键值存储,机密信息用 Keychain 或 Keystore。本指南对比了 Capacitor 8 中可用的选项,展示了每种方案的可用 code,并解释了如何在切换时不丢失用户数据。

大多数应用会同时使用其中两种:用 Preferences 或 SQLite 存储应用状态,用安全存储保存认证 token。

选项概览

localStorage / IndexedDB @capacitor/preferences 快速 SQL KeyValueStore @capgo/capacitor-data-storage-sqlite @capgo/capacitor-native-biometric setData
基于 WebView 存储 UserDefaults / SharedPreferences SQLite 文件 SQLite 文件 Keychain / Keystore
可抵御系统存储清理 无保证 是 是 是 是
值类型 字符串 (localStorage) 字符串 JSON 值、数字、布尔值、 Uint8Array 字符串 字符串
合适的尺寸范围 小 小,每个值几 KB 大值,多键 多键 非常小(Android 上约 8 KB 以内)
加密 否 否 可选 SQLCipher,密钥来自安全存储 可选 SQLCipher,密码在配置中 是,硬件支持
命名空间 按来源 group 选项 store 选项 数据表 键名
按前缀查询 否 否 列出键 filtervalues 否
Web 端 是 localStorage SQLite Wasm(OPFS) IndexedDB 不适合存储敏感信息

为什么不用 localStorage

localStorage 和 IndexedDB 在 Capacitor WebView 中运行,但数据归属于 WebView,而非应用的本地存储。当设备空间不足时,操作系统可能会回收 WebView 的存储空间,且 Capacitor 的官方文档也不建议将其用于存储不可丢失的数据。更改应用的 capacitor.config 也会改变 origin,导致 WebView 指向另一个空的存储空间。

请将它们视为可随时清除的缓存。其他所有数据请使用基于本地存储的选项。

方案 1:用 Capacitor Preferences 存少量设置

@capacitor/preferences 是用于轻量级键值数据的官方插件。在 iOS 上,它写入 UserDefaults,在 Android 上写入 SharedPreferences,在 Web 上写入 localStorage.

bun add @capacitor/preferences
bunx cap sync
import { Preferences } from '@capacitor/preferences';

interface Settings {
  theme: 'light' | 'dark' | 'system';
  fontScale: number;
}

const DEFAULTS: Settings = { theme: 'system', fontScale: 1 };

export async function loadSettings(): Promise<Settings> {
  const { value } = await Preferences.get({ key: 'settings' });
  return value ? { ...DEFAULTS, ...JSON.parse(value) } : DEFAULTS;
}

export async function saveSettings(settings: Settings): Promise<void> {
  await Preferences.set({ key: 'settings', value: JSON.stringify(settings) });
}

适用场景:主题、语言、onboarding 标记、最后选中的标签页、功能开关。

请勿用于:

  • 机密信息。 值以明文形式存储在应用沙箱中,并包含在设备备份里。
  • 大体积数据。 UserDefaults 以及 SharedPreferences 会将整个文件加载到内存中。大型 JSON 数据块会拖慢每次读写操作。
  • 需要按前缀列出或清除大量键。 keys() 会返回全部内容,因此你需要在 JavaScript 中过滤。

Preferences.configure({ group: 'MyApp' }) 更改键值存储的命名空间。默认分组为 CapacitorStorage.

方案 2:使用 SQLite 键值存储

一旦你开始缓存 API 响应、草稿、离线队列或数百条记录,SQLite 文件会是更好的选择。它能处理大值,写入是原子的,并且可以加密。

高性能 SQL KeyValueStore

@capgo/capacitor-fast-sql 包含一个 KeyValueStore 基于其 SQLite 连接的类。它会创建一个 __kv_store 数据表,保留值类型,并支持在一个数据库中使用多个命名 store。

bun add @capgo/capacitor-fast-sql
bunx cap sync

Fast SQL 需要两个原生设置(iOS 的本地网络配置 Info.plist以及 Android 上的 localhost 明文规则)。它们都包含在 如何在 Capacitor 应用中使用 SQLite.

import { KeyValueStore } from '@capgo/capacitor-fast-sql';

const cache = await KeyValueStore.open({ database: 'app', store: 'api-cache' });

// Objects, arrays, numbers, booleans, null and Uint8Array are stored as-is
await cache.set('user:42', { id: 42, name: 'Ada', roles: ['admin'] });
await cache.set('lastSync', Date.now());

const user = (await cache.get('user:42')) as { id: number; name: string } | null;
const exists = await cache.has('lastSync');
const allKeys = await cache.keys();

await cache.remove('user:42');
await cache.clear(); // only clears the 'api-cache' store
await cache.close();

API 摘要: open(options), fromConnection(connection, store), set, get (在缺失时返回 null ), has, remove, clear, keys, close.

两种值得了解的模式:

多个 store,一个数据库。 本指南中介绍的 store 该选项会为键划分命名空间,因此 settings, drafts 以及 api-cache 可以并存,并独立清除。

键值对与真实表并存。 如果你的应用已经使用 Fast SQL 处理关系型数据,请复用现有连接,而不是再开一个:

import { FastSQL, KeyValueStore } from '@capgo/capacitor-fast-sql';

const db = await FastSQL.connect({ database: 'app' });
const drafts = await KeyValueStore.fromConnection(db, 'drafts');
// You own `db`: drafts.close() does not disconnect it

之后,当某个键值对数据膨胀到需要查询时(例如所有超过一周的草稿),你可以将其迁移到同一数据库中的正式表里。

加密的键值存储

KeyValueStore.open 接受与 FastSQL.connect,包括 encrypted 以及 encryptionKey 在 iOS 和 Android 上:

import { KeyValueStore } from '@capgo/capacitor-fast-sql';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';

async function getKey(): Promise<string> {
  const { isSaved } = await NativeBiometric.isDataSaved({ key: 'kv.key' });
  if (isSaved) return (await NativeBiometric.getData({ key: 'kv.key' })).value;
  const bytes = crypto.getRandomValues(new Uint8Array(32));
  const key = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
  await NativeBiometric.setData({ key: 'kv.key', value: key });
  return key;
}

const secureStore = await KeyValueStore.open({
  database: 'secure-kv',
  encrypted: true,
  encryptionKey: await getKey(),
});

这需要在原生构建中使用 SQLCipher。配置方法以及备份恢复后密钥丢失的处理方式,详见 如何在 Capacitor 中加密 SQLite 数据库.

@capgo/capacitor-data-storage-sqlite

@capgo/capacitor-data-storage-sqlite 是一个基于 SQLite 的专用键值插件(最初由 Jean Pierre Quéau 开发,现由 Capgo 维护)。值为字符串,并增加了多项功能:每个存储支持多张表、前缀和后缀过滤,以及 JSON 导入和导出。

bun add @capgo/capacitor-data-storage-sqlite
bunx cap sync
import { CapgoCapacitorDataStorageSqlite as Store } from '@capgo/capacitor-data-storage-sqlite';

await Store.openStore({ database: 'appStore', table: 'settings' });

await Store.set({ key: 'locale', value: 'fr' });
await Store.set({ key: 'profile', value: JSON.stringify({ name: 'Ada' }) });

const { value } = await Store.get({ key: 'locale' });
const { result: hasProfile } = await Store.iskey({ key: 'profile' });
const { keys } = await Store.keys();

// Switch table inside the same store
await Store.setTable({ table: 'drafts' });
const { values } = await Store.filtervalues({ filter: 'note%' }); // values whose key starts with "note"

await Store.remove({ key: 'note-1' });
await Store.clear(); // clears the current table

filtervalues 将过滤器作为 SQL LIKE 模式应用于键(note% 前缀用 %note ,后缀用,纯文本用于包含匹配)。模式会直接插入 SQL 字符串中,因此请仅传入你控制的过滤器,切勿传入原始用户输入。

在 Web 端,它通过 localforage,需单独安装:

bun add localforage

关于其加密机制: openStore({ encrypted: true, mode: 'secret' }) 使用 SQLCipher,并采用来自 capacitor.config 在……下 plugins.CapgoCapacitorDataStorageSqlite.encryptionSecret的密码。该密码打包在您的应用 bundle 中,若未设置,插件将回退到内置默认值。请将其视为防止随意文件检查的手段,而非对抗解包应用者的防护。对于真正的密钥,请使用 Keychain 或 Keystore 中的密钥,如上文 Fast SQL 示例所示。

选择高性能 SQL KeyValueStore 还是 data-storage-sqlite?

  • 选择 快速 SQL KeyValueStore 如果你想要无需手动处理 JSON 的类型化值、二进制值、运行时加密密钥,或者已经在表操作中使用 Fast SQL。
  • 选择 Data Storage SQLite 如果你需要它的表和过滤功能、JSON 导出和导入,或者 Electron 支持。

方案 3:使用 Keychain 和 Keystore 存储敏感信息

Auth token、refresh token、API 密钥和数据库加密密钥应存放在平台的安全存储中。 @capgo/capacitor-native-biometric (8.6.0 及更高版本)在 iOS Keychain 和 Android Keystore 之上提供了通用的键值 API:

bun add @capgo/capacitor-native-biometric
bunx cap sync
import { AccessControl, NativeBiometric } from '@capgo/capacitor-native-biometric';

// Silent read and write
await NativeBiometric.setData({ key: 'session.refreshToken', value: token });
const { value: refreshToken } = await NativeBiometric.getData({ key: 'session.refreshToken' });

// Require Face ID / Touch ID / fingerprint to read
await NativeBiometric.setData({
  key: 'wallet.pin',
  value: pin,
  accessControl: AccessControl.BIOMETRY_ANY,
});
const { value: secretPin } = await NativeBiometric.getSecureData({
  key: 'wallet.pin',
  reason: 'Confirm it is you',
});

await NativeBiometric.deleteData({ key: 'session.refreshToken' });

让每个值保持较小。插件文档指出,基于 Android Keystore 的加密在每个值约 8 KB 以下时效果最佳。如果你需要保护大量数据,请在这里存一个随机密钥,用它来加密 SQLite 数据库。我们的这篇文章: 离线 token 的安全存储 深入探讨 Token 处理。

请注意,iOS 上的 Keychain 条目在应用卸载后仍可能保留。如果应用需要干净初始状态,请在全新安装后的首次启动时清除残留密钥(在 Preferences 中存一个“首次运行完成”标志,该标志会在卸载时被移除;若该标志不存在,就清除 Keychain 条目)。

选择:决策清单

  1. 它是敏感信息吗? Token、密码、PIN、密钥。请使用 Keychain 或 Keystore 存储。
  2. 只是几个小设置吗? 请使用 Preferences。
  3. 是大量条目、大型 JSON、二进制数据,还是需要批量过期和清除的缓存? 请使用 SQLite key-value 存储。
  4. 你需要对其进行过滤、排序或关联查询吗? 它已不再是 key-value 数据。请创建一张表,参见 SQLite 指南,或使用类型化层,例如 Kysely.

在不同存储方案间迁移现有数据

从 localStorage 从 Preferences 迁移到 SQLite 存储只需在启动时执行一次复制。请在应用其他部分读取数据之前完成,并记录该操作已执行。

import { Preferences } from '@capacitor/preferences';
import { KeyValueStore, type KeyValueValue } from '@capgo/capacitor-fast-sql';

export async function migrateToSqliteStore(): Promise<KeyValueStore> {
  const kv = await KeyValueStore.open({ database: 'app', store: 'settings' });
  if (await kv.has('__migrated_v1')) return kv;

  const { keys } = await Preferences.keys();
  for (const key of keys) {
    const { value } = await Preferences.get({ key });
    if (value === null) continue;
    let parsed: KeyValueValue = value;
    try {
      parsed = JSON.parse(value) as KeyValueValue;
    } catch {
      // keep plain strings as strings
    }
    await kv.set(key, parsed);
  }

  // Also copy from localStorage if older versions used it
  for (let i = 0; i < localStorage.length; i++) {
    const key = localStorage.key(i);
    if (key && !(await kv.has(key))) {
      await kv.set(key, localStorage.getItem(key));
    }
  }

  await kv.set('__migrated_v1', true);
  // Remove the old copies only after a release or two, once you are confident
  return kv;
}

将密钥复制到安全存储后,立即从旧存储中删除。如果将 token 移至 Keychain 后仍保留在 Preferences 中,原有的暴露风险依然存在。

由于此 code 在 JavaScript 中运行,您可以将其与 Capgo live update。SQLite 插件本身是原生的,因此添加它的版本需要商店构建。先发布插件,然后执行迁移 code。

故障排除

Preferences 的值是 null 在 Web 更新后。 源已更改(端口、主机名或协议),因此 localStorage 是一个不同的、空的存储。

快速 SQL KeyValueStore.open 在 iOS 上抛出异常。 本地 HTTP 通道被阻止。请添加 NSAllowsLocalNetworking 改为 Info.plist 并重新构建原生应用。

get 在 data-storage-sqlite 中,你预期得到对象,却返回了字符串。 该插件只存储字符串。 JSON.parse 该值。

在新手机上恢复备份后,Secret 丢失。 Keystore 密钥和仅限设备的 Keychain 项不会迁移到新硬件。请将安全存储视为凭据缓存,重新登录即可重新获取。

在 iOS 上重装后设置丢失,但 token 还在。 预期:Preferences 会随应用一起删除,Keychain 项可能保留。请使用上面的首次运行标志模式。

总结

  • Preferences:轻量级、非敏感的设置。
  • SQLite 键值存储 (Fast SQL KeyValueStore 或 @capgo/capacitor-data-storage-sqlite): 大量或大型值、缓存、草稿,可选加密。
  • Keychain 或 Keystore(@capgo/capacitor-native-biometric setData):用于存储密钥和加密密钥。

插件参考: Fast SQL 文档, data-storage-sqlite 文档,以及 原生生物识别插件页面.

为 Capacitor 应用提供 Live Updates

当 Web 层出现线上 Bug 时,通过 Capgo 发布修复,而不用等 App Store 审核好几天。用户会在后台收到更新,原生变更则仍走正常审核流程。

博客最新动态

Capgo 为你提供打造真正专业的移动应用所需的最有价值的洞察。