区块链钱包开发(一)—— 浏览器插件开发

219 阅读6分钟

前言

本文专注于 Chrome 插件 Manifest V3(MV3)版本的开发指南,原因如下:

  1. 行业标准

    • Chrome 浏览器占据全球浏览器市场 65% 以上的份额(2024年统计)
    • 2024年1月起,Chrome 应用商店已全面下架所有 Manifest V2 插件,仅接受 MV3 版本上架
  2. 学习聚焦

    • 本教程针对区块链钱包开发场景,插件作为载体只需掌握核心功能:
      • 通信相关
      • 存储相关
      • 基础组件(content-script,service worker,popup等)
  3. 延伸学习

开发调试

Chrome插件没有严格的项目结构要求,只要保证本目录有一个manifest.json即可,也不需要专门的IDE,普通的web开发工具即可。

从右上角菜单->更多工具->扩展程序可以进入 插件管理页面,也可以直接在地址栏输入 chrome://extensions 访问。

勾选开发者模式即可以文件夹的形式直接加载插件,否则只能安装.crx格式的文件。Chrome要求插件必须从它的Chrome应用商店安装,其它任何网站下载的都无法直接安装,所以,其实我们可以把crx文件解压,然后通过开发者模式直接加载。

核心组件

manifest.json

每个插件都必须包含一个 manifest.json 文件,位于扩展根目录。以下是一个基本的 MV3 清单文件结构:

{
  "manifest_version": 3,  // 必须为3
  "name": "扩展名称",     // 显示名称(≤45字符)
  "version": "1.0.0",     // 版本号
  "action": {             // 浏览器工具栏按钮配置
    "default_popup": "popup.html"
  },
  "background": {         // 后台脚本(Service Worker)
    "service_worker": "background.js"
  },
  "content_scripts": [{    // 注入页面的脚本
    "matches": ["*://*.example.com/*"],
    "js": ["content.js"]
  }],
  "permissions": [        // 所需API权限
    "storage", "tabs"
  ],
  "host_permissions": [    // 需要访问的网站
    "https://api.example.com/"
  ]
}

content-scripts

content_scripts 是 Chrome 扩展中用于向网页注入 JS/CSS 的脚本,我们每新建一个网页,都会被注入一个或多个content_scripts脚本

{
  "manifest_version": 3,
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],  // 匹配的网址
      "js": ["content1.js, content2.js"],    // 注入的JS
      "css": ["content.css"],                // 注入的CSS
      "run_at": "document_idle"              // 注入时机,可选值:"document_start","document_end",or "document_idle",最后一个表示页面空闲时,默认document_idle```(document_start|document_end|document_idle)
    }
  ]
}

大多数教程里都会说content-scripts和原始页面共享DOM,但是不共享JS,想要访问JS需要通过DOM 事件注入 script 标签,这是不严谨的,在manifest.json文件中配置content_scripts可以指定world参数,如果不指定默认为ISOLATED,这种情况确实无法访问JS,因为content-scripts运行在和页面隔离的环境中。但是如果我们把参数设置为MAIN,意味着脚本会被注入到页面的主执行环境(main world),与页面原有的 JavaScript 共享同一个全局作用域(即 window 对象)。这意味着注入的脚本可以:

  • 直接访问和修改页面的全局变量(如 window.pageVariable)。
  • 调用页面定义的函数。
  • 覆盖或扩展页面原有的 JS 逻辑

需要特别注意的是我们直接在manifest.json文件中配置参数world: 'MAIN'无法生效,这是Chrome的一个bug,只能通过chrome.scripting.registerContentScripts动态注入:

// 注册一个在 MAIN 环境中运行的内容脚本
const scriptConfig = {
  id: "main-world-script",
  js: ["main-world.js"],  // 脚本文件需打包在扩展中
  matches: ["https://example.com/*"],
  world: "MAIN"  // 关键参数
};

await chrome.scripting.registerContentScripts([scriptConfig]);

background(service worker)

插件的后台服务,理论上可以随着浏览器的打开关闭一直运行在后台,但在MV3中如果长时间无操作会被关闭,我们可以通过定时触发后台事件的方式使其一直运行,这在钱包开发中很重要,因为我们要确保后台不会停止运行。

{
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  }
}

popup

popup是点击插件图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互

{
  "manifest_version": 3,
  "action": {
    "default_popup": "popup.html",
    "default_icon": "icon.png"
  }
}

消息通信

网页,content-script,popup,background之间相互通信的关系图:

flowchart LR
    subgraph 网页环境
        A[原始网页] -->|window.postMessage| B[Content Script]
    end

    subgraph 扩展环境
        B -->|chrome.runtime.sendMessage/connect| C[Background\nService Worker]
        C -->|chrome.runtime.sendMessage| D[Popup]
        D -->|chrome.runtime.sendMessage| C
        C -->|chrome.tabs.sendMessage/connect| B
    end

    B -->|window.postMessage| A

    style C fill:#f9f,stroke:#333
    style B fill:#bbf,stroke:#333
    style D fill:#bfb,stroke:#333
    style A fill:#ffb,stroke:#333

实际的钱包开发中我们会对这些API做进一步的封装,详见后续系列教程。

存储

在 Chrome 扩展开发(Manifest V3)中,我们应当避免过度依赖全局变量,因为 Service Worker 的生命周期特性决定了它会在不活跃时被浏览器自动终止,此时所有全局变量都会丢失。为了确保数据的持久性和一致性,我们需要将关键变量进行持久化存储。Chrome 扩展提供了多种存储方案,以下是详细说明和实际应用建议:


为什么避免全局变量?

  • Service Worker 特性:MV3 的后台脚本运行在 Service Worker 中,浏览器会在空闲时终止它(通常 30 秒无活动后)
  • 数据丢失风险:全局变量在 Service Worker 重启后会被重置
  • 状态不可靠:依赖内存状态的逻辑会失效(如计数器、临时缓存)

持久化存储方案

chrome.storage.local

用途:长期持久化存储(扩展卸载前一直存在)
特点

  • 异步操作(避免阻塞主线程)
  • 默认配额 5MB(可通过 unlimitedStorage 权限申请更多)
  • 数据以键值对存储,支持 所有 JSON 兼容类型

示例代码

// 存储数据
await chrome.storage.local.set({ 
  userSettings: { theme: "dark", locale: "zh-CN" } 
});

// 读取数据
const { userSettings } = await chrome.storage.local.get("userSettings");
console.log(userSettings.theme); // "dark"

// 监听变化
chrome.storage.onChanged.addListener((changes) => {
  if (changes.userSettings) {
    console.log("设置已更新:", changes.userSettings.newValue);
  }
});

适用场景

  • 用户配置项
  • 需要跨扩展组件共享的数据
  • 低频更新的缓存数据

chrome.storage.session

用途:内存级临时存储(仅在浏览器会话期间有效)
特点

  • 数据保存在内存中,浏览器关闭后清除
  • 默认配额 10MB
  • 读写速度比 local 更快

示例代码

// 存储会话数据
await chrome.storage.session.set({ 
  tempToken: "abc123",
  lastActiveTime: Date.now()
});

// 读取数据
const { tempToken } = await chrome.storage.session.get("tempToken");

适用场景

  • 敏感数据(如 OAuth token,避免长期存储)
  • 高频更新的临时状态
  • 需要快速读写的中间结果

IndexedDB

用途:结构化大数据存储(类似小型数据库)
特点

  • 支持事务、索引、复杂查询
  • 存储量更大(通常 50% 磁盘空间)

示例代码

// 打开/创建数据库
const db = await new Promise((resolve) => {
  const request = indexedDB.open("WalletDB", 1);
  request.onupgradeneeded = (e) => {
    const db = e.target.result;
    if (!db.objectStoreNames.contains("wallets")) {
      db.createObjectStore("wallets", { keyPath: "id" });
    }
  };
  request.onsuccess = (e) => resolve(e.target.result);
});

// 存储钱包数据
const tx = db.transaction("wallets", "readwrite");
tx.objectStore("wallets").put({
  id: "1",
  address: "0x123...",
  balance: "100 ETH"
});
await new Promise((resolve) => tx.oncomplete = resolve);

在实际钱包开发中我们既需要chrome.storage也需要IndexedDB做备用,详见后续系列教程。

学习交流请添加vx: gh313061

下期预告:创建manifest文件