Skip to main content

Plasmo 浏览器插件开发框架

Plasmo 是一个可插拔的企业级浏览器插件框架,基于 Webpack + Vite 构建,支持约定式路由、插件体系、微前端等企业级特性。

框架介绍

Plasmo 是一个专为现代浏览器扩展开发设计的框架,它简化了浏览器插件的开发流程,提供了一套完整的工具链和最佳实践。Plasmo 框架采用 React 作为 UI 层,同时支持 TypeScript,让开发者能够使用熟悉的技术栈构建高质量的浏览器扩展。

核心理念

  • 开发体验优先:提供热更新、TypeScript 支持和现代化开发工具链
  • 约定大于配置:通过文件系统路由和智能默认值减少配置负担
  • 企业级可扩展性:支持插件系统、微前端架构和多浏览器兼容
  • 性能优化:内置代码分割、Tree-shaking 和资源优化

与其他框架对比

特性Plasmo传统扩展开发其他框架
开发体验热更新、TypeScript手动刷新、原生JS部分支持热更新
路由系统约定式路由手动配置多种配置方式
构建系统Webpack + Vite手动打包多种构建工具
跨浏览器支持内置适配手动适配部分支持
微前端支持原生支持不支持少数支持
社区生态活跃成长中分散依赖框架而定

安装与使用

快速开始

# 安装 Plasmo CLI
npm install -g plasmo

# 创建新项目
plasmo init my-extension

# 进入项目目录
cd my-extension

# 启动开发服务器
npm run dev

项目结构

my-extension/
├── .plasmo/ # 构建缓存和临时文件
├── assets/ # 静态资源
├── node_modules/ # 依赖包
├── popup/ # 弹出窗口相关组件
├── background.ts # 后台脚本
├── content.ts # 内容脚本
├── options.tsx # 选项页面
├── package.json # 项目配置
└── plasmo.config.ts # Plasmo 配置文件

部署与发布

# 构建生产版本
npm run build

# 打包为 zip 文件
npm run package

生成的扩展包可以上传到 Chrome Web Store、Firefox Add-ons 或其他浏览器的扩展商店。

核心功能

1. 开发模式

// plasmo.config.ts
export default {
manifest: {
name: "我的浏览器插件",
version: "1.0.0",
permissions: ["storage", "activeTab"]
},
// 支持热更新
dev: {
hmr: true
}
}

2. 构建配置

// 支持多入口配置
build: {
outDir: "dist",
sourcemap: process.env.NODE_ENV === "development",
minify: !process.env.DEBUG
}

3. 约定式路由

├── popup.tsx      # 弹出窗口 UI
├── options.tsx # 选项页面
├── background.ts # 后台脚本
├── content.ts # 内容脚本
└── assets/ # 静态资源

通过文件系统自动生成对应的扩展组件,无需手动配置路由。

4. 插件体系

// 示例:内容脚本插件
import { defineContentScript } from "plasmo"

export default defineContentScript({
matches: ["https://*.example.com/*"],
css: ["styles.css"],
main: () => {
console.log("内容脚本已加载")
}
})

5. 状态管理

// 使用内置状态管理
import { Storage } from "@plasmohq/storage"

const storage = new Storage()

// 存储数据
await storage.set("key", "value")

// 读取数据
const value = await storage.get("key")

// 监听变化
storage.watch({
key: "key",
callback: (newValue) => {
console.log("值已更新:", newValue)
}
})

典型使用场景

1. 跨浏览器支持

// manifest.json
{
"manifest_version": 3,
"browser_specific_settings": {
"gecko": {
"id": "extension@example.com",
"strict_min_version": "91.0"
}
}
}

2. 微前端集成

// 远程组件加载
import { loadComponent } from "plasmo"

const RemoteButton = await loadComponent(
"https://cdn.example.com/button.js"
)

3. 国际化支持

// i18n.ts
import { createI18n } from "@plasmohq/i18n"

export const i18n = createI18n({
en: {
hello: "Hello",
welcome: "Welcome to my extension"
},
zh: {
hello: "你好",
welcome: "欢迎使用我的扩展"
}
})

// 在组件中使用
import { i18n } from "./i18n"

function Popup() {
return (
<div>
<h1>{i18n.t("hello")}</h1>
<p>{i18n.t("welcome")}</p>
</div>
)
}

浏览器存储与安全

1. 浏览器存储 API

localStorage 和 sessionStorage

// 使用原生 Web Storage API
const saveToLocalStorage = () => {
// 存储数据
localStorage.setItem("user_preference", JSON.stringify({
theme: "dark",
fontSize: "medium"
}))

// 读取数据
const preferences = JSON.parse(localStorage.getItem("user_preference") || "{}")

// 删除数据
localStorage.removeItem("user_preference")

// 清空所有数据
localStorage.clear()
}

// 使用 Plasmo 封装的存储 API
import { Storage } from "@plasmohq/storage"
const localStorage = new Storage({ area: "local" })
const sessionStorage = new Storage({ area: "session" })

// 存储和获取数据
await localStorage.set("key", { complex: "value" })
const value = await localStorage.get("key")

安全加密存储

// 使用 Web Crypto API 进行数据加密存储
import { Storage } from "@plasmohq/storage"

// 创建安全存储工具类
class SecureStorage {
private storage: Storage
private cryptoKey: CryptoKey | null = null

constructor(storageArea: string = "local") {
this.storage = new Storage({ area: storageArea })
}

// 生成加密密钥
async generateKey(password: string): Promise<CryptoKey> {
// 从密码派生密钥
const encoder = new TextEncoder()
const passwordData = encoder.encode(password)

// 创建盐值(在实际应用中应存储并重用)
const salt = crypto.getRandomValues(new Uint8Array(16))

// 从密码派生密钥材料
const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

// 使用 PBKDF2 派生实际的加密密钥
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
)

// 存储盐值以便后续使用
await this.storage.set("_secure_salt", Array.from(salt))

this.cryptoKey = key
return key
}

// 加载已有密钥
async loadKey(password: string): Promise<CryptoKey> {
// 获取存储的盐值
const saltArray = await this.storage.get("_secure_salt")
if (!saltArray) {
throw new Error("未找到加密盐值,请先生成密钥")
}

const salt = new Uint8Array(saltArray)
const encoder = new TextEncoder()
const passwordData = encoder.encode(password)

// 从密码派生密钥材料
const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

// 使用相同参数重新派生密钥
const key = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
)

this.cryptoKey = key
return key
}

// 加密并存储数据
async setEncrypted(key: string, value: any): Promise<void> {
if (!this.cryptoKey) {
throw new Error("请先初始化或加载加密密钥")
}

// 生成随机初始化向量
const iv = crypto.getRandomValues(new Uint8Array(12))

// 将数据转换为字节数组
const encoder = new TextEncoder()
const data = encoder.encode(JSON.stringify(value))

// 加密数据
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
this.cryptoKey,
data
)

// 存储加密数据和初始化向量
await this.storage.set(`${key}_encrypted`, {
data: Array.from(new Uint8Array(encryptedData)),
iv: Array.from(iv)
})
}

// 获取并解密数据
async getEncrypted(key: string): Promise<any> {
if (!this.cryptoKey) {
throw new Error("请先初始化或加载加密密钥")
}

// 获取加密数据
const encryptedObj = await this.storage.get(`${key}_encrypted`)
if (!encryptedObj) {
return null
}

// 转换回二进制格式
const encryptedData = new Uint8Array(encryptedObj.data)
const iv = new Uint8Array(encryptedObj.iv)

// 解密数据
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
this.cryptoKey,
encryptedData
)

// 转换回原始格式
const decoder = new TextDecoder()
const jsonString = decoder.decode(decryptedData)
return JSON.parse(jsonString)
}

// 删除加密数据
async removeEncrypted(key: string): Promise<void> {
await this.storage.remove(`${key}_encrypted`)
}

// 清除所有加密数据和密钥
async clearEncrypted(): Promise<void> {
// 获取所有键
const allKeys = await this.storage.getAll()

// 删除所有加密数据
for (const key of Object.keys(allKeys)) {
if (key.endsWith("_encrypted") || key === "_secure_salt") {
await this.storage.remove(key)
}
}

this.cryptoKey = null
}
}

// 使用示例
async function secureStorageExample() {
const secureStorage = new SecureStorage()

// 初始化密钥(首次使用)
await secureStorage.generateKey("user-strong-password")
// 或加载已有密钥(后续使用)
// await secureStorage.loadKey("user-strong-password")

// 加密存储敏感数据
await secureStorage.setEncrypted("credentials", {
apiKey: "sk_live_abcdefghijklmnopqrstuvwxyz",
userToken: "user_token_12345",
accountId: "acc_67890"
})

// 获取并解密数据
const credentials = await secureStorage.getEncrypted("credentials")
console.log("解密的凭证:", credentials)

// 删除特定加密数据
await secureStorage.removeEncrypted("credentials")

// 清除所有加密数据和密钥
await secureStorage.clearEncrypted()
}

非对称加密存储

// 使用 RSA 非对称加密进行更高安全性的数据存储
import { Storage } from "@plasmohq/storage"

class AsymmetricSecureStorage {
private storage: Storage
private publicKey: CryptoKey | null = null
private privateKey: CryptoKey | null = null

constructor(storageArea: string = "local") {
this.storage = new Storage({ area: storageArea })
}

// 生成密钥对
async generateKeyPair(): Promise<{publicKey: CryptoKey, privateKey: CryptoKey}> {
// 生成 RSA 密钥对
const keyPair = await crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
)

this.publicKey = keyPair.publicKey
this.privateKey = keyPair.privateKey

// 导出公钥以便存储
const exportedPublicKey = await crypto.subtle.exportKey(
"spki",
keyPair.publicKey
)

// 存储公钥
await this.storage.set("_rsa_public_key", Array.from(new Uint8Array(exportedPublicKey)))

return keyPair
}

// 安全地存储私钥(使用密码加密)
async securePrivateKey(password: string): Promise<void> {
if (!this.privateKey) {
throw new Error("请先生成密钥对")
}

// 从密码派生 AES 密钥
const encoder = new TextEncoder()
const passwordData = encoder.encode(password)
const salt = crypto.getRandomValues(new Uint8Array(16))

const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
)

// 导出私钥
const exportedPrivateKey = await crypto.subtle.exportKey(
"pkcs8",
this.privateKey
)

// 加密私钥
const iv = crypto.getRandomValues(new Uint8Array(12))
const encryptedPrivateKey = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
aesKey,
exportedPrivateKey
)

// 存储加密的私钥和相关信息
await this.storage.set("_rsa_private_key_encrypted", {
key: Array.from(new Uint8Array(encryptedPrivateKey)),
iv: Array.from(iv),
salt: Array.from(salt)
})
}

// 加载密钥对
async loadKeyPair(password: string): Promise<boolean> {
// 加载公钥
const publicKeyArray = await this.storage.get("_rsa_public_key")
if (!publicKeyArray) {
return false
}

// 加载加密的私钥
const encryptedPrivateKeyObj = await this.storage.get("_rsa_private_key_encrypted")
if (!encryptedPrivateKeyObj) {
return false
}

// 导入公钥
this.publicKey = await crypto.subtle.importKey(
"spki",
new Uint8Array(publicKeyArray),
{
name: "RSA-OAEP",
hash: "SHA-256"
},
true,
["encrypt"]
)

// 从密码派生 AES 密钥用于解密私钥
const encoder = new TextEncoder()
const passwordData = encoder.encode(password)
const salt = new Uint8Array(encryptedPrivateKeyObj.salt)

const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

const aesKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
)

try {
// 解密私钥
const iv = new Uint8Array(encryptedPrivateKeyObj.iv)
const encryptedPrivateKey = new Uint8Array(encryptedPrivateKeyObj.key)

const decryptedPrivateKey = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
aesKey,
encryptedPrivateKey
)

// 导入私钥
this.privateKey = await crypto.subtle.importKey(
"pkcs8",
decryptedPrivateKey,
{
name: "RSA-OAEP",
hash: "SHA-256"
},
true,
["decrypt"]
)

return true
} catch (error) {
console.error("密码错误或密钥已损坏:", error)
return false
}
}

// 加密数据
async encrypt(data: any): Promise<string> {
if (!this.publicKey) {
throw new Error("请先生成或加载密钥对")
}

// 将数据转换为字节数组
const encoder = new TextEncoder()
const dataString = JSON.stringify(data)
const dataBytes = encoder.encode(dataString)

// 由于 RSA 加密有大小限制,对于大数据需要使用混合加密
// 这里使用 AES 加密数据,然后用 RSA 加密 AES 密钥

// 生成随机 AES 密钥
const aesKey = await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256
},
true,
["encrypt", "decrypt"]
)

// 使用 AES 加密数据
const iv = crypto.getRandomValues(new Uint8Array(12))
const encryptedData = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
aesKey,
dataBytes
)

// 导出 AES 密钥
const exportedAesKey = await crypto.subtle.exportKey("raw", aesKey)

// 使用 RSA 加密 AES 密钥
const encryptedAesKey = await crypto.subtle.encrypt(
{
name: "RSA-OAEP"
},
this.publicKey,
exportedAesKey
)

// 将所有加密数据打包在一起
return JSON.stringify({
data: Array.from(new Uint8Array(encryptedData)),
key: Array.from(new Uint8Array(encryptedAesKey)),
iv: Array.from(iv)
})
}

// 解密数据
async decrypt(encryptedPackage: string): Promise<any> {
if (!this.privateKey) {
throw new Error("请先加载私钥")
}

// 解析加密包
const encryptedObj = JSON.parse(encryptedPackage)
const encryptedData = new Uint8Array(encryptedObj.data)
const encryptedAesKey = new Uint8Array(encryptedObj.key)
const iv = new Uint8Array(encryptedObj.iv)

// 使用 RSA 解密 AES 密钥
const aesKeyBytes = await crypto.subtle.decrypt(
{
name: "RSA-OAEP"
},
this.privateKey,
encryptedAesKey
)

// 导入 AES 密钥
const aesKey = await crypto.subtle.importKey(
"raw",
aesKeyBytes,
{
name: "AES-GCM",
length: 256
},
false,
["decrypt"]
)

// 使用 AES 解密数据
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
aesKey,
encryptedData
)

// 转换回原始格式
const decoder = new TextDecoder()
const jsonString = decoder.decode(decryptedData)
return JSON.parse(jsonString)
}

// 加密并存储数据
async setEncrypted(key: string, value: any): Promise<void> {
const encryptedPackage = await this.encrypt(value)
await this.storage.set(`${key}_rsa_encrypted`, encryptedPackage)
}

// 获取并解密数据
async getEncrypted(key: string): Promise<any> {
const encryptedPackage = await this.storage.get(`${key}_rsa_encrypted`)
if (!encryptedPackage) {
return null
}

return await this.decrypt(encryptedPackage)
}

// 删除加密数据
async removeEncrypted(key: string): Promise<void> {
await this.storage.remove(`${key}_rsa_encrypted`)
}
}

// 使用示例
async function asymmetricEncryptionExample() {
const secureStorage = new AsymmetricSecureStorage()

// 首次使用:生成密钥对并安全存储私钥
await secureStorage.generateKeyPair()
await secureStorage.securePrivateKey("master-password")

// 后续使用:加载密钥对
const success = await secureStorage.loadKeyPair("master-password")
if (!success) {
console.error("无法加载密钥对,密码可能不正确")
return
}

// 加密存储敏感数据
await secureStorage.setEncrypted("api_credentials", {
apiKey: "very-sensitive-api-key",
clientSecret: "super-secret-value"
})

// 获取并解密数据
const credentials = await secureStorage.getEncrypted("api_credentials")
console.log("解密的凭证:", credentials)
}

2. 安全存储最佳实践

敏感数据处理原则

// 安全存储最佳实践示例

// 1. 永远不要存储明文密码
// 错误示例 ❌
const saveUserCredentials = (username: string, password: string) => {
localStorage.setItem("user_password", password) // 不安全!
}

// 正确示例 ✅
import { SecureStorage } from "./secure-storage"

const secureStorage = new SecureStorage()
const saveUserCredentials = async (username: string, password: string) => {
// 仅存储密码哈希用于验证,而不是密码本身
const encoder = new TextEncoder()
const passwordData = encoder.encode(password)
const hashBuffer = await crypto.subtle.digest("SHA-256", passwordData)
const hashArray = Array.from(new Uint8Array(hashBuffer))
const passwordHash = hashArray.map(b => b.toString(16).padStart(2, "0")).join("")

// 使用密码初始化加密存储,但只存储哈希
await secureStorage.generateKey(password)
await secureStorage.setEncrypted("user_auth", {
username,
passwordHash,
lastLogin: Date.now()
})
}

// 2. 最小化存储敏感数据
// 错误示例 ❌
const storeUserProfile = (userData) => {
localStorage.setItem("user_profile", JSON.stringify({
name: userData.name,
email: userData.email,
ssn: userData.socialSecurityNumber, // 不应存储高敏感数据
creditCard: userData.creditCardNumber // 不应存储支付信息
}))
}

// 正确示例 ✅
const storeUserProfile = async (userData) => {
// 普通数据使用常规存储
localStorage.setItem("user_profile", JSON.stringify({
name: userData.name,
email: userData.email,
preferences: userData.preferences
}))

// 仅在必要时加密存储高敏感数据,并考虑是否真的需要存储
if (userData.rememberPaymentInfo) {
// 仅存储支付信息的最后四位和令牌,而非完整信息
await secureStorage.setEncrypted("payment_info", {
lastFour: userData.creditCardNumber.slice(-4),
token: userData.paymentToken, // 使用支付处理商提供的令牌
expiry: userData.expiryDate
})
}
}

// 3. 实现数据过期机制
const storeTemporarySensitiveData = async (data, expiryMinutes = 30) => {
await secureStorage.setEncrypted("temp_sensitive_data", {
data,
expiryTimestamp: Date.now() + (expiryMinutes * 60 * 1000)
})
}

const retrieveTemporarySensitiveData = async () => {
const encryptedData = await secureStorage.getEncrypted("temp_sensitive_data")

if (!encryptedData) return null

// 检查数据是否已过期
if (Date.now() > encryptedData.expiryTimestamp) {
// 数据已过期,删除它
await secureStorage.removeEncrypted("temp_sensitive_data")
return null
}

return encryptedData.data
}

防止 XSS 攻击窃取存储数据

// 实现内容安全策略 (CSP)
// 在 manifest.json 中添加 CSP
{
"manifest_version": 3,
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; frame-ancestors 'none'"
}
}

// 使用 HttpOnly Cookie 而非本地存储存储会话令牌
// 在后台脚本中处理身份验证
import { Storage } from "@plasmohq/storage"

const storage = new Storage()

// 安全地处理身份验证令牌
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.type === "authenticate") {
try {
// 使用安全的 fetch 请求获取令牌
const response = await fetch("https://api.example.com/auth", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: message.username,
password: message.password
})
})

const data = await response.json()

if (data.token) {
// 存储令牌在扩展的存储中,而不是在内容脚本可访问的存储中
await storage.set("auth_token", data.token)
sendResponse({ success: true })
} else {
sendResponse({ success: false, error: "认证失败" })
}
} catch (error) {
sendResponse({ success: false, error: error.message })
}

return true // 保持消息通道开放以进行异步响应
}
})

// 在内容脚本中,不直接访问令牌
chrome.runtime.sendMessage(
{ type: "makeAuthenticatedRequest", endpoint: "/user/profile" },
(response) => {
if (response.success) {
// 处理响应数据
updateUI(response.data)
}
}
)

// 在后台脚本中处理经过身份验证的请求
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.type === "makeAuthenticatedRequest") {
try {
// 从存储中获取令牌
const token = await storage.get("auth_token")
if (!token) {
sendResponse({ success: false, error: "未认证" })
return
}

// 使用令牌发出请求
const response = await fetch(`https://api.example.com${message.endpoint}`, {
headers: {
"Authorization": `Bearer ${token}`
}
})

const data = await response.json()
sendResponse({ success: true, data })
} catch (error) {
sendResponse({ success: false, error: error.message })
}

return true
}
})

密钥轮换和备份策略

// 实现密钥轮换机制
class KeyRotationManager {
private secureStorage: SecureStorage
private keyMetadata: Storage

constructor() {
this.secureStorage = new SecureStorage()
this.keyMetadata = new Storage({ area: "local" })
}

// 初始化密钥
async initializeKey(password: string): Promise<void> {
await this.secureStorage.generateKey(password)

// 存储密钥元数据
await this.keyMetadata.set("key_metadata", {
created: Date.now(),
lastRotated: null,
version: 1
})
}

// 检查密钥是否需要轮换
async shouldRotateKey(): Promise<boolean> {
const metadata = await this.keyMetadata.get("key_metadata")
if (!metadata) return false

// 如果密钥超过90天未轮换,建议轮换
const ninetyDaysMs = 90 * 24 * 60 * 60 * 1000
const lastRotated = metadata.lastRotated || metadata.created

return (Date.now() - lastRotated) > ninetyDaysMs
}

// 轮换密钥
async rotateKey(oldPassword: string, newPassword: string): Promise<boolean> {
try {
// 加载旧密钥
await this.secureStorage.loadKey(oldPassword)

// 获取所有加密数据
const allKeys = await this.keyMetadata.getAll()
const encryptedKeys = Object.keys(allKeys).filter(key =>
key.endsWith("_encrypted") && key !== "_secure_salt"
)

// 临时存储解密的数据
const decryptedData: Record<string, any> = {}

// 解密所有数据
for (const key of encryptedKeys) {
const baseKey = key.replace("_encrypted", "")
decryptedData[baseKey] = await this.secureStorage.getEncrypted(baseKey)
}

// 生成新密钥
await this.secureStorage.generateKey(newPassword)

// 使用新密钥重新加密所有数据
for (const baseKey in decryptedData) {
await this.secureStorage.setEncrypted(baseKey, decryptedData[baseKey])
}

// 更新密钥元数据
const metadata = await this.keyMetadata.get("key_metadata")
await this.keyMetadata.set("key_metadata", {
...metadata,
lastRotated: Date.now(),
version: metadata.version + 1
})

return true
} catch (error) {
console.error("密钥轮换失败:", error)
return false
}
}

// 导出加密备份
async exportEncryptedBackup(backupPassword: string): Promise<string> {
// 获取所有加密数据
const allStorage = await this.keyMetadata.getAll()

// 创建备份对象
const backup = {
timestamp: Date.now(),
metadata: await this.keyMetadata.get("key_metadata"),
data: {}
}

// 收集所有加密数据
for (const key in allStorage) {
if (key.endsWith("_encrypted") || key === "_secure_salt") {
backup.data[key] = allStorage[key]
}
}

// 使用备份密码加密整个备份
const encoder = new TextEncoder()
const backupData = encoder.encode(JSON.stringify(backup))

// 从备份密码派生密钥
const salt = crypto.getRandomValues(new Uint8Array(16))
const passwordData = encoder.encode(backupPassword)

const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

const backupKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt"]
)

// 加密备份
const iv = crypto.getRandomValues(new Uint8Array(12))
const encryptedBackup = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv
},
backupKey,
backupData
)

// 创建最终备份包
const finalBackup = {
salt: Array.from(salt),
iv: Array.from(iv),
data: Array.from(new Uint8Array(encryptedBackup))
}

return JSON.stringify(finalBackup)
}

// 导入加密备份
async importEncryptedBackup(backupString: string, backupPassword: string): Promise<boolean> {
try {
// 解析备份包
const backup = JSON.parse(backupString)
const salt = new Uint8Array(backup.salt)
const iv = new Uint8Array(backup.iv)
const encryptedData = new Uint8Array(backup.data)

// 从备份密码派生密钥
const encoder = new TextEncoder()
const passwordData = encoder.encode(backupPassword)

const keyMaterial = await crypto.subtle.importKey(
"raw",
passwordData,
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
)

const backupKey = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["decrypt"]
)

// 解密备份
const decryptedData = await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv
},
backupKey,
encryptedData
)

// 解析备份内容
const decoder = new TextDecoder()
const backupContent = JSON.parse(decoder.decode(decryptedData))

// 恢复所有数据
for (const key in backupContent.data) {
await this.keyMetadata.set(key, backupContent.data[key])
}

// 恢复元数据
await this.keyMetadata.set("key_metadata", backupContent.metadata)

return true
} catch (error) {
console.error("导入备份失败:", error)
return false
}
}
}

IndexedDB

// 使用 Plasmo 的 IndexedDB 封装
import { IndexedDB } from "@plasmohq/storage/indexed-db"

const db = new IndexedDB({
name: "my-extension-db",
version: 1,
stores: [{
name: "user_data",
keyPath: "id",
indices: [{ name: "timestamp", keyPath: "timestamp" }]
}]
})

// 添加数据
await db.add("user_data", {
id: "user_1",
name: "张三",
preferences: { theme: "dark" },
timestamp: Date.now()
})

// 获取数据
const userData = await db.get("user_data", "user_1")

// 更新数据
await db.put("user_data", {
...userData,
preferences: { theme: "light" }
})

// 删除数据
await db.delete("user_data", "user_1")

// 使用索引查询
const recentUsers = await db.getAll("user_data", {
index: "timestamp",
range: IDBKeyRange.lowerBound(Date.now() - 86400000) // 过去24小时
})

chrome.storage API

// 使用 Chrome 扩展存储 API
import { Storage } from "@plasmohq/storage"

// 创建不同类型的存储实例
const localStorage = new Storage({ area: "local" }) // 本地存储
const syncStorage = new Storage({ area: "sync" }) // 同步存储(跨设备)
const managedStorage = new Storage({ area: "managed" }) // 管理员配置的存储

// 存储数据(支持复杂对象)
await syncStorage.set("settings", {
notifications: true,
autoUpdate: false,
dataLimit: 1000
})

// 批量操作
await localStorage.setMany({
"history": [...previousHistory, newItem],
"lastUpdated": Date.now(),
"counter": (await localStorage.get("counter") || 0) + 1
})

// 监听存储变化
syncStorage.watch({
settings: (newValue, oldValue) => {
console.log("设置已更新:", { newValue, oldValue })
updateUI(newValue)
}
})

// 获取存储使用情况
const usage = await chrome.storage.local.getBytesInUse(null)
console.log(`已使用 ${usage} 字节的存储空间`)

最佳实践

1. 调试技巧

# 开启调试模式
DEBUG=* plasmo dev

# 查看构建分析报告
plasmo build --analyze

2. 性能优化

// 使用动态导入
chrome.runtime.onMessage.addListener(
async (message, sender, sendResponse) => {
const module = await import("./heavy-module")
module.handleMessage(message)
}
)

3. 代码组织

├── components/    # 可复用组件
│ ├── Button.tsx
│ └── Card.tsx
├── hooks/ # 自定义 Hooks
│ ├── useStorage.ts
│ └── useMessaging.ts
├── utils/ # 工具函数
│ ├── api.ts
│ └── helpers.ts
└── pages/ # 页面组件
├── popup.tsx
└── options.tsx

4. 测试策略

// 使用 Jest 测试 Plasmo 组件
import { render, screen } from "@testing-library/react"
import Popup from "../popup"

test("renders popup correctly", () => {
render(<Popup />)
expect(screen.getByText("Hello")).toBeInTheDocument()
})

与构建工具集成

Vite 配置示例

// vite.config.ts
import { defineConfig } from "vite"
import plasmo from "plasmo/vite"

export default defineConfig({
plugins: [plasmo()],
build: {
rollupOptions: {
output: {
assetFileNames: "assets/[name].[hash].[ext]"
}
}
}
})

Webpack 集成

// webpack.config.js
const plasmoPreset = require("plasmo/webpack-preset")

module.exports = plasmoPreset({
// 自定义配置
optimization: {
splitChunks: {
chunks: "all"
}
}
})

高级用法

自定义主题

// theme.ts
import { createTheme } from "@plasmohq/theme"

export const theme = createTheme({
colors: {
primary: "#3498db",
secondary: "#2ecc71