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