WXT浏览器插件开发中文教程(18)----存储详解

前言

大家好,我是倔强青铜三 。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

存储

Chrome 文档Firefox 文档

您可以使用原生 API(请参阅上面的文档),使用 WXT 的内置存储 API,或从 NPM 安装一个包。

替代方案

  1. wxt/utils/storage(推荐):WXT 自带了一个围绕原生存储 API 的封装,简化了常见的使用场景。

  2. DIY:如果您正在迁移到 WXT 并且已经有了一个存储封装,可以继续使用它。将来,如果您想删除该代码,可以使用这些替代方案之一,但在迁移过程中没有理由替换正在工作的代码。

  3. 其他任何 NPM 包:有很多围绕存储 API 的封装,您可以找到喜欢的一个。这里是一些流行的:

    • webext-storage - 一个更可用的类型化存储 API,适用于 Web 扩展
    • @webext-core/storage - 一个类型安全的、类似 localStorage 的封装,围绕 Web 扩展存储 API

WXT 存储 [​](#WXT 存储 "#wxt-storage")

变更日志

扩展存储 API 的简化封装。

安装

使用 WXT [​](#使用 WXT "#with-wxt")

此模块已内置在 WXT 中,因此您不需要安装任何东西。

ts 复制代码
import { storage } from '#imports';

如果您使用自动导入,storage 会自动为您导入,因此您甚至不需要导入它!

不使用 WXT [​](#不使用 WXT "#without-wxt")

安装 NPM 包:

sh 复制代码
npm i @wxt-dev/storage
pnpm add @wxt-dev/storage
yarn add @wxt-dev/storage
bun add @wxt-dev/storage
ts 复制代码
import { storage } from '@wxt-dev/storage';

存储权限

要使用 @wxt-dev/storage API,必须在清单中添加 "storage" 权限:

wxt.config.ts

ts 复制代码
export default defineConfig({
  manifest: {
    permissions: ['storage'],
  },
});

基本用法

所有存储键必须以其存储区域为前缀。

ts 复制代码
// ❌ 这将抛出错误
await storage.getItem('installDate');
// ✅ 这是正确的
await storage.getItem('local:installDate');

您可以使用 local:session:sync:managed:

如果您使用 TypeScript,可以在大多数方法中添加类型参数以指定键值的预期类型:

ts 复制代码
await storage.getItem<number>('local:installDate');
await storage.watch<number>(
  'local:installDate',
  (newInstallDate, oldInstallDate) => {
    // ...
  },
);
await storage.getMeta<{ v: number }>('local:installDate');

有关可用方法的完整列表,请参见 API 参考

监听器

要监听存储更改,请使用 storage.watch 函数。它允许您为单个键设置监听器:

ts 复制代码
const unwatch = storage.watch<number>('local:counter', (newCount, oldCount) => {
  console.log('计数已更改:', { newCount, oldCount });
});

要移除监听器,请调用返回的 unwatch 函数:

ts 复制代码
const unwatch = storage.watch(...);
// 一段时间后...
unwatch();

元数据

@wxt-dev/storage 还支持为键设置元数据,存储在 key + "$" 处。元数据是与键关联的一组属性。它可能是一个版本号、最后修改日期等。

除了版本控制之外,您还需要负责管理字段的元数据:

ts 复制代码
await Promise.all([
  storage.setItem('local:preference', true),
  storage.setMeta('local:preference', { lastModified: Date.now() }),
]);

当从多次调用中设置不同属性的元数据时,这些属性会被合并而不是被覆盖:

ts 复制代码
await storage.setMeta('local:preference', { lastModified: Date.now() });
await storage.setMeta('local:preference', { v: 2 });
await storage.getMeta('local:preference'); // { v: 2, lastModified: 1703690746007 }

您可以移除与键关联的所有元数据,或仅移除特定属性:

ts 复制代码
// 移除所有属性
await storage.removeMeta('local:preference');
// 仅移除 "lastModified" 属性
await storage.removeMeta('local:preference', 'lastModified');
// 移除多个属性
await storage.removeMeta('local:preference', ['lastModified', 'v']);

定义存储项

一遍又一遍地为同一个键编写键和类型参数可能会很烦人。作为替代方案,您可以使用 storage.defineItem 创建一个"存储项"。

存储项包含与 storage 变量相同的 API,但您可以在一个地方配置其类型、默认值等:

ts 复制代码
// utils/storage.ts
const showChangelogOnUpdate = storage.defineItem<boolean>(
  'local:showChangelogOnUpdate',
  {
    fallback: true,
  },
);

现在,您可以使用创建的存储项上的辅助函数,而不是使用 storage 变量:

ts 复制代码
await showChangelogOnUpdate.getValue();
await showChangelogOnUpdate.setValue(false);
await showChangelogOnUpdate.removeValue();
const unwatch = showChangelogOnUpdate.watch((newValue) => {
  // ...
});

有关可用属性和方法的完整列表,请参见 API 参考

版本控制

如果期望存储项随着时间增长或变化,您可以为其添加版本控制。在定义项目的第一个版本时,从版本 1 开始。

例如,考虑一个存储项,它存储被扩展忽略的网站列表。

v1

ts 复制代码
type IgnoredWebsiteV1 = string;
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>(
  'local:ignoredWebsites',
  {
    fallback: [],
    version: 1,
  },
);

v2

ts 复制代码
import { nanoid } from 'nanoid'; 
type IgnoredWebsiteV1 = string;
interface IgnoredWebsiteV2 { 
  id: string; 
  website: string; 
} 
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV1[]>( 
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( 
  'local:ignoredWebsites',
  {
    fallback: [],
    version: 1, 
    version: 2, 
    migrations: { 
      // 从 v1 迁移到 v2 时运行
      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { 
        return websites.map((website) => ({ id: nanoid(), website })); 
      }, 
    }, 
  },
);

v3

ts 复制代码
import { nanoid } from 'nanoid';
type IgnoredWebsiteV1 = string;
interface IgnoredWebsiteV2 {
  id: string;
  website: string;
}
interface IgnoredWebsiteV3 { 
  id: string; 
  website: string; 
  enabled: boolean; 
} 
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( 
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV3[]>( 
  'local:ignoredWebsites',
  {
    fallback: [],
    version: 2, 
    version: 3, 
    migrations: {
      // 从 v1 迁移到 v2 时运行
      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => {
        return websites.map((website) => ({ id: nanoid(), website }));
      },
      // 从 v2 迁移到 v3 时运行
      3: (websites: IgnoredWebsiteV2[]): IgnoredWebsiteV3[] => { 
        return websites.map((website) => ({ ...website, enabled: true })); 
      }, 
    },
  },
);

INFO

内部使用一个名为 v 的元数据属性来跟踪值的当前版本。

在这种情况下,我们认为忽略的网站列表将来可能会发生变化,并能够从一开始就设置一个版本化的存储项。

实际上,直到需要更改其模式时,您才会知道某个项需要版本控制。幸运的是,向未版本化的存储项添加版本控制非常简单。

当找不到先前版本时,WXT 假定版本为 1。这意味着您只需设置 version: 2 并为 2 添加迁移,它就会正常工作!

让我们再看一下之前的忽略网站示例,但这次从一个未版本化的项开始:

未版本化

ts 复制代码
export const ignoredWebsites = storage.defineItem<string[]>(
  'local:ignoredWebsites',
  {
    fallback: [],
  },
);

v2

ts 复制代码
import { nanoid } from 'nanoid'; 
// 回溯性地为第一个版本添加类型
type IgnoredWebsiteV1 = string; 
interface IgnoredWebsiteV2 { 
  id: string; 
  website: string; 
} 
export const ignoredWebsites = storage.defineItem<string[]>( 
export const ignoredWebsites = storage.defineItem<IgnoredWebsiteV2[]>( 
  'local:ignoredWebsites',
  {
    fallback: [],
    version: 2, 
    migrations: { 
      // 从 v1 迁移到 v2 时运行
      2: (websites: IgnoredWebsiteV1[]): IgnoredWebsiteV2[] => { 
        return websites.map((website) => ({ id: nanoid(), website })); 
      }, 
    }, 
  },
);

运行迁移

一旦调用 storage.defineItem,WXT 就会检查是否需要运行迁移,如果需要,则运行它们。对存储项的值或元数据进行获取或更新的调用(如 getValuesetValueremoveValuegetMeta 等)会自动等待迁移过程完成后再实际读取或写入值。

默认值

使用 storage.defineItem,有多种方式定义默认值:

  1. fallback - 如果值缺失,则从 getValue 返回此值而不是 null

    此选项非常适合为设置提供默认值:

    ts 复制代码
    const theme = storage.defineItem('local:theme', {
      fallback: 'dark',
    });
    const allowEditing = storage.defineItem('local:allow-editing', {
      fallback: true,
    });
  2. init - 如果尚未保存,则初始化并保存一个值到存储中。

    这对于需要初始化或一次设置的值非常有用:

    ts 复制代码
    const userId = storage.defineItem('local:user-id', {
      init: () => globalThis.crypto.randomUUID(),
    });
    const installDate = storage.defineItem('local:install-date', {
      init: () => new Date().getTime(),
    });

    值会立即在存储中初始化。

批量操作

当一次性获取或设置多个值时,您可以执行批量操作以通过减少单独的存储调用来提高性能。storage API 提供了几个用于执行批量操作的方法:

  • getItems - 一次性获取多个值。
  • getMetas - 一次性获取多个项的元数据。
  • setItems - 一次性设置多个值。
  • setMetas - 一次性设置多个项的元数据。
  • removeItems - 一次性移除多个值(可选移除元数据)。

所有这些 API 都支持字符串键和定义的存储项:

ts 复制代码
const userId = storage.defineItem('local:userId');
await storage.setItems([
  { key: 'local:installDate', value: Date.now() },
  { item: userId, value: generateUserId() },
]);

有关如何使用所有批量 API 的类型和示例,请参见 API 参考

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

相关推荐
崔庆才丨静觅13 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax