Skip to content

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. 消息传递

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. 加载扩展

  1. 运行 npm run dev
  2. 打开 Chrome 浏览器
  3. 访问 chrome://extensions/
  4. 开启"开发者模式"
  5. 点击"加载已解压的扩展程序"
  6. 选择 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

  1. 访问 Chrome Web Store Developer Dashboard
  2. 上传 zip 文件
  3. 填写商店信息(名称、描述、截图等)
  4. 设置价格和分发区域
  5. 提交审核

4. 发布到其他浏览器

最佳实践

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 来持久化数据。

参考资源