强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 12 章:右键菜单(Context Menus)

第 12 章:右键菜单(Context Menus)

右键菜单(Context Menu)是扩展融入浏览器原生体验的最佳方式之一。用户在网页、图片、链接或选中文本上右键时,可以直接看到和使用扩展的功能。


12.1 Context Menus 基础

权限与声明

{
  "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页面空白区域
frameiframe 上
selection选中文本选中的文本
link链接上链接 URL
editable可编辑区域
image图片上图片 URL
video视频上视频 URL
audio音频上音频 URL
launcherChrome 启动器
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 扩展阅读