Chrome 扩展开发完全指南 / 第 12 章:右键菜单(Context Menus)
右键菜单(Context Menu)是扩展融入浏览器原生体验的最佳方式之一。用户在网页、图片、链接或选中文本上右键时,可以直接看到和使用扩展的功能。
权限与声明
{
"permissions": ["contextMenus"]
}
创建菜单项
// Service Worker 中创建菜单(在 onInstalled 事件中)
chrome.runtime.onInstalled.addListener(() => {
// 基本菜单项
chrome.contextMenus.create({
id: 'searchSelection',
title: '搜索 "%s"',
contexts: ['selection'] // 选中文本时显示
});
// 带图标的菜单项
chrome.contextMenus.create({
id: 'saveImage',
title: '保存图片到收藏',
contexts: ['image'],
icons: {
16: 'icons/save-16.png',
32: 'icons/save-32.png'
}
});
});
上下文类型
| contexts 值 | 显示条件 | %s 替换为 |
|---|
all | 所有情况 | — |
page | 页面空白区域 | — |
frame | iframe 上 | — |
selection | 选中文本 | 选中的文本 |
link | 链接上 | 链接 URL |
editable | 可编辑区域 | — |
image | 图片上 | 图片 URL |
video | 视频上 | 视频 URL |
audio | 音频上 | 音频 URL |
launcher | Chrome 启动器 | — |
browser_action | 扩展图标上 | — |
page_action | 页面操作图标上 | — |
action | 扩展 Action 上 | — |
12.2 菜单结构
12.2.1 多层级菜单
chrome.runtime.onInstalled.addListener(() => {
// 父级菜单
chrome.contextMenus.create({
id: 'myExtension',
title: '我的扩展',
contexts: ['all']
});
// 子菜单 — 通过 parentId 关联
chrome.contextMenus.create({
id: 'translate',
parentId: 'myExtension',
title: '翻译选中文本',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'highlight',
parentId: 'myExtension',
title: '高亮标记',
contexts: ['selection']
});
// 分隔线
chrome.contextMenus.create({
id: 'separator1',
parentId: 'myExtension',
type: 'separator',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'saveNote',
parentId: 'myExtension',
title: '保存为笔记',
contexts: ['selection']
});
// 三级菜单
chrome.contextMenus.create({
id: 'shareTo',
parentId: 'myExtension',
title: '分享到...',
contexts: ['page', 'link']
});
chrome.contextMenus.create({
id: 'shareTwitter',
parentId: 'shareTo',
title: 'Twitter',
contexts: ['page', 'link']
});
chrome.contextMenus.create({
id: 'shareWeibo',
parentId: 'shareTo',
title: '微博',
contexts: ['page', 'link']
});
});
菜单结构:
我的扩展
├── 翻译选中文本
├── 高亮标记
├── ─────────
├── 保存为笔记
└── 分享到...
├── Twitter
└── 微博
12.2.2 菜单项类型
| type 值 | 说明 |
|---|
normal | 默认,普通菜单项 |
checkbox | 复选框菜单项 |
radio | 单选按钮菜单项 |
separator | 分隔线 |
// 复选框菜单项
chrome.contextMenus.create({
id: 'autoTranslate',
title: '自动翻译',
type: 'checkbox',
checked: false,
contexts: ['all']
});
// 单选按钮组
['zh-CN', 'en', 'ja', 'ko'].forEach((lang, i) => {
chrome.contextMenus.create({
id: `lang_${lang}`,
title: lang,
type: 'radio',
checked: i === 0,
contexts: ['all']
});
});
12.3 处理菜单点击
chrome.contextMenus.onClicked.addListener((info, tab) => {
// info 对象包含:
// - menuItemId: 菜单项 ID
// - parentMenuItemId: 父菜单项 ID
// - mediaType: 媒体类型
// - linkUrl: 链接 URL
// - srcUrl: 图片/媒体源 URL
// - pageUrl: 页面 URL
// - frameUrl: iframe URL
// - selectionText: 选中的文本
// - editable: 是否在可编辑区域
// - checked: checkbox/radio 的选中状态
// - wasChecked: 修改前的选中状态
switch (info.menuItemId) {
case 'searchSelection':
chrome.tabs.create({
url: `https://www.google.com/search?q=${encodeURIComponent(info.selectionText)}`
});
break;
case 'translate':
translateText(info.selectionText, tab.id);
break;
case 'highlight':
chrome.tabs.sendMessage(tab.id, {
type: 'HIGHLIGHT',
text: info.selectionText
});
break;
case 'saveImage':
saveImageToCollection(info.srcUrl);
break;
case 'autoTranslate':
toggleAutoTranslate(info.checked);
break;
case 'lang_zh-CN':
case 'lang_en':
case 'lang_ja':
case 'lang_ko':
const lang = info.menuItemId.replace('lang_', '');
setTargetLanguage(lang);
break;
}
});
12.4 动态管理菜单
class ContextMenuManager {
constructor() {
this.menus = new Map();
}
create(config) {
chrome.contextMenus.create(config);
this.menus.set(config.id, config);
}
remove(id) {
chrome.contextMenus.remove(id);
this.menus.delete(id);
}
update(id, properties) {
chrome.contextMenus.update(id, properties);
if (this.menus.has(id)) {
Object.assign(this.menus.get(id), properties);
}
}
removeAll() {
chrome.contextMenus.removeAll();
this.menus.clear();
}
// 根据页面类型动态显示/隐藏
async updateForPage(url) {
const isGitHub = url?.includes('github.com');
const isYouTube = url?.includes('youtube.com');
// GitHub 专属菜单
if (isGitHub && !this.menus.has('ghClone')) {
this.create({
id: 'ghClone',
title: '一键克隆仓库',
contexts: ['link'],
targetUrlPatterns: ['*://github.com/*/*']
});
} else if (!isGitHub && this.menus.has('ghClone')) {
this.remove('ghClone');
}
// YouTube 专属菜单
if (isYouTube && !this.menus.has('ytDownload')) {
this.create({
id: 'ytDownload',
title: '下载字幕',
contexts: ['page'],
documentUrlPatterns: ['*://www.youtube.com/watch*']
});
} else if (!isYouTube && this.menus.has('ytDownload')) {
this.remove('ytDownload');
}
}
}
// 监听标签页变化,动态更新菜单
const menuManager = new ContextMenuManager();
chrome.tabs.onActivated.addListener(async (info) => {
const tab = await chrome.tabs.get(info.tabId);
menuManager.updateForPage(tab.url);
});
12.5 菜单与 Content Script 协作
// 选中文本后弹出自定义操作
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId === 'smartLookup') {
// 向 Content Script 发送查询结果
try {
const result = await lookupTerm(info.selectionText);
await chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_POPUP',
data: {
term: info.selectionText,
definitions: result.definitions,
examples: result.examples
}
});
} catch (error) {
await chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_ERROR',
message: `查询失败: ${error.message}`
});
}
}
});
12.6 业务场景
场景一:网页翻译工具
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'extParent',
title: '翻译工具',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'toChinese',
parentId: 'extParent',
title: '翻译为中文',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'toEnglish',
parentId: 'extParent',
title: '翻译为英文',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'addToGlossary',
parentId: 'extParent',
title: '添加到词汇表',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'sep1',
parentId: 'extParent',
type: 'separator',
contexts: ['selection']
});
chrome.contextMenus.create({
id: 'autoDetect',
parentId: 'extParent',
title: '自动检测语言',
type: 'checkbox',
checked: true,
contexts: ['selection']
});
});
场景二:图片管理工具
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: 'imgManager',
title: '图片管理',
contexts: ['image']
});
chrome.contextMenus.create({
id: 'imgSave',
parentId: 'imgManager',
title: '保存到收藏',
contexts: ['image']
});
chrome.contextMenus.create({
id: 'imgCopy',
parentId: 'imgManager',
title: '复制图片链接',
contexts: ['image']
});
chrome.contextMenus.create({
id: 'imgInfo',
parentId: 'imgManager',
title: '查看图片信息',
contexts: ['image']
});
chrome.contextMenus.create({
id: 'imgReverse',
parentId: 'imgManager',
title: '以图搜图',
contexts: ['image']
});
});
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
switch (info.menuItemId) {
case 'imgReverse':
const searchUrl = `https://lens.google.com/uploadbyurl?url=${encodeURIComponent(info.srcUrl)}`;
chrome.tabs.create({ url: searchUrl });
break;
case 'imgCopy':
await chrome.tabs.sendMessage(tab.id, {
type: 'COPY_TO_CLIPBOARD',
text: info.srcUrl
});
break;
case 'imgInfo':
await chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_IMAGE_INFO',
imageUrl: info.srcUrl
});
break;
}
});
12.7 注意事项
| 问题 | 说明 | 解决方案 |
|---|
| 菜单项不显示 | contexts 未匹配 | 检查上下文类型是否正确 |
| 菜单项重复 | create 被多次调用 | 使用唯一 id,或先检查是否存在 |
%s 不替换 | 非 selection 上下文 | %s 仅在 selection 上下文可用 |
| 更新不生效 | 需要重载扩展 | 修改后重新加载扩展 |
| 多级菜单太多 | 影响用户体验 | 最多 2-3 层 |
12.8 扩展阅读