浏览器插件开发必备概念——用户交互

2,636 阅读13分钟

系列文章可以查看《浏览器扩展程序开发笔记》专栏


Action

参考:

UI-Action.png

该交互控件是指在浏览器工具栏的扩展程序图标,供用户点击,可以执行预定的操作。它也可以作为唤起其他交互控件的入口,例如通过适当的配置,在悬停时可以显示提示框 tooltip,在点击时可以弹出弹出框 popup

💡 在 MV2 版本中有 API chrome.browserAction(浏览器级别的按钮,对所有页面都可以响应)和 API chrome.pageAction(针对特定匹配条件的页面才会响应);而在 MV3 版本中新增了 chorme.action API,它的功能有点像是 MV2 的 Browser Action,如果希望实现类似于 Page Action 的功能,可以参考官方示例

  • 如果要使用该控件,需要先在配置清单 manifest.json 的选项 action 中进行声明注册,例如指定图标文件的相对路径,如果使用了 popup 要指定弹出框的 HTML 文件。

    {
      // ...
      "action": {
        // 图标文件
        "default_icon": {              // optional
          "16": "images/icon16.png",   // optional
          "24": "images/icon24.png",   // optional
          "32": "images/icon32.png"    // optional
        },
        // 提示框
        "default_title": "Click Me",   // optional, shown in tooltip
        // 弹出框
        "default_popup": "popup.html"  // optional
      },
    }
    

    💡 即使扩展程序的配置清单中没有设置 action 选项,它也可以显示在浏览器工具栏上,会使用一个默认图标,一般以背景色为灰底且包含扩展程序的首字母的图标指代。

  • 图标

    icon 图标的默认高和宽都是 16 DIPs(device-independent pixels),推荐预设多个尺寸的图标,浏览器会使用恰当的文件。图标文件的格式需要是 Blink 渲染引擎所支持的,例如 PNG、JPEG、BMP、ICO 等,而 SVG 是支持的。如果是解压的扩展程序,则只支持 PNG 格式。

    除了在配置清单 manifest.json 的选项 action.default_icon 中提供固定的文件,还有通过编程的方式,使用方法 chrome.action.setIcon() 设置图标,可以根据条件设置不同的图片路径,也可以使用 canvas 创建图片。⚠️ 该方法是用于设置静态图片,而不应该用于设置动图。

    const canvas = new OffscreenCanvas(16, 16);
    const context = canvas.getContext('2d');
    context.clearRect(0, 0, 16, 16);
    context.fillStyle = '#00FF00';  // Green
    context.fillRect(0, 0, 16, 16);
    const imageData = context.getImageData(0, 0, 16, 16);
    chrome.action.setIcon({imageData: imageData}, () => { /* ... */ });
    
  • 提示框

    tooltip 是在用户悬停在图标时显示的提示框,可以设置一段简短的文字,用以提示该扩展程序的名称。

    💡 当图标按钮被聚焦时,它可以被屏幕阅读软件识别,可以增强扩展程序的可及性。

    除了在配置清单 manifest.json 的选项 action.default_title 中设置文字,还可以使用方法 chrome.action.setTitle() 进行设置。

  • 弹出框

    popup 是在用户点击图标时显示的弹出框,它实际上是一个大小受到限制(高和宽的最小值是 25px,高的最大值是 600px,宽的最大值是 800px)的 HTML 页面,默认大小是基于内容的。

    除了在配置清单 manifest.json 的选项 action.default_popup 中设置 popup 页面,还可以使用方法 chrome.action.setPopup() 动态更新弹出框所指向的 HTML 文件的路径。

  • 标记

    badge 是一个添加到图标上的文字,并允许设置背景色,一般用以显示扩展程序的状态,例如更新了新版本可以显示 new,如果扩展程序带统计功能则可以形式数值等。

    ⚠️ 由于标记的空间有限,所以一般只能显示 4 个或以下的字符

    通过方法 chrome.action.setBadgeText() 设置标记文字;通过chrome.action.setBadgeBackgroundColor() 设置标记的背景色

    chrome.action.setBadgeBackgroundColor(
      {color: [0, 255, 0, 0]},  // Green
      () => { /* ... */ },
    );
    
    chrome.action.setBadgeBackgroundColor(
      {color: '#00FF00',  // Also green
      () => { /* ... */ },
    );
    
    chrome.action.setBadgeBackgroundColor(
      {color: 'green'},  // Also, also green
      () => { /* ... */ },
    );
    
  • action 在每一个标签页都可以有不同的状态,例如可以针对不同的标签页,设置不同 badge 内容

    function getTabId() { /* ... */}
    function getTabBadge() { /* ... */}
    
    chrome.action.setBadgeText(
      {
        text: getTabBadge(tabId),
        tabId: getTabId(),
      },
      () => { ... }
    );
    

    💡 如果方法 setBadgeText 第一个参数的选项 tabId 省略了,则该标记会作为全局设置,而提供了 tabId 则针对特定的标签页,优先级更高,会覆盖全局设置的标记文字。

  • 默认所有标签页下,action 图标都是可以响应点击的 clickable,可以通过方法 chrome.action.enable()chrome.action.disable() 来手动控制 action 的响应状态,这会影响 popup 的显示或 chrome.action.onClicked 所监听的相应事件的分发。

Context Menu

参考:

UI-Context-Menu.png

该交互控件是指浏览器的右键菜单,可以通过扩展程序往其中添加选项,除了可以针对整个页面,还可以针对特定的 DOM 元素,或 action 图标,添加右键菜单项。

  • 如果要使用该控件,需要先在配置清单 manifest.json 的选项 permissions 中进行声明注册。为了便于将菜单选项与扩展程序相匹配,需要在配置清单的选项 icons 中指定图标文件,最好提供多种尺寸的图片文件。

    {
      // ...
      "permissions": [
        "contextMenus"
      ],
      "icons": {
        "16": "icon-bitty.png",
        "48": "icon-small.png",
        "128": "icon-large.png"
      },
    }
    
  • 使用方法 chrome.contextMenus.create({}, callback()) 为扩展程序创建专属的菜单项,它可以接收两个入参,第一个参数是配置对象,第二个参数是回调函数。返回值该菜单选项的唯一 ID 值。

    配置对象有多个选项,常用如下:

    • id 为当前菜单选项分配一个唯一 ID
    • title必须,除非该菜单选项的类型是分割线 type: "separator")菜单选项的内容
    • type 菜单选项的类型,默认值是 normal,就是正常的菜单选项,还可以是 checkboxradioseparator
    • contexts 数组,限制菜单选项出现在对页面的哪个元素进行右键点击时,默认值是 ['page'],即在整个网页任何地方右键点击时,该菜单选项都显示在菜单中
    • parentId 当前菜单选项的父级菜单的 ID
    • onclick 监听菜单选项的点击事件和事件处理函数,当菜单选项被点击时执行该事件处理函数,会有两个入参 info(该菜单选项的信息)和 tab(当前标签页的信息)传入

    (可选)回到函数是在用户右键点击,该菜单选项被创建时所执行的

  • 使用方法 chrome.contextMenus.update(menuItemId, {}, callback()) 更新给定菜单选项,第一个参数是菜单选项的唯一 ID 值,第二个参数是配置对象(和方法 chrome.contextMenus.create() 的配置对象可使用的选项一样),第三个(可选)参数是回调函数,在更新完菜单选项后执行。

💡 浏览器的右键菜单是全局的,可以出现在任何页面中,甚至是 file://chrome://URLs 的页面,如果希望控制菜单选项出现在指定的页面,你可以在创建 create() 或更新 update() 菜单选项时,通过配置对象的选项 documentUrlPatterns 来限制只能在特定的 URL 页面或 <iframe> 中,右键点击才显示相应的菜单项。

  • 使用方法 chrome.contextMenus.remove(menuItemId, callback()) 动态删除已创建的菜单选项。(可选)回调函数在删除指定的菜单选项后执行。如果想删除所有该扩展程序创建的菜单选项,可以使用方法 chrome.contextMenus.removeAll(callback()) 其(可选)回调函数会在删除菜单选项后执行。
  • 可以创建多个菜单选项,但是如果选项多于一个,浏览器会自动将它们收纳到一个次级菜单
  • 使用方法 chrome.contextMenus.onClicked(callback()) 监听该扩展程序菜单选项的点击事件,并执行相应的事件处理函数,该函数接收两个入参 info 被点击的菜单选项的相关信息,tab 标签页相关信息。

Omnibox

参考:

UI-Omnibox.png

该交互控件是指地址栏上的搜索关键词,当用户在地址栏输入相应的关键词,就会触发 Omnibox(唤起相应的扩展程序),接下来用户在地址栏中输入内容是直接与该扩展程序进行交互。一般会在扩展程序中预先设置一系列的搜索建议,当用户输入的内容模糊匹配成功时,就会在一个 dropdown 中显示相应的搜索建议。

  • 如果要使用该控件,需要先在配置清单 manifest.json 的选项 omnibox 中进行声明注册。当 Omnibox 被触发时,扩展程序图标(以灰阶的形式展示)和名称会显示在地址栏的左侧,为了便于识别,需要在配置清单的选项 icons 中指定图标文件,最好提供多种尺寸的图片文件(默认使用高和宽为 16px 的图标)。

    该控件的交互逻辑在后台脚本的 service worker 中设置(需要在配置清单 manifest.json 的选项 background.service_worker 中声明注册),基于事件监听-响应的原理。

    {
      // ...
      "background": {
        "service_worker": "background.js"
      },
      "omnibox": { "keyword" : "demo" },
      "icons": {
        "16": "icon-bitty.png",
        "48": "icon-small.png",
        "128": "icon-large.png"
      },
    }
    

    以上示例将扩展程序的 Omnibox 触发关键词设定为 demo,当用户在地址栏输入 demo 时,会在下拉框显示一个扩展程序名称的选项,可以点击该选项,或按 Tab 键,或键入空格 Space,即可触发 Omnibox。

    UI-Omnibox-trigger.png

  • 常用于监听事件的 API 如下:

    • onInputChanged 进入 Omnibox 模式后,当用户在地址栏中输入内容时会触发。

      chrome.omnibox.onInputChanged.addListener((text, suggest) => {
        if(!text) return;
        suggest([
            {
                content: text,
                description: `search for ${text}`
            }
        ])
      });
      

      事件处理函数中接收两个入参,第一个参数 text 是用户输入的内容(字符串),第二个参数 suggest 是一个方法,它接收一个数组,包含一系列的建议结果 SuggestResult,该方法会将这些建议选项传递回浏览器,显示在地址栏的下拉框中。

      💡 建议结果 SuggestResult 是一个对象,以供用户选择,包括以下属性:

      • content 实际上会输入到地址栏的内容,当用户选中该建议选项时,会传递给扩展程序的内容
      • deletable 该建议选项是否可以让用户删除
      • description 描述内容,显示在地址栏的下拉框中,可以包含 XML 风格的样式修饰。但是不能包含 5 种 XML 转义字符

      💡 可以使用方法 chrome.omnibox.setDefaultSuggestion(suggestion) 设置默认的建议选项,该方法接收的入参是一个不完整 suggestionResult 对象,没有 content 属性,其作用类似于输入框中的 placeholder。当触发了 Omnibox 时,在用户还没输入内容时,该默认建议就会出现在地址栏的下拉框中第一条的位置。

      ⚠️ 根据一个 Bug 报告,由于 Omnibox 的搜索建议内容支持 XML,所以需要调用 DomParser,但是后台脚本在 MV3 版本迁移改用了 service worker,该运行环境并没有 DomParser,所以会导致报错,且无法正常显示搜索建议选项。

    • onInputEntered 在用户选择执行一个建议选项后,触发回调函数。

      chrome.omnibox.onInputEntered.addListener((text, disposition) => {
        if(!text) return;
        console.log('inputEntered: ' + text);
        // Encode user input for special characters , / ? : @ & = + $ #
        var newURL = 'https://www.google.com/search?q=' + encodeURIComponent(text);
        chrome.tabs.create({ url: newURL });
        console.log(disposition)
      });
      

      事件处理函数接收两个参数,第一个参数 text 是输入到地址栏的内容,第二个参数 disposition 是进行搜寻时窗口的设置,有三种可能的结果:

      • currentTab 在当前标签页进行搜寻

      • newForegroundTab 新建一个标签页进行搜寻,同时切换到该标签页

      • newBackgroundTab 新建一个标签页进行搜寻,但不进行标签页的切换

      以上示例调用了 chrome.tabs.create() 方法,在当前标签页进行搜索,所以终端打印的值是 currentTab

Override Pages

参考:

UI-Override-Page-NewTab.png

该交互控件是通过覆写页面实现的,扩展程序可以覆写三个 Chrome 的特殊页面:

  • Bookmark Manager:书签管理页面 chrome://bookmarks
  • History:历史记录页面 chrome://history
  • New Tab:新建标签页 chrome://newtab

💡 每一个扩展程序只能覆写以上三个特殊 Chrome 页面之,而每一种特殊 Chrome 页面,只能选择被一个扩展程序进行覆写。此外隐身模式下,新建标签页不能被覆写。

  • 如果要使用该控件,需要先在配置清单 manifest.json 的选项 chrome_url_overrides 中进行声明注册,将需要覆写的页面作为属性,bookmarkshistorynewtab 三者之一,属性值就是用以替换的 HTML 页面文档的相对路径。

    {
      // ...
      "chrome_url_overrides" : {
        "newTab": "newPage.html"
      },
      ...
    }
    
  • 为了提供更好的用户体验,用以替换的页面应该遵循以下指引:

    • 页面文件大小应该较小,便于快速加载显示,避免使用同步访问网络或数据库资源,导致渲染阻塞。
    • 包含明确的信息,告知用户当前浏览的是 Chrome 的特殊页面。
    • 不要在新建标签页使用输入框聚焦功能,因为新建页面时,标签页的地址栏会首先获取焦点。

Commands

参考:

该交互控件是指使用快捷键操作扩展程序,可以通过快捷键激活 action 或执行特定的命令。

💡 所有扩展程序的快捷键都在 chrome://extensions/shortcuts 中显示,用户可以在其中修改快捷键的组合值,或快捷键的局部与全局适用性

UI-Commands-Shortcuts-Setting.png

  • 如果要使用该控件,需要先将这些快捷键在配置清单 manifest.json 的选项 commands 中进行声明注册,该选项的值是一个对象,属性名是一个描述命令的名称,属性值是关于快捷键定义的对象,一般有两个选项:

    • suggested_key(可选)声明默认的快捷键,其值可以是一个表示(跨平台适用的)快捷键的字符串,或是一个对象,可以针对不同的系统平台设定不同的快捷键,其中系统平台支持 defaultchromeoslinusmacwindows。如果该选项省略,则该命令没有相应的触发快捷键,等待用户来设定后再生效
    • description 一段告知用户该快捷键功能作用的字符串,它会显示在扩展程序的快捷键管理界面中 chrome://extensions/shortcuts,对于标准快捷键 standard commands 它是必须的,对于 action commands 是可选的。
    {
      // ...
      "commands": {
        "run-foo": {
          "suggested_key": {
            "default": "Ctrl+Shift+Y",
            "mac": "Command+Shift+Y"
          },
          "description": "Run \"foo\" on the current page."
        },
        "_execute_action": {
          "suggested_key": {
            "windows": "Ctrl+Shift+Y",
            "mac": "Command+Shift+Y",
            "chromeos": "Ctrl+Shift+U",
            "linux": "Ctrl+Shift+J"
          }
        }
      },
    }
    

    💡 其中 _execute_action 是保留属性,用以定义激活 action 的快捷键(对于 MV2 版本,则有 _execute_browser_action_execute_page_action 保留属性,分别设定激活 browser action 和 page action 的快捷键),对于激活 action 的快捷键无法**触command.onCommand 事件。

    💡 当新安装的扩展程序的快捷键默认值,与已安装的其他扩展程序的快捷键冲突时,对于后来安装的扩展程序,浏览器就不会再注册这些快捷键,避免覆盖之前已存在的快捷键。为了避免让用户觉得快捷键「无故失灵」的现象,我们应该采用以下更健壮的方法,在安装扩展程序时 chrome.runtime.onInstalled.addListener 先进行检查快捷键冲突,并告知用户

    // background.js
    
    // Only use this function during the initial install phase. After
    // installation the user may have intentionally unassigned commands.
    chrome.runtime.onInstalled.addListener((reason) => {
      if (reason === chrome.runtime.OnInstalledReason.INSTALL) {
        checkCommandShortcuts();
      }
    });
    
    function checkCommandShortcuts() {
      // 获取当前插件已注册的快捷键
      chrome.commands.getAll((commands) => {
        let missingShortcuts = [];
    
        for (let {name, shortcut} of commands) {
          // 如果冲突无法注册快捷键默认值,则 shortcut 值为空字符串
          if (shortcut === '') {
            missingShortcuts.push(name);
          }
        }
        
        // 如果该扩展程序与其他扩展程序真的存在快捷键冲突
        if (missingShortcuts.length > 0) {
          // Update the extension UI to inform the user that one or more
          // commands are currently unassigned.
        }
      });
    }
    
  • 快捷键必须包含 CtrlAlt 两者之一,对大小写敏感,支持使用以下按键组合为快捷键:

    • 字母键 A-Z
    • 数字键 0~9
    • 标准的功能键
      • 通用键 Comma, Period, Home, End, PageUp, PageDown, Space, Insert, Delete
      • 方向键 Up, Down, Left, Right
    • 修饰键 Ctrl, Alt, Shift, MacCtrl (macOS only), Command (macOS only), Search (Chrome OS only)

    💡 不支持 Tab 键,媒体键能与修饰键组合。

  • 为了响应快捷键,需要在后台脚本中使用 chrome.commands.onCommand.addListener API 进行监听

    chrome.commands.onCommand.addListener((command) => {
      console.log(`Command: ${command}`);
    });
    

    💡 和标准的快捷键 standard commands 不同,对于激活 action 的快捷键,无法通过以上方法进行监听。可以在弹出框 popup 的脚本文件中,监听 DOMContentLoaded 事件来替代。

  • 默认情况下,注册的快捷键是 Chrome 浏览器的局部快捷键(当浏览器是当前系统的激活应用时,按下快捷键,扩展程序才响应),也可以在快捷键定义对象中,通过选项 global: true 设定为全局快捷键。建议注册全局快捷键限制在 Ctrl+Shift+[0..9] 范围中,以避免覆盖掉其他系统级别的快捷键。

    {
      // ...
      "commands": {
        "toggle-feature-foo": {
          "suggested_key": {
            "default": "Ctrl+Shift+5"
          },
          "description": "Toggle feature foo",
          "global": true
        }
      },
    }
    

    💡 但是 Chrome OS 不支持扩展程序设定全局快捷键