mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +08:00
579 lines
14 KiB
Plaintext
579 lines
14 KiB
Plaintext
---
|
|
description: 使用 WXT、React 和 TypeScript 构建代理管理扩展的指南
|
|
globs:
|
|
alwaysApply: false
|
|
---
|
|
---
|
|
description: 使用 WXT、React 和 TypeScript 构建代理管理扩展的指南
|
|
globs: "**/*.ts, **/*.tsx, **/*.js, **/*.jsx"
|
|
---
|
|
|
|
## 概览
|
|
|
|
](https://wxt.dev/) 是一个为浏览器扩展开发提供现代开发体验的框架。本指南将帮助您使用 WXT、React 和 TypeScript 重构类似 SwitchyOmega 的代理管理扩展。
|
|
|
|
## 项目结构
|
|
|
|
推荐使用以下项目结构:
|
|
```
|
|
.
|
|
├── .output/
|
|
├── .wxt/
|
|
├── modules/
|
|
├── public/ # 包含要复制到输出文件夹的任何文件,而无需WXT处理
|
|
├── ord/ # 需要重构的旧代码
|
|
├── src/
|
|
│ │ ├── assets/
|
|
│ │ ├── components/
|
|
│ │ ├── composables/
|
|
│ │ ├── entrypoints/ # 包含所有被捆绑到扩展名的入口点
|
|
│ │ ├── hooks/ # 默认自动导入,包含项目用于 React 和 Solid 的钩子的源代码
|
|
│ │ ├── utils/
|
|
├── .env
|
|
├── .env.publish
|
|
├── app.config.ts
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── web-ext.config.ts # 配置浏览器启动
|
|
├── wxt.config.ts # WXT项目的主要配置文件
|
|
|
|
```
|
|
|
|
|
|
Different browsers provide different global variables for accessing the extension APIs (chrome provides chrome, firefox provides browser, etc).
|
|
|
|
WXT merges these two into a unified API accessed through the browser variable.
|
|
|
|
```
|
|
import { browser } from 'wxt/browser';
|
|
|
|
browser.action.onClicked.addListener(() => {
|
|
// ...
|
|
});
|
|
```
|
|
TIP
|
|
|
|
With auto-imports enabled, you don't even need to import this variable from wxt/browser!
|
|
|
|
The browser variable WXT provides is a simple export of the browser or chrome globals provided by the browser at runtime:
|
|
|
|
|
|
export const browser = globalThis.browser?.runtime?.id
|
|
? globalThis.browser
|
|
: globalThis.chrome;
|
|
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
|
|
|
|
Accessing Types
|
|
All types can be accessed via WXT's Browser namespace:
|
|
|
|
```
|
|
import { type Browser } from 'wxt/browser';
|
|
|
|
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
|
|
// ...
|
|
}
|
|
```
|
|
|
|
## 入口点设置
|
|
|
|
### 后台脚本
|
|
|
|
```typescript
|
|
// entrypoints/background/index.ts
|
|
import { defineBackground } from 'wxt/background';
|
|
import { setupProxyManagement } from './proxy';
|
|
|
|
export default defineBackground({
|
|
// 设置清单选项
|
|
type: 'module',
|
|
|
|
main() {
|
|
// 初始化代理管理
|
|
setupProxyManagement();
|
|
|
|
// 监听消息
|
|
browser.runtime.onMessage.addListener((message, sender) => {
|
|
if (message.type === 'SWITCH_PROXY') {
|
|
return handleProxySwitch(message.proxyId);
|
|
}
|
|
});
|
|
},
|
|
});
|
|
```
|
|
|
|
### 弹出窗口
|
|
|
|
```html
|
|
<!-- entrypoints/popup/index.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>代理切换器</title>
|
|
<meta
|
|
name="manifest.default_icon"
|
|
content="{
|
|
16: '/icon-16.png',
|
|
48: '/icon-48.png'
|
|
}"
|
|
/>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module" src="./index.tsx"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
```tsx
|
|
// entrypoints/popup/index.tsx
|
|
import React from 'react';
|
|
import { createRoot } from 'react-dom/client';
|
|
import App from './App';
|
|
|
|
const root = createRoot(document.getElementById('app')!);
|
|
root.render(<App />);
|
|
```
|
|
|
|
```tsx
|
|
// entrypoints/popup/App.tsx
|
|
import React, { useState, useEffect } from 'react';
|
|
import ProxySelector from '../../components/ProxySelector';
|
|
import { getProxyList, getCurrentProxy } from '../../utils/proxy';
|
|
import type { Proxy } from '../../types';
|
|
|
|
const App: React.FC = () => {
|
|
const [proxies, setProxies] = useState<Proxy[]>([]);
|
|
const [currentProxy, setCurrentProxy] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
const loadData = async () => {
|
|
const proxyList = await getProxyList();
|
|
const current = await getCurrentProxy();
|
|
setProxies(proxyList);
|
|
setCurrentProxy(current);
|
|
};
|
|
|
|
loadData();
|
|
}, []);
|
|
|
|
const handleProxyChange = async (proxyId: string) => {
|
|
await browser.runtime.sendMessage({ type: 'SWITCH_PROXY', proxyId });
|
|
setCurrentProxy(proxyId);
|
|
};
|
|
|
|
return (
|
|
<div className="popup">
|
|
<h1>代理切换器</h1>
|
|
<ProxySelector
|
|
proxies={proxies}
|
|
currentProxy={currentProxy}
|
|
onChange={handleProxyChange}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|
|
```
|
|
|
|
### 选项页面
|
|
|
|
```html
|
|
<!-- entrypoints/options/index.html -->
|
|
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>代理切换器设置</title>
|
|
<meta name="manifest.open_in_tab" content="true" />
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
<script type="module" src="./index.tsx"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
```tsx
|
|
// entrypoints/options/App.tsx
|
|
import React, { useState, useEffect } from 'react';
|
|
import { getProxyList, saveProxy, deleteProxy } from '../../utils/proxy';
|
|
import type { Proxy } from '../../types';
|
|
|
|
const App: React.FC = () => {
|
|
const [proxies, setProxies] = useState<Proxy[]>([]);
|
|
const [newProxy, setNewProxy] = useState<Partial<Proxy>>({
|
|
name: '',
|
|
host: '',
|
|
port: '',
|
|
protocol: 'http'
|
|
});
|
|
|
|
useEffect(() => {
|
|
loadProxies();
|
|
}, []);
|
|
|
|
const loadProxies = async () => {
|
|
const list = await getProxyList();
|
|
setProxies(list);
|
|
};
|
|
|
|
const handleSaveProxy = async () => {
|
|
if (!newProxy.name || !newProxy.host || !newProxy.port) return;
|
|
|
|
await saveProxy(newProxy as Proxy);
|
|
loadProxies();
|
|
setNewProxy({
|
|
name: '',
|
|
host: '',
|
|
port: '',
|
|
protocol: 'http'
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="options">
|
|
<h1>代理管理器设置</h1>
|
|
|
|
<div className="proxy-list">
|
|
{proxies.map(proxy => (
|
|
<div key={proxy.id} className="proxy-item">
|
|
<span>{proxy.name} ({proxy.protocol}://{proxy.host}:{proxy.port})</span>
|
|
<button onClick={() => deleteProxy(proxy.id)}>删除</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="add-proxy">
|
|
<h2>添加新代理</h2>
|
|
<input
|
|
type="text"
|
|
placeholder="名称"
|
|
value={newProxy.name}
|
|
onChange={e => setNewProxy({...newProxy, name: e.target.value})}
|
|
/>
|
|
<select
|
|
value={newProxy.protocol}
|
|
onChange={e => setNewProxy({...newProxy, protocol: e.target.value})}
|
|
>
|
|
<option value="http">HTTP</option>
|
|
<option value="https">HTTPS</option>
|
|
<option value="socks4">SOCKS4</option>
|
|
<option value="socks5">SOCKS5</option>
|
|
</select>
|
|
<input
|
|
type="text"
|
|
placeholder="主机"
|
|
value={newProxy.host}
|
|
onChange={e => setNewProxy({...newProxy, host: e.target.value})}
|
|
/>
|
|
<input
|
|
type="text"
|
|
placeholder="端口"
|
|
value={newProxy.port}
|
|
onChange={e => setNewProxy({...newProxy, port: e.target.value})}
|
|
/>
|
|
<button onClick={handleSaveProxy}>保存</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|
|
```
|
|
|
|
### 内容脚本
|
|
|
|
```typescript
|
|
// entrypoints/content.ts
|
|
import { defineContentScript } from 'wxt/content-script';
|
|
|
|
export default defineContentScript({
|
|
matches: ['<all_urls>'],
|
|
|
|
main() {
|
|
// 在页面中执行的内容脚本逻辑
|
|
console.log('代理切换器内容脚本已加载');
|
|
|
|
// 根据需要与后台脚本通信
|
|
browser.runtime.sendMessage({ type: 'CONTENT_SCRIPT_LOADED' });
|
|
},
|
|
});
|
|
```
|
|
|
|
## 最佳实践
|
|
|
|
1. **使用 WXT 存储模块**: 利用 `@wxt-dev/storage` 管理扩展数据。
|
|
|
|
```typescript
|
|
// 安装: npm install @wxt-dev/storage
|
|
|
|
// utils/storage.ts
|
|
import { createStorage } from '@wxt-dev/storage';
|
|
|
|
export const storage = createStorage({
|
|
proxies: {
|
|
defaultValue: [],
|
|
schema: z.array(z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
protocol: z.enum(['http', 'https', 'socks4', 'socks5']),
|
|
host: z.string(),
|
|
port: z.string()
|
|
}))
|
|
},
|
|
currentProxyId: {
|
|
defaultValue: null,
|
|
schema: z.string().nullable()
|
|
}
|
|
});
|
|
```
|
|
|
|
2. **组件化开发**: 创建可重用的React组件。
|
|
|
|
```tsx
|
|
// components/ProxySelector.tsx
|
|
import React from 'react';
|
|
import type { Proxy } from '../types';
|
|
|
|
interface ProxySelectorProps {
|
|
proxies: Proxy[];
|
|
currentProxy: string | null;
|
|
onChange: (proxyId: string) => void;
|
|
}
|
|
|
|
const ProxySelector: React.FC<ProxySelectorProps> = ({ proxies, currentProxy, onChange }) => {
|
|
return (
|
|
<div className="proxy-selector">
|
|
{proxies.map(proxy => (
|
|
<div
|
|
key={proxy.id}
|
|
className={`proxy-item ${currentProxy === proxy.id ? 'active' : ''}`}
|
|
onClick={() => onChange(proxy.id)}
|
|
>
|
|
{proxy.name}
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ProxySelector;
|
|
```
|
|
|
|
3. **类型安全**: 为所有对象定义TypeScript接口。
|
|
|
|
```typescript
|
|
// types/index.ts
|
|
export interface Proxy {
|
|
id: string;
|
|
name: string;
|
|
protocol: 'http' | 'https' | 'socks4' | 'socks5';
|
|
host: string;
|
|
port: string;
|
|
username?: string;
|
|
password?: string;
|
|
}
|
|
|
|
export interface ProxyRule {
|
|
id: string;
|
|
name: string;
|
|
pattern: string;
|
|
proxyId: string;
|
|
}
|
|
```
|
|
|
|
4. **使用环境变量**: 为不同环境配置不同的设置。
|
|
|
|
```typescript
|
|
// wxt.config.ts
|
|
import { defineConfig } from 'wxt';
|
|
|
|
export default defineConfig({
|
|
manifest: {
|
|
name: process.env.NODE_ENV === 'development' ? '[DEV] 代理切换器' : '代理切换器',
|
|
version: '1.0.0',
|
|
description: '一个强大的浏览器代理管理扩展',
|
|
},
|
|
// 其他配置...
|
|
});
|
|
```
|
|
|
|
5. **消息通信**: 使用结构化消息系统。
|
|
|
|
```typescript
|
|
// utils/messaging.ts
|
|
export type MessageType =
|
|
| { type: 'SWITCH_PROXY'; proxyId: string }
|
|
| { type: 'GET_CURRENT_PROXY' }
|
|
| { type: 'PROXY_CHANGED'; proxyId: string };
|
|
|
|
export function sendMessage<T extends MessageType>(message: T): Promise<any> {
|
|
return browser.runtime.sendMessage(message);
|
|
}
|
|
```
|
|
|
|
6. **图标状态管理**: 根据当前代理状态更新扩展图标。
|
|
|
|
```typescript
|
|
// background/proxy.ts
|
|
function updateExtensionIcon(proxyId: string | null) {
|
|
const iconPath = proxyId
|
|
? '/icons/proxy-active.png'
|
|
: '/icons/proxy-inactive.png';
|
|
|
|
browser.action.setIcon({ path: iconPath });
|
|
}
|
|
```
|
|
|
|
7. **错误处理**: 实现良好的错误捕获和报告。
|
|
|
|
```typescript
|
|
// utils/error.ts
|
|
export async function executeWithErrorHandling<T>(
|
|
fn: () => Promise<T>,
|
|
errorMessage = '执行操作时出错'
|
|
): Promise<T | null> {
|
|
try {
|
|
return await fn();
|
|
} catch (error) {
|
|
console.error(`${errorMessage}:`, error);
|
|
browser.notifications.create({
|
|
type: 'basic',
|
|
iconUrl: '/icon-48.png',
|
|
title: '代理切换器错误',
|
|
message: errorMessage
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
8. **使用现代钩子**: 为React组件编写自定义钩子。
|
|
|
|
```typescript
|
|
// hooks/useProxies.ts
|
|
import { useState, useEffect } from 'react';
|
|
import { storage } from '../utils/storage';
|
|
import type { Proxy } from '../types';
|
|
|
|
export function useProxies() {
|
|
const [proxies, setProxies] = useState<Proxy[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const load = async () => {
|
|
const data = await storage.proxies.get();
|
|
setProxies(data);
|
|
setLoading(false);
|
|
};
|
|
|
|
load();
|
|
|
|
return storage.proxies.subscribe(newProxies => {
|
|
setProxies(newProxies);
|
|
});
|
|
}, []);
|
|
|
|
return { proxies, loading };
|
|
}
|
|
```
|
|
|
|
## 版本兼容性
|
|
|
|
本指南适用于:
|
|
- WXT v0.20.0 及以上
|
|
- React 18+
|
|
- TypeScript 5.0+
|
|
|
|
## 扩展功能实现
|
|
|
|
### 代理管理功能
|
|
|
|
```typescript
|
|
// utils/proxy.ts
|
|
import { storage } from './storage';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import type { Proxy } from '../types';
|
|
|
|
export async function getProxyList(): Promise<Proxy[]> {
|
|
return await storage.proxies.get();
|
|
}
|
|
|
|
export async function getCurrentProxy(): Promise<string | null> {
|
|
return await storage.currentProxyId.get();
|
|
}
|
|
|
|
export async function switchProxy(proxyId: string | null): Promise<void> {
|
|
// 更新存储
|
|
await storage.currentProxyId.set(proxyId);
|
|
|
|
if (!proxyId) {
|
|
// 清除代理
|
|
await browser.proxy.settings.clear({});
|
|
return;
|
|
}
|
|
|
|
// 获取代理详情
|
|
const proxies = await storage.proxies.get();
|
|
const proxy = proxies.find(p => p.id === proxyId);
|
|
|
|
if (!proxy) return;
|
|
|
|
// 设置代理
|
|
await browser.proxy.settings.set({
|
|
value: {
|
|
mode: 'fixed_servers',
|
|
rules: {
|
|
proxyForHttp: {
|
|
scheme: proxy.protocol,
|
|
host: proxy.host,
|
|
port: parseInt(proxy.port)
|
|
},
|
|
proxyForHttps: {
|
|
scheme: proxy.protocol,
|
|
host: proxy.host,
|
|
port: parseInt(proxy.port)
|
|
}
|
|
}
|
|
},
|
|
scope: 'regular'
|
|
});
|
|
}
|
|
|
|
export async function saveProxy(proxy: Omit<Proxy, 'id'>): Promise<Proxy> {
|
|
const newProxy: Proxy = {
|
|
...proxy,
|
|
id: uuidv4()
|
|
};
|
|
|
|
const proxies = await storage.proxies.get();
|
|
await storage.proxies.set([...proxies, newProxy]);
|
|
|
|
return newProxy;
|
|
}
|
|
|
|
export async function deleteProxy(proxyId: string): Promise<void> {
|
|
const proxies = await storage.proxies.get();
|
|
await storage.proxies.set(proxies.filter(p => p.id !== proxyId));
|
|
|
|
// 如果删除的是当前使用的代理,清除当前代理
|
|
const currentProxyId = await storage.currentProxyId.get();
|
|
if (currentProxyId === proxyId) {
|
|
await storage.currentProxyId.set(null);
|
|
await browser.proxy.settings.clear({});
|
|
}
|
|
}
|
|
```
|
|
|
|
## 相关资源
|
|
|
|
- [WXT 官方文档](mdc:https:/wxt.dev)
|
|
- [WXT GitHub 仓库](mdc:https:/github.com/wxt-dev/wxt)
|
|
- [Chrome 扩展 API 文档](mdc:https:/developer.chrome.com/docs/extensions/reference)
|
|
- [React 文档](mdc:https:/reactjs.org)
|
|
- [TypeScript 文档](mdc:https:/www.typescriptlang.org)
|