Chrome 扩展开发完全指南 / 第 6 章:选项页面(Options Page)
第 6 章:选项页面(Options Page)
选项页面(Options Page)是扩展提供给用户的配置界面。与 Popup 不同,Options Page 是一个独立的标签页或内嵌面板,适合展示复杂的设置项和配置界面。
6.1 Options Page 基础
6.1.1 两种类型
| 类型 | 声明方式 | 展示方式 | 适用场景 |
|---|---|---|---|
| 独立页面 | "options_page": "options.html" | 新标签页打开 | 复杂设置界面 |
| 内嵌页面 | "options_ui": { "page": "options.html", "open_in_tab": false } | 在扩展管理页内嵌 | 简单配置 |
6.1.2 manifest.json 声明
{
"options_ui": {
"page": "options/options.html",
"open_in_tab": true
}
}
⚠️ 注意:
options_ui和options_page二选一。推荐使用options_ui,它支持open_in_tab参数。
6.2 Options 页面结构
HTML 模板
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>扩展设置</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<div class="options-container">
<header class="options-header">
<h1>⚙️ 扩展设置</h1>
<p class="subtitle">自定义您的扩展体验</p>
</header>
<main class="options-main">
<!-- 通知设置 -->
<section class="setting-section">
<h2>通知</h2>
<div class="setting-item">
<div class="setting-info">
<label for="enableNotifications">启用通知</label>
<p class="setting-desc">接收重要更新和提醒</p>
</div>
<label class="toggle-switch">
<input type="checkbox" id="enableNotifications" checked>
<span class="slider"></span>
</label>
</div>
</section>
<!-- 外观设置 -->
<section class="setting-section">
<h2>外观</h2>
<div class="setting-item">
<div class="setting-info">
<label for="theme">主题</label>
<p class="setting-desc">选择扩展的显示主题</p>
</div>
<select id="theme" class="setting-select">
<option value="system">跟随系统</option>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
</section>
<!-- 数据管理 -->
<section class="setting-section">
<h2>数据</h2>
<div class="setting-item">
<div class="setting-info">
<label>导出数据</label>
<p class="setting-desc">将您的配置和数据导出为 JSON 文件</p>
</div>
<button id="exportBtn" class="btn btn-secondary">导出</button>
</div>
<div class="setting-item">
<div class="setting-info">
<label>导入数据</label>
<p class="setting-desc">从 JSON 文件恢复配置和数据</label>
</p>
</div>
<button id="importBtn" class="btn btn-secondary">导入</button>
<input type="file" id="importFile" accept=".json" hidden>
</div>
<div class="setting-item danger-zone">
<div class="setting-info">
<label>重置设置</label>
<p class="setting-desc">恢复所有设置为默认值</p>
</div>
<button id="resetBtn" class="btn btn-danger">重置</button>
</div>
</section>
</main>
<!-- 保存提示 -->
<div id="toast" class="toast hidden">设置已保存</div>
</div>
<script src="options.js"></script>
</body>
</html>
CSS 样式
/* options/options.css */
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--primary: #4285f4;
--danger: #ea4335;
--success: #34a853;
--bg: #f8f9fa;
--card-bg: #ffffff;
--text: #202124;
--text-secondary: #5f6368;
--border: #dadce0;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
.options-container {
max-width: 720px;
margin: 0 auto;
padding: 24px;
}
.options-header {
margin-bottom: 32px;
}
.options-header h1 {
font-size: 28px;
font-weight: 600;
}
.subtitle {
color: var(--text-secondary);
margin-top: 4px;
}
/* 设置区块 */
.setting-section {
background: var(--card-bg);
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.setting-section h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f0;
}
.setting-item:last-child {
border-bottom: none;
}
.setting-info label {
font-weight: 500;
cursor: pointer;
}
.setting-desc {
font-size: 13px;
color: var(--text-secondary);
margin-top: 2px;
}
/* Toggle Switch */
.toggle-switch {
position: relative;
width: 48px;
height: 26px;
cursor: pointer;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
inset: 0;
background: #ccc;
border-radius: 26px;
transition: 0.3s;
}
.slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
left: 3px;
bottom: 3px;
background: white;
border-radius: 50%;
transition: 0.3s;
}
.toggle-switch input:checked + .slider {
background: var(--primary);
}
.toggle-switch input:checked + .slider::before {
transform: translateX(22px);
}
/* Select */
.setting-select {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 14px;
background: white;
cursor: pointer;
}
/* Buttons */
.btn {
padding: 8px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary {
background: var(--bg);
color: var(--text);
border: 1px solid var(--border);
}
.btn-secondary:hover {
background: #e8eaed;
}
.btn-danger {
background: #fce8e6;
color: var(--danger);
}
.btn-danger:hover {
background: #f8d7da;
}
.danger-zone {
background: #fff8f8;
margin: 0 -24px -24px;
padding: 16px 24px;
border-radius: 0 0 12px 12px;
}
/* Toast */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #323232;
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
transition: opacity 0.3s;
}
.hidden { display: none; }
@media (prefers-color-scheme: dark) {
:root {
--bg: #202124;
--card-bg: #292a2d;
--text: #e8eaed;
--text-secondary: #9aa0a6;
--border: #5f6368;
}
}
6.3 Options JavaScript 逻辑
6.3.1 设置管理类
// options/options.js
class SettingsManager {
constructor() {
this.defaults = {
enableNotifications: true,
theme: 'system',
autoSync: false,
syncInterval: 60,
language: 'zh-CN',
maxHistoryItems: 100,
exportFormat: 'json'
};
this.settings = {};
this.init();
}
async init() {
await this.loadSettings();
this.renderSettings();
this.bindEvents();
}
async loadSettings() {
const stored = await chrome.storage.sync.get('settings');
this.settings = { ...this.defaults, ...stored.settings };
}
async saveSettings() {
await chrome.storage.sync.set({ settings: this.settings });
this.showToast('设置已保存');
}
renderSettings() {
// 遍历设置项并更新 UI
Object.entries(this.settings).forEach(([key, value]) => {
const element = document.getElementById(key);
if (!element) return;
if (element.type === 'checkbox') {
element.checked = value;
} else if (element.tagName === 'SELECT' ||
element.tagName === 'INPUT') {
element.value = value;
}
});
}
bindEvents() {
// 自动保存:开关和选择器
document.querySelectorAll('input[type="checkbox"], select')
.forEach(element => {
element.addEventListener('change', (e) => {
const key = e.target.id;
const value = e.target.type === 'checkbox'
? e.target.checked
: e.target.value;
this.settings[key] = value;
this.saveSettings();
});
});
// 数字输入延迟保存
document.querySelectorAll('input[type="number"]')
.forEach(element => {
let timeout;
element.addEventListener('input', (e) => {
clearTimeout(timeout);
timeout = setTimeout(() => {
this.settings[e.target.id] = Number(e.target.value);
this.saveSettings();
}, 500);
});
});
// 导出数据
document.getElementById('exportBtn')
.addEventListener('click', () => this.exportData());
// 导入数据
document.getElementById('importBtn')
.addEventListener('click', () => {
document.getElementById('importFile').click();
});
document.getElementById('importFile')
.addEventListener('change', (e) => this.importData(e));
// 重置设置
document.getElementById('resetBtn')
.addEventListener('click', () => this.resetSettings());
}
async exportData() {
const allData = await chrome.storage.local.get(null);
const exportData = {
version: chrome.runtime.getManifest().version,
exportedAt: new Date().toISOString(),
settings: this.settings,
data: allData
};
const blob = new Blob(
[JSON.stringify(exportData, null, 2)],
{ type: 'application/json' }
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `extension-backup-${Date.now()}.json`;
a.click();
URL.revokeObjectURL(url);
this.showToast('数据已导出');
}
async importData(event) {
const file = event.target.files[0];
if (!file) return;
try {
const text = await file.text();
const imported = JSON.parse(text);
if (imported.settings) {
this.settings = { ...this.defaults, ...imported.settings };
await chrome.storage.sync.set({ settings: this.settings });
}
if (imported.data) {
await chrome.storage.local.set(imported.data);
}
this.renderSettings();
this.showToast('数据已导入');
} catch (error) {
this.showToast('导入失败: 文件格式错误');
}
}
async resetSettings() {
if (!confirm('确定要重置所有设置吗?此操作不可恢复。')) {
return;
}
this.settings = { ...this.defaults };
await chrome.storage.sync.set({ settings: this.settings });
this.renderSettings();
this.showToast('设置已重置');
}
showToast(message) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.classList.remove('hidden');
setTimeout(() => {
toast.classList.add('hidden');
}, 2500);
}
}
// 启动
document.addEventListener('DOMContentLoaded', () => {
new SettingsManager();
});
6.4 从 Popup 打开 Options
// 方式一:使用 chrome.runtime.openOptionsPage()
document.getElementById('settingsBtn').addEventListener('click', () => {
chrome.runtime.openOptionsPage();
});
// 方式二:直接打开 URL
document.getElementById('settingsBtn').addEventListener('click', () => {
chrome.tabs.create({
url: chrome.runtime.getURL('options/options.html')
});
});
// Service Worker 中也可以打开
// chrome.runtime.openOptionsPage();
6.5 Settings 同步
跨设备同步
// 使用 chrome.storage.sync 实现跨设备同步
class SyncSettings {
constructor() {
this.defaults = { theme: 'system', fontSize: 14 };
}
async load() {
const result = await chrome.storage.sync.get('settings');
return { ...this.defaults, ...result.settings };
}
async save(settings) {
await chrome.storage.sync.set({ settings });
// sync 存储自动同步到同一 Google 账号的其他设备
}
// 监听其他设备的变更
onChange(callback) {
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.settings) {
callback(changes.settings.newValue, changes.settings.oldValue);
}
});
}
}
// Service Worker 中监听设置变更
const sync = new SyncSettings();
sync.onChange((newSettings, oldSettings) => {
console.log('设置已更新:', newSettings);
// 应用新设置
applySettings(newSettings);
});
6.6 注意事项
| 问题 | 说明 | 解决方案 |
|---|---|---|
| 设置不生效 | Popup 未重新加载 | 关闭后重新打开 Popup |
| 同步延迟 | sync 存储有写入频率限制 | 每分钟最多 120 次写入 |
| 存储超限 | sync 存储每项 8KB,总量 100KB | 使用 local 存储大数据 |
| Open in Tab 不生效 | 需要 open_in_tab: true | 检查 options_ui 配置 |