Appearance
Plasmo
Plasmo 是一个现代化的浏览器插件开发框架,支持 React、TypeScript、Tailwind CSS 等现代技术栈,大大简化了插件开发流程。
特性
- 🚀 零配置:开箱即用,无需复杂配置
- ⚛️ React 支持:使用 React 组件开发插件
- 🖖 Vue 支持:使用 Vue 3 组件开发插件
- 📘 TypeScript:完整的类型支持
- 🎨 Tailwind CSS:内置样式支持
- 🔄 热重载:开发时自动刷新
- 📦 模块化:支持 npm 包管理
- 🔧 自动打包:自动生成 manifest.json
快速开始
安装
bash
# 使用 pnpm
pnpm create plasmo项目结构
my-extension/
├── package.json
├── plasmo.config.ts
├── tsconfig.json
├── .gitignore
├── src/
│ ├── popup.tsx # Popup 页面
│ ├── options.tsx # 选项页面
│ ├── background.ts # 后台脚本
│ ├── content.tsx # 内容脚本
│ ├── content-scripts/ # 多个内容脚本
│ │ ├── style.css
│ │ └── index.tsx
│ └── injected-content.tsx # 注入内容脚本
├── assets/
│ └── icon.png
└── build/ # 构建输出配置文件
plasmo.config.ts
typescript
import type { PlasmoConfig } from 'plasmo';
const config: PlasmoConfig = {
srcDir: 'src',
build: {
manifest: {
name: 'My Extension',
description: 'My awesome extension',
version: '1.0.0',
permissions: ['storage', 'tabs'],
host_permissions: ['https://*/*'],
},
},
};
export default config;核心组件
1. Popup
Popup 是点击插件图标时显示的弹出页面。
typescript
// src/popup.tsx
import { useState, useEffect } from 'react';
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
function IndexPopup() {
const [count, setCount] = useState(0);
useEffect(() => {
storage.get('count').then(value => {
setCount((value as number) || 0);
});
}, []);
const increment = async () => {
const newCount = count + 1;
setCount(newCount);
await storage.set('count', newCount);
};
return (
<div
style={{
width: '300px',
padding: '16px',
fontFamily: 'Arial, sans-serif',
}}
>
<h1 style={{ marginBottom: '16px' }}>My Extension</h1>
<p>Count: {count}</p>
<button
onClick={increment}
style={{
padding: '8px 16px',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Increment
</button>
</div>
);
}
export default IndexPopup;2. Content Script
Content Script 在网页中运行,可以访问和修改页面内容。
typescript
// src/content.tsx
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['<all_urls>'],
all_frames: true,
run_at: 'document_end',
};
export default function ContentScript() {
return (
<div
style={{
position: 'fixed',
top: '20px',
right: '20px',
background: 'white',
padding: '12px',
borderRadius: '8px',
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
zIndex: '9999',
}}
>
<h3>Hello from Content Script!</h3>
<p>Current URL: {window.location.href}</p>
</div>
);
}3. Background Script
Background Script 在后台运行,处理事件和持久化数据。
typescript
// src/background.ts
import type { PlasmoBackgroundConfig } from 'plasmo';
export const config: PlasmoBackgroundConfig = {
persistent: true,
};
const storage = new Storage();
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
storage.set({
settings: {
enabled: true,
theme: 'light',
},
});
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
console.log('Page loaded:', tab.url);
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'getData') {
storage.get('settings').then(settings => {
sendResponse(settings);
});
}
return true;
});4. Options Page
Options Page 是插件的设置页面。
typescript
// src/options.tsx
import { useState, useEffect } from 'react';
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
function Options() {
const [settings, setSettings] = useState({
enabled: true,
theme: 'light',
});
useEffect(() => {
storage.get('settings').then(value => {
if (value) {
setSettings(value as any);
}
});
}, []);
const handleChange = (key: string, value: any) => {
const newSettings = { ...settings, [key]: value };
setSettings(newSettings);
storage.set('settings', newSettings);
};
return (
<div style={{ padding: '24px', maxWidth: '600px' }}>
<h1>Extension Settings</h1>
<div style={{ marginBottom: '16px' }}>
<label>
<input
type="checkbox"
checked={settings.enabled}
onChange={e => handleChange('enabled', e.target.checked)}
/>
Enable Extension
</label>
</div>
<div>
<label>
Theme:
<select
value={settings.theme}
onChange={e => handleChange('theme', e.target.value)}
style={{ marginLeft: '8px' }}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</label>
</div>
</div>
);
}
export default Options;高级功能
0. 使用 Vue
Plasmo 支持 Vue 3,可以使用 Vue 的组合式 API 和单文件组件(SFC)开发插件。
安装 Vue 依赖
bash
npm install vue @vitejs/plugin-vue配置 Plasmo 支持 Vue
typescript
// plasmo.config.ts
import type { PlasmoConfig } from 'plasmo';
import vue from '@vitejs/plugin-vue';
const config: PlasmoConfig = {
srcDir: 'src',
build: {
manifest: {
name: 'My Extension',
description: 'My awesome extension',
version: '1.0.0',
permissions: ['storage', 'tabs'],
host_permissions: ['https://*/*'],
},
vite: {
plugins: [vue()],
},
},
};
export default config;Vue Popup 组件
vue
<!-- src/popup.vue -->
<template>
<div class="popup-container">
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
const title = ref('My Vue Extension');
const count = ref(0);
onMounted(async () => {
const value = await storage.get('count');
count.value = (value as number) || 0;
});
const increment = async () => {
count.value++;
await storage.set('count', count.value);
};
</script>
<style scoped>
.popup-container {
width: 300px;
padding: 16px;
font-family: Arial, sans-serif;
}
h1 {
margin-bottom: 16px;
}
button {
padding: 8px 16px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>Vue Content Script
vue
<!-- src/content.vue -->
<template>
<div class="content-widget">
<h3>Hello from Vue Content Script!</h3>
<p>Current URL: {{ currentUrl }}</p>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['<all_urls>'],
all_frames: true,
run_at: 'document_end',
};
const currentUrl = ref(window.location.href);
</script>
<style scoped>
.content-widget {
position: fixed;
top: 20px;
right: 20px;
background: white;
padding: 12px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 9999;
}
</style>Vue Options Page
vue
<!-- src/options.vue -->
<template>
<div class="options-container">
<h1>Extension Settings</h1>
<div class="setting-item">
<label>
<input
type="checkbox"
v-model="settings.enabled"
@change="saveSettings"
/>
Enable Extension
</label>
</div>
<div class="setting-item">
<label>
Theme:
<select v-model="settings.theme" @change="saveSettings">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</label>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue';
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
const settings = reactive({
enabled: true,
theme: 'light',
});
onMounted(async () => {
const value = await storage.get('settings');
if (value) {
Object.assign(settings, value);
}
});
const saveSettings = async () => {
await storage.set('settings', settings);
};
</script>
<style scoped>
.options-container {
padding: 24px;
max-width: 600px;
}
.setting-item {
margin-bottom: 16px;
}
select {
margin-left: 8px;
}
</style>Vue Background Script
typescript
// src/background.ts
import type { PlasmoBackgroundConfig } from 'plasmo';
import { Storage } from '@plasmohq/storage';
export const config: PlasmoBackgroundConfig = {
persistent: true,
};
const storage = new Storage();
chrome.runtime.onInstalled.addListener(() => {
console.log('Extension installed');
storage.set({
settings: {
enabled: true,
theme: 'light',
},
});
});
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
console.log('Page loaded:', tab.url);
}
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'getData') {
storage.get('settings').then(settings => {
sendResponse(settings);
});
}
return true;
});Vue 组件中使用 Storage
vue
<template>
<div>
<p>Value: {{ data }}</p>
<button @click="updateData">Update</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
const data = ref('');
onMounted(async () => {
const value = await storage.get('key');
data.value = (value as string) || '';
});
const updateData = async () => {
const newValue = 'Updated at ' + new Date().toISOString();
data.value = newValue;
await storage.set('key', newValue);
};
// 监听存储变化
storage.watch({
key: newValue => {
data.value = newValue as string;
},
});
</script>1. 多个 Content Script
typescript
// src/content-scripts/home.tsx
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['https://www.google.com/*'],
};
export default function HomeContentScript() {
return <div>Google Page Script</div>;
}typescript
// src/content-scripts/youtube.tsx
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['https://www.youtube.com/*'],
};
export default function YouTubeContentScript() {
return <div>YouTube Page Script</div>;
}2. 注入 Content Script
注入脚本可以访问页面的 JavaScript 上下文。
typescript
// src/injected-content.tsx
import type { PlasmoCSConfig } from 'plasmo';
export const config: PlasmoCSConfig = {
matches: ['<all_urls>'],
};
export default function InjectedContent() {
console.log('Injected into page context');
console.log('Page variables:', window.someVariable);
return null;
}3. 消息传递
Popup 到 Content Script
typescript
// popup.tsx
const sendMessage = async () => {
const [tab] = await chrome.tabs.query({
active: true,
currentWindow: true,
});
if (tab.id) {
chrome.tabs.sendMessage(tab.id, {
type: 'highlight',
text: 'Hello',
});
}
};typescript
// content.tsx
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'highlight') {
console.log('Highlight:', request.text);
}
});Content Script 到 Background
typescript
// content.tsx
chrome.runtime.sendMessage({
type: 'saveData',
data: { key: 'value' },
});typescript
// background.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'saveData') {
storage.set('data', request.data);
}
});4. Storage API
Plasmo 提供了更友好的 Storage API。
typescript
import { Storage } from '@plasmohq/storage';
const storage = new Storage();
// 保存数据
await storage.set('key', 'value');
// 读取数据
const value = await storage.get('key');
// 监听变化
storage.watch({
key: newValue => {
console.log('Key changed:', newValue);
},
});
// 删除数据
await storage.remove('key');
// 清空所有数据
await storage.clear();5. 使用 CSS
内联样式
typescript
// src/popup.tsx
export default function Popup() {
return (
<div style={{ padding: '16px' }}>
<h1>Hello</h1>
</div>
);
}CSS 模块
css
/* src/style.css */
.container {
padding: 16px;
background: white;
}
.title {
font-size: 24px;
color: #333;
}typescript
// src/popup.tsx
import styles from './style.css';
export default function Popup() {
return (
<div className={styles.container}>
<h1 className={styles.title}>Hello</h1>
</div>
);
}Tailwind CSS
typescript
// src/popup.tsx
export default function Popup() {
return (
<div className="p-4 bg-white">
<h1 className="text-2xl text-gray-800">Hello</h1>
</div>
);
}6. 使用图标
typescript
// src/popup.tsx
import { useState } from 'react';
function Icon() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"
stroke="currentColor"
/>
</svg>
);
}
export default function Popup() {
return (
<div>
<Icon />
</div>
);
}开发命令
bash
# 开发模式(支持热重载)
npm run dev
# 构建生产版本
npm run build
# 打包扩展
npm run package
# 清理构建文件
npm run clean调试
1. 加载扩展
- 运行
npm run dev - 打开 Chrome 浏览器
- 访问
chrome://extensions/ - 开启"开发者模式"
- 点击"加载已解压的扩展程序"
- 选择
build/chrome-mv3-dev文件夹
2. 查看日志
- Popup:右键点击 popup,选择"检查"
- Content Script:在网页控制台查看
- Background:在扩展程序页面点击"service worker"
3. 热重载
修改代码后,扩展会自动刷新。如果没有自动刷新,可以在扩展程序页面点击刷新按钮。
发布
1. 构建
bash
npm run build构建完成后,在 build/chrome-mv3-prod 文件夹中找到打包好的扩展。
2. 打包
将 build/chrome-mv3-prod 文件夹压缩为 zip 文件。
3. 发布到 Chrome Web Store
- 访问 Chrome Web Store Developer Dashboard
- 上传 zip 文件
- 填写商店信息(名称、描述、截图等)
- 设置价格和分发区域
- 提交审核
4. 发布到其他浏览器
- Firefox:访问 Firefox Add-ons Developer Hub
- Edge:访问 Microsoft Edge Add-ons
- Safari:使用 Xcode 打包并发布
最佳实践
1. 使用 TypeScript
充分利用 TypeScript 的类型检查,减少运行时错误。
typescript
interface Settings {
enabled: boolean;
theme: 'light' | 'dark' | 'auto';
notifications: boolean;
}
const settings: Settings = {
enabled: true,
theme: 'light',
notifications: false,
};2. 组件化开发
将功能拆分为可复用的组件。
typescript
// components/Button.tsx
interface ButtonProps {
onClick: () => void;
children: React.ReactNode;
}
export function Button({ onClick, children }: ButtonProps) {
return (
<button
onClick={onClick}
style={{
padding: '8px 16px',
background: '#4CAF50',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{children}
</button>
);
}3. 错误处理
typescript
try {
const data = await storage.get('key');
} catch (error) {
console.error('Failed to load data:', error);
}4. 性能优化
typescript
// 使用 React.memo 避免不必要的重渲染
export default React.memo(function ContentScript() {
return <div>Content</div>;
});
// 使用 useMemo 缓存计算结果
const expensiveValue = useMemo(() => {
return computeExpensiveValue(props.data);
}, [props.data]);5. 权限最小化
只在 plasmo.config.ts 中声明必要的权限。
typescript
const config: PlasmoConfig = {
build: {
manifest: {
permissions: ['storage'], // 只申请必要的权限
host_permissions: ['https://example.com/*'], // 只访问需要的域名
},
},
};常见问题
1. 热重载不工作
确保在开发模式下运行 npm run dev,并且正确加载了 build/chrome-mv3-dev 文件夹。
2. Content Script 样式冲突
使用 CSS Modules 或 scoped CSS 避免样式冲突。
3. 消息传递失败
确保消息发送和接收的代码都在正确的上下文中运行,并且正确处理了异步操作。
4. Storage 数据丢失
使用 chrome.storage.local 而不是 chrome.storage.session 来持久化数据。