浏览器拓展V3指南-看这篇就够了

avatar
Ctrl+C、V工程师 @豌豆公主

背景

实现一个需求,有很多的方案。但是我发现我从来没有用过浏览器拓展去实现过什么,所以打算‘作’一次,体验一下通过浏览器拓展来批量导出公司后台的商品图。目前版本我用的是V3,与V2还是有一些不同的。

什么是浏览器拓展

  • 拓展不同于组件,却比组件更加紧密。
  • 用户可以通过自己的行为和需求增加浏览器的功能。
  • 用户可以通过熟悉的HTML、CSS、JavaScript来创建拓展。
  • 虽然有很多的API,但是浏览器不会每个都主动开放,按需引用。

通俗一点说: 拓展其实是运行在浏览器的程序,可以让你的浏览器功能更加丰富。


还是不知道什么是拓展?请看图:

这个总看到过吧...这个就是浏览器的拓展。你也可以通过 chrome://extensios 访问查看已安装的拓展。

浏览器拓展的组成

拓展的组成比想象中的多,主要有如下几块:

  • manifest.json
  • background scripts
  • content scripts
  • an options page
  • UI elements

简单的按照进程区分,当然有一些会比较笼统,但是这么区分完之后对于他们之间的通信是很方便的。

浏览器拓展主要模块解析

manifest.json

每一个拓展在创建的时候都需要配置一套manifest.json,这里边包含着,权限,安全,页面配置等等。和我们的packege.json类似。新版本的配置有不少东西是需要注意的,因为从v2到v3的升级有不少的改动。官方给了一个Demo,大家可以看看,其实如果自用可以不用配置的那么麻烦,很多东西我也还是在摸索中。

{
  // Required
  "manifest_version": 3,
  "name": "My Extension",
  "version": "versionString",

  // Recommended
  "action": {
    "default_icon": {       
      "16": "icon16.png",   
      "24": "icon24.png",   
      "32": "icon32.png"    
    },
    "default_title": "Click Me",   
    "default_popup": "popup.html"  
  },
  "default_locale": "en",
  "description": "A plain text description",
  "icons": { // action中的icon是在工具栏中的icon,而这里的icon则是在拓展管理,和应用商店中使用
    "16": "icon16.png",
    "48": "icon48.png",
    "128": "icon128.png"
  },
  // Optional
  "author": ...,
  "automation": ...,
  "background": {
    // Required
    "service_worker": ... //这个声明的是background script
  },
  "chrome_settings_overrides": {...},
  "chrome_url_overrides": {...},
  "commands": {...},
  "content_capabilities": ...,
  "content_scripts": [{...}],
  "content_security_policy": "policyString",
  "converted_from_user_script": ...,
  "current_locale": ...,
  "declarative_net_request": ...,
  "devtools_page": "devtools.html",
  "differential_fingerprint": ...,
  "event_rules": [{...}],
  "externally_connectable": {
    "matches": ["*://*.example.com/*"]
  },
  "file_browser_handlers": [...],
  "file_system_provider_capabilities": {
    "configurable": true,
    "multiple_mounts": true,
    "source": "network"
  },
  "homepage_url": "http://path/to/homepage",
  "host_permissions": [...],
  "import": [{"id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}],
  "incognito": "spanning, split, or not_allowed",
  "input_components": ...,
  "key": "publicKey",
  "minimum_chrome_version": "versionString",
  "nacl_modules": [...],
  "natively_connectable": ...,
  "oauth2": ...,
  "offline_enabled": true,
  "omnibox": {
    "keyword": "aString"
  },
  "optional_permissions": ["tabs"],
  "options_page": "options.html",
  "options_ui": {
    "chrome_style": true,
    "page": "options.html"
  },
  "permissions": ["tabs"],
  "platforms": ...,
  "replacement_web_app": ...,
  "requirements": {...},
  "sandbox": [...],
  "short_name": "Short Name",
  "storage": {
    "managed_schema": "schema.json"
  },
  "system_indicator": ...,
  "tts_engine": {...},
  "update_url": "http://path/to/updateInfo.xml",
  "version_name": "aString",
  "web_accessible_resources": [...]
}

background scripts

什么是background scripts

我们平时删除书签,关闭tab之类的,这些操作事件,都是在background scripts中写一些指令来进行操作(监听或者更加合适)。我的理解是background server通过一些命令可以监听浏览器的主进程或者UI进程的一些事件,并通过一些指令进行响应。

如何注册background scripts

{
  ...
  "background": {
    "service_worker": "background.js"
  },
  ...
}

在V3中我们依然需要在manifest.json进行声明。先在manifest.json同级下创建一个js文件,然后将这个文件配置到manifest.json中的service_worker

background scripts加载时机

background scripts只有在被需要的时候会被加载,比如:

  • 拓展第一次被安装,以及更新的时候。
  • 后台页面正在侦听一个事件,并且该事件被调度,比如我们监听页面书签创建的事件chrome.bookmarks.onCreated.addListener,每当有书签被创建的时候就会background serve就会被加载。
  • 接收来自content script或者其他模块发送的消息时,会被加载。
  • 在这个拓展中的其他模块,比如popup(就是点击图标的时候会弹出一个弹窗)在调用与background相关的API,例如runtime.getBackgroundPage的时候已失效。

background scripts一旦加载完成后,background scripts会保持休眠被调用的时候会重新运行。大家可以通过浏览器的进程监控来查看拓展的CPU占有率。此外,在所有可见视图和所有消息端口都关闭之前,Service Worker 不会卸载。

关于监听事件的创建

说实话,在background写一些监听事件在开发体验上感觉与写自动化测试的脚本中的断言类似,我们拿文档中的一些栗子说明

// 这一段就是当页面加载完成之后创建菜单
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    "id": "sampleContextMenu",
    "title": "Sample Context Menu",
    "contexts": ["selection"],
  });
});
// 这一段就是监听书签创建的事件
chrome.bookmarks.onCreated.addListener(() => {
  ...
});

是不是其实还挺简单的?大家其实只要只要记住:

要监听一些浏览器自带的操作要用background scripts
要监听一些浏览器自带的操作要用background scripts
要监听一些浏览器自带的操作要用background scripts

content scripts

什么是content scripts

content script是运行在web页面上下文的一个脚本。我的理解是他是页面中的一个沙盒,与当前页面运行的JS是两个不同的环境,(这点比较容易弄混),但是content script是可以操作DOM的,并且可以通过API与父级拓展进行交互。如图:

大家可以这么理解,共用了一个HTML,其他的各自划片为王。

当然他能使用的API也是有限的,如下:

i18n,
storage,
runtime: {
  connect
  getManifest
  getURL
  id
  onConnect
  onMessage
  sendMessage
}

content scripts与当前页面的JS环境不互通

在实际代码操作之前,我们先假设他们的变量是一个顶级的变量可以互通,验证的话我们需要用一个相同的变量测试。

// 
// content-script.js
console.log(greeting);
var greeting = "hola, ";
console.log(greeting);
var button = document.getElementById("mybutton");
button.person_name = "Roberto";
button.addEventListener("click", () =>
  alert(greeting + button.person_name + ".")
, false);
<!-- test.html -->
<body>
  <button id="mybutton">click me</button>
  <script>
    console.log(greeting);
    var greeting = "hello, ";
    console.log(greeting);
    var button = document.getElementById("mybutton");
    button.person_name = "Bob";
    button.addEventListener("click", () =>
      alert(greeting + button.person_name + ".")
    , false);
  </script>
</body>

代码如上,通过测试我们确认他们确实无法共用一个变量,输出如下:

// undefined                  test.html:11 
// hello,                     test.html:13 
// undefined                  content-script.js:1 
// hola,                      content-script.js:3  

如何注入脚本

content script注入脚本有两种方式

  • 静态注入
  • 函数式注入
静态注入

我之前测试环境互通问题的时候,用的就是静态注入脚本的方式,我们可以直接在manifest.json进行声明

{
  ...
 "content_scripts": [
   {
     "matches": ["<all_urls>"], // 这个地方我为了方便,直接给所有URL都赋予了权限
     "js": ["content-script.js"]
   }
 ],
  ...
}

官方也提供了静态注入的配置规则

函数式注入

按需注入脚本也是比较常见的,但是需要我们配置一下permissions。就比如:我要对当前聚焦的页面赋予权限,那么就配上activeTab

{
  ...
  "permissions": [
    "activeTab"
  ],
  ...
}

动态注入的方法我们就直接看看官网的demo,可以支持直接引用文件或者直接调用函数

引用文件
//// content-script.js ////
document.body.style.backgroundColor = 'orange';

//// background.js ////
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    files: ['content-script.js']
  });
});
引用函数
// background.js
function injectedFunction() {
  alert('hxdm');
}
chrome.action.onClicked.addListener((tab) => {
  chrome.scripting.executeScript({
    target: { tabId: tab.id },
    function: injectedFunction
  });
});

content scripts加载时机

content scriptbackground scripts不同的是,他的运行时间是可以配置的,浏览器提供了以下几个时机

nameDescription
document_idle首选 浏览器选择document_endwindow.onload一被触发之间注入脚本。注入的时间取决于脚本的复杂程度和加载所需的时间。因为是在window.onload触发之后,所以此时DOM基本上加载完毕。
document_start脚本在css文件加载之后,在DOM构建之前注入
document_end脚本在 DOM 完成后注入,但在图像和帧等子资源加载之前。

content scripts与子页面之间的通讯

这个在顶级上下文和iframe中的页面进行通讯的时候,与平常开发一样,也可以通过window.postMessage进行通讯。

options page

什么是options page

options page就是用户对拓展进行配置的页面,允许用户通过提供选项页面来自定义扩展的行为。

这么说可能不太那么清晰,我们举一个栗子:

👉如图所示,点击右键就会出现一个菜单,菜单中的选项就是options page

点击进入之后页面就是一些配置项,我们拿google翻译来看

如何注册options page

{
  ...
  "options_page": "options.html",
  ...
}

当然,假如你的配置就是非常简单的表单,其实我们也可以用弹窗之类的嵌入式UI来展示,但是需要多配置一些属性,如下:

{
  ...
  "options_ui": {
    "page": "options.html",
    "open_in_tab": false
  },
  ...
}

但是这个嵌入式的页面有兼容问题,所以大家在使用的时候可以这么写:

{
  if (chrome.runtime.openOptionsPage) {
    chrome.runtime.openOptionsPage();
  } else {
    window.open(chrome.runtime.getURL('options.html'));
  }
}

options page中的修改如何保存

因为很多时候options page会配置一些操作选项,需要进行保存,官方推荐的方法是使用storage.sync

UI elements

什么是UI elements

他其实并不是一个单独的模块,而是多个模块的集合体,他包含了page_actionpopupTooltipContext menu等。

举个栗子:

  • 浏览器上边的icon,就是一部分
  • 以及点击展开的popup

...

以上都是UI elements的一部分。

怎么注册UI elements

注册action也分成2种方式

  • browser_action=>V2V3中失效
  • page_action=>V2V3中失效
  • action

一般情况下,配置默认的action就能满足大部分的需求了

{
  ...
  "action": {
    "default_icon": {              // 浏览器上边的icon
      "16": "images/icon16.png",   
      "24": "images/icon24.png",   
      "32": "images/icon32.png"    
    },
    "default_popup": "popup.html"  // 点击小图标出来的弹窗
  },
  ...
}

但是如果需要用一些自定义的功能,就需要用到一些API了,比如action.setIcon()或者OffscreenCanvas

还有很多有意思的操作,大家可以看一下具体的API。

怎么注册popup

popup注册如下,他就是一个html文件,在Manifest中声明

{
  ...
  "action": {
    "default_icon": {              // 浏览器上边的icon
      "16": "images/icon16.png",   
      "24": "images/icon24.png",   
      "32": "images/icon32.png"    
    },
    "default_popup": "popup.html"  // 点击小图标出来的弹窗
  },
  ...
}

Context menu 设置

给右键菜单进行拓展,比如:我们平时用的一些翻译拓展就经常用这个东西。

Context menu 是什么

这个也是我们很常见的UI,就是当我们在页面点击右键的时候,常常会看默认选项之外的选项,比如这样:

我们会发现额外多出了一个截图的选项。这个功能就是通过Context menu来进行设置。

Context menu 注册
{
  ...
  "permissions": [
    "contextMenus",
    ...
  ],
  ...
}

注册好之后,我们可以通过API来创建options

chrome.runtime.onInstalled.addListener(function() {
  chrome.contextMenus.create({
    id: 'key',
    title: 'title',
    type: 'normal',
    contexts: ['selection'],
  });
});

Commands

什么是Commands

扩展可以定义特定的命令并将它们绑定到一个组合键。在该"commands"字段下的json中注册一个或多个命令。

注册Commands

{
  ...
  "commands": {
    "flip-tabs-forward": {
      "suggested_key": {
        "default": "Shift+1",
        "mac": "Shift+1"
      },
      "description": "demo"
    },
    ...
  }
  ...
}

然后我们可以通过API监听快捷键

chrome.commands.onCommand.addListener(function(command) {
  if (command === 'demo') console.log('demo');
});

Tooltip设置

这个主要就是当我们hover一个图标的时候,会出现一个提示。

{
  ...
  "browser_action": {
    "default_title": "这是一个拓展的提示"
  }
  ...
}

这个提示也可以直接通过API进行设置

chrome.browserAction.onClicked.addListener(function(tab) {
  chrome.browserAction.setTitle({tabId: tab.id, title: "这是动态设置的提示"});
});

改。

Omnibox 设置

这个就是设置一个搜索栏,我们可以通过监听输入事件,获取输入内容,在通过servewokrk的API来打开新的tab。

{
  ...
  "omnibox": { "keyword" : "nt" },
  ...
}
chrome.omnibox.onInputEntered.addListener(function(key) {
  chrome.tabs.create({ url: `https://www.google.com/search?q= ${encodeURIComponent(key)}` });
});

图示

浏览器拓展各个模块的通讯

通过上边的梳理,我们已经知道了各个模块分别是负责什么,以及有什么功能。在实际的开发中,其实几个模块之间的通讯也是非常常见的。

模块之间通讯的示意

常用4种通信路径:

  • popupbackground script之间的通信
  • background scriptcontent script之间的通信
  • popupcontent script之间的通信
  • inject scriptcontent script之间的通信

模块之间通讯的方式

已经梳理出常见的通讯方式,但是因为运行的环境等不同,他们之间能够访问与操作的内容也会有不一样,比如是否能够访问chrome的API,是否能操作DOM,是否能访问页面的JS等等。

类型可以访问的API能否操作DOM能否访问页面的JS
inject scriptx,因为是运行在页面的JS环境中可以可以
content scritp可以访问extentions, runtime可以x
background Script可以访问各种拓展的API,除了devtoolsxx
UI elements(popup.js)可以访问各种拓展的API,除了devtoolsxx

梳理完权限之后,我们就可以根据表格,来进行模块之间的通信。大家再看看之前的图

为什么这么分,之后再说,可以简单的理解为是不同的进程,因为进程不同,所以通讯方式也不一样。

模块之间通讯的方式

background script 与 popup之间的通信

通过chrome.storage进行通信
// background.js //
chrome.runtime.onInstalled.addListener(() => {
  chrome.storage.sync.set({ msg: '我来自background' });
});

// popup.js 
let el = document.getElementById('btn');

chrome.storage.sync.get('msg', ({ msg }) => {
  console.log(`消息:${msg}`);
});
通过message通信
// background.js 
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  sendResponse('ok');
});

// popup.js 
chrome.runtime.sendMessage({text: info.selectionText}, (response) => {
console.log('response: ', response);
});

background script 与 content Script之间的通信

只能通过message的方式进行通信,并且方式还不一样,可以理解为是跨进程的通信,因此还要指定哪一个进程,也就是哪一个tab页

// background.js 
chrome.tabs.sendMessage(tab.id, {text: info.selectionText}, (response) => {
console.log('bgresponse: ', response);
});

// popup.js 
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
  sendResponse('ok');
});

popup和content script之间的通信

这一点同上

inject script和content script之间的通信

大家可以理解为inject script就是页面上的js,没法访问任何拓展的API,但是访问DOM,所以这两者只能通过DOM进行通信

浏览器拓展的运行原理

通过小学二年级的学习,我们知道:Chrome浏览器是一个多进程的程序,里边有主进程,渲染进城,网络进程,拓展进程等等。

每次浏览器初始化的时候,主进程会创建一个启动任务而这个启动任务会初始化Extension Service,而这个service中有一个loader方法,会加载用户所有拓展中允许安装的拓展,比如下图就是用户允许安装的拓展

这些拓展会被放在一个extension register的列表中进行维护,假如检测到这个页面有一个background_page那么,在浏览器初始化完窗口之后,会创建一个extentionHost的对象,通过这个对象去调用webContents的类,创建一个我们看不见的网页而这个地址一般就是chrome://extensions/***********(extensionId),并且通过这个host,主进程就可以与拓展进程进行IPC通信。

因此,大家也可以理解为拓展就是一个特殊的渲染进程,之前大家看到的这个模块就可以解释了:

  • Manifest就是一个配置
  • background,options,UI elements这些可以理解为就是extensions Page,可以通过这个地址访问chrome://extensions/***********(extensionId)
  • devtool是单独的一个进程
  • content Script是后期被加载到各个渲染进程下的

总结

浏览器拓展还是很强大的,对于他的理解最好是通过浏览器的运行机制去研究,去分析,有时间的话大家也可以尝试尝试,假如哪天你需要爬一些资源是吧,试试他?