javascript-Chrome扩展实例(三)

Chrome扩展实例(三)

前面的教程中,我们已经阐述了Chrome扩展开发的整体框架和主干内容,经历了扩展开发的大致流程。Chrome以manifest.json为组织框架,Chrome API为轴心,service worker为协调者,通过Content scripts的三种注入模式来更改网页内容,再辅以popupoptions来增强操作,能够实现丰富的用户定制化功能。此外,我们之前还介绍了权限、上下文、快捷键等概念,通过这些知识,如果已经有了一定javascript/CSS基础,那么开发常用的插件大多不成问题。

这篇文章将会介绍三个更深入一些的概念,不一定在开发过程中会用到,但是能提升Chrome扩展的可用性和丰富其功能性。它们分别是:

  • Web可访问资源(Web Accessible Resources)
  • 消息传递(Message passing)
  • 丰富的通知API(Rich notifications API)

为了完整性,本文还将补充匹配模式(match pattern)的内容。如果在阅读过程中遇到匹配模式不懂的内容,也可以优先阅读匹配模式(match pattern)的章节。

Web可访问资源(Web Accessible Resources,WAR)

在阐述tab页面和扩展时,我们已经知道它们分属于不同的上下文,即有独立的运行环境。扩展的代码正常情况下只可以使用扩展自身的资源(如函数、CSS文件、图片、javascript文件)等等,无法访问网页的DOM及JS,同时网页在开发时也是意识不到Chrome扩展存在的,因此无法访问Chrome扩展的资源。

为了打破这种界限,扩展使用了Inject scripts技术,使用三种模式进行内容注入,从而使得扩展的content script能够操作网页的DOM元素。需要指出的是扩展的content script无论使用静态声明注入、动态声明注入还是以编程方式注入,都只是获得了网页DOM元素的操作权,本质上Chrome的content script脚本和网页的JavaScript脚本还是处在两个独立的空间,没有互操作性,同时网页的DOM元素依然无法使用扩展内的资源。

如果我们希望网页的DOM元素直接使用扩展的资源(比如图片、视频、CSS样式表甚至javascript文件),有什么方法呢?这就需要设置Web可访问资源(Web Accessible Resources, WAR)

Web可访问的资源是Chrome扩展中的文件,可由Web页面或其他Chrome扩展访问。扩展程序通常使用此功能来显示需要在网页中加载的图像或其他资源,扩展包中任何文件都可以设置成Web访问的。

默认情况下,扩展中的任何资源都不是Web可访问的,只有扩展本身的页面或Script脚本能够访问这些内容。如果希望扩展以外的Web页面或脚本能够使用扩展中的资源,需要使用manifest.json中的web_accessible_resources字段来配置,格式如下:

1"web_accessible_resources": [
2    {
3      "resources": [ "images/file1.png", "css/file2.css" ],
4      "matches": [ "https://surprisedcat.github.io/*" ],//<all_urls>
5      //"extension_ids": ["chrome-extension://EXTENSION_ID/"],
6      //"use_dynamic_url": true
7    }
8  ],

其中,resources表示需要暴露给外界的资源文件名称;matches表示匹配的URL,使用match pattern,只有匹配成功的url才能使用暴露出来的资源,也可以使用extension_ids来匹配其他Chrome扩展。matchesextension_ids二者必有一个。use_dynamic_url,可选项,它为真时,只允许使用动态ID来访问Web可用资源,每个session都会的自己生成动态ID。

之后,外部Web页面可以通过三种方式访问。一是外部Web页面通过chrome-extension://extension-id/FILE_PATH直接访问;二是当使用content script时,content script中可以使用Chrome API chrome.runtime.getURL(FILE_PATH)来访问。chrome.runtime.getURL将根据自身来自哪个扩展生成extension ID。三是可以使用@@extension_id来替代javascript中chrome.runtime.getURL生成extension ID的效果(用法chrome.i18n.getMessage("@@extension_id")),也可以在CSS文件中使用(用法__MSG_@@extension_id__),注意__MSG_@@extension_id__用在html的内联css中貌似不生效,只在Web可访问文件的独立CSS/JS文件中生效,可能是因为Chrome扩展觉得Html网页的内容不归它管吧,只有从它这里取出去的文件才会做__MSG_@@extension_id__的转换。

实例:资源可用性展示

下面我们通过下面例子中4个图片的可访问性来展示Web可访问资源的用法与性质。为了方便,我们先将该扩展的文件结构展示如下:

 1web_accessible_resources
 2    ├─ manifest.json
 3    ├─ options.html
 4    ├─ assets
 5    │    └─ test.css
 6    ├─ images
 7    │    ├─ icon-128.png
 8    │    ├─ test1.png
 9    │    ├─ test2.png
10    │    ├─ test3.png
11    │    └─ test4.png
12    └─ js
13       ├─ content.js
14       └─ web_access_resources.js

其中,test1.png-test4.png是四张标有数字的图片。另外,示例的Demo网页网站为:https://surprisedcat.github.io/%E7%BD%91%E9%A1%B5%E8%B5%84%E6%96%99/DemoAndTest/javascript_chrome_extension_web_accessible_resources.html。该网页相关的结构如下:

1DemoAndTest
2    ├─ javascript_chrome_extension_web_accessible_resources.html
3    └─ js
4      └─ web_access_resources.js

为了便于对比,我们将扩展的option.html设置的与Demo网页javascript_chrome_extension_web_accessible_resources.html几乎一样(包括web_access_resources.js也一样),除了content script的js内容用了不同引入方式。因为扩展内部的option.html是无法使用content script,因此,我们直接用<script src="js/content.js"></script>引入,javascript_chrome_extension_web_accessible_resources.html中则是使用扩展静态声明注入。

首先,我们查看manifest.json来大体了解下这个扩展:

 1{
 2    "name": "Web Accessible Resources Demo",
 3    "version": "1.0",
 4    "manifest_version": 3,
 5    "action": {
 6        "default_icon": "images/icon-128.png"
 7    },
 8    "options_page": "options.html",
 9    "content_scripts": [
10        {
11        "matches": [ "https://surprisedcat.github.io/*" ],
12        "all_frames": true,
13        "js": ["js/content.js"]
14        }
15    ],
16    "web_accessible_resources": [
17        {
18        "resources": [ "images/test1.png", "images/test2.png", "images/test3.png", "assets/test.css" ],
19        "matches": [ "https://surprisedcat.github.io/*" ]
20        }
21    ]
22}

其中,和Web可访问资源相关的就是web_accessible_resources,设置了4个https://surprisedcat.github.io这个域名下访问的的资源,前三个是图片1,2,3,最后一个是一个CSS文件,它调用了图片test3.png作为背景图片。test4.png没有被作为Web可访问资源,在扩展的options.html可以访问,而外部页面无法访问,后面我们将展示这个例子。此外,manifest.json还使用静态声明注入了js/content.js

下面我们看看javascript_chrome_extension_web_accessible_resources.html的源码:

 1<!DOCTYPE html>
 2<html lang="en">
 3  <head>
 4    <meta charset="utf-8">
 5    <meta http-equiv="X-UA-Compatible" content="IE=edge">
 6    <meta name="viewport" content="width=device-width, initial-scale=1">
 7    <style>
 8        #test3 {
 9            border-style: solid;
10            height: 100px;
11            width: 100px;
12        }
13    </style>
14    <title>Web accessible resources</title>
15  </head>  
16  <body>
17    <h1>Web可用资源示例</h1>
18    <h2>本页面位于外部网站</h2>
19    <h2>Web页面直接使用Extension ID加载:text1.png</h2>
20    <form><label>Extension ID:</label><input type="text" id="extension_id_input">
21    <input type="button" id="testButton" value="确定"></form><br>
22    <img id="test1" height="100" width="100"><br>
23    <h2 id="header2">Content script加载:text2.png</h2>
24    <img id="test2" height="100" width="100"><br>
25    <h2 id="header3">CSS作为背景加载:text3.png</h2>
26    <div id="test3"></div><br>
27    <h2>一个不在web accessible resources中的文件:text4.png</h2>
28    <img id="test4" height="100" width="100"><br>
29
30    <script src="js/web_access_resources.js"></script>
31  </body>
32</html>

总结来说,就是放了四个框框,每个框框里希望引入一个图片。直观效果如下:

chrome_extension_web_accessible_resources_demo

第一个<img id="test1" height="100" width="100">和第四个<img id="test4" height="100" width="100">都是通过js脚本修改其src属性来更新图片的。js脚本内容在js/web_access_resources.js文件中,内容如下;

1//为“testButton”按钮增加监听事件
2document.getElementById("testButton").addEventListener("click",setExtensionID)
3//点击按钮,将输入框中的extension id值赋值给第一、四个<img>标签的src属性。
4function setExtensionID(){
5    let input_id = document.getElementById("extension_id_input").value;
6    document.getElementById("test1").src="chrome-extension://"+input_id+"/images/test1.png";
7    document.getElementById("test4").src="chrome-extension://"+input_id+"/images/test4.png";
8}

因为在Web页面的Html中,Chrome扩展无法使用@@extension_idchrome.runtime.getURL(),所以Html页面若要使用Web可访问资源,只能使用最原始的chrome-extension://extension_id/File_path的方式。我们可以通过chrome://extensions/页面找到扩展的ID(Demo中ID为ioianjljdbholheahfhjidofmjpgnfho,载入后每个用户可能不同)。如果我们已经载入了扩展,就能看到test1.png已经被载入了,而test4.png无法访问,并且报错

Denying load of chrome-extension://ioianjljdbholheahfhjidofmjpgnfho/images/test4.png. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

这是因为我们并没有把test4.png作为Web可访问资源,因此外界无法访问。但是,如果我们打开扩展的选项页options.html,执行同样的操作,test4.png是能显示出来的。这是因为options.html是扩展内的Web页面,可直接访问到扩展内部的所有资源,无需web_accessible_resources字段。二者区别如下:(图左外部页面,图右扩展options.html页面。)

chrome_extension_web_accessible_resources_demo2.png

Tips: 在实际使用Web可访问资源的时候,遇到一个坑。我刚开始开发Chrome扩展的时候,使用Web Accessible Resources的资源文件是一个名叫test image.jpeg的图片,这个文件名中间有个空格,当时运行扩展的时候,就是找不到这个图片资源,报找不到资源的错误。后来发现,在js编码的时候,把空格编码成了“%20”,也就是test%20image.jpeg,因此在manifest.json中"web_accessible_resources"设置资源名称时应该将空格改成“%20”,这样才能正常加载图片。不过为了少出错误,文件名中最好还是不要有空格等特殊字符吧。

有细心的读者会发现,上面对比的页面的图片与原始Web页面相比,中间两个标题后面多出了两个按钮:“载入图片”和“载入CSS”。这是我们载入扩展后,静态声明注入的js/content.js文件中添加的DOM元素,其代码如下:

 1//content.js
 2//在标题后添加按钮元素,点击后添加图片src
 3let loadButton = document.createElement('button');
 4loadButton.innerText = '载入图片';
 5loadButton.addEventListener('click', handleLoadImage);
 6document.getElementById("header2").append(loadButton);
 7//使用chrome.runtime.getURL生成带扩展ID的URL
 8function handleLoadImage() {
 9    let element = document.getElementById("test2");
10    strtest = chrome.runtime.getURL("images/test2.png");
11    element.src = strtest;
12}
13//在标题后添加按钮元素,点击后添加link元素链接外部CSS
14let loadCSS = document.createElement('button');
15loadCSS.innerText = '载入CSS';
16loadCSS.addEventListener('click', handleLoadCSS);
17document.getElementById("header3").append(loadCSS);
18//使用chrome.i18n.getMessage("@@extension_id")生成带扩展ID的URL
19function handleLoadCSS() {
20    const head = document.getElementsByTagName('head')[0];
21    const link = document.createElement('link');
22    link.type='text/css';
23    link.rel = 'stylesheet';
24    link.href = "chrome-extension://"+chrome.i18n.getMessage("@@extension_id")+"/assets/test.css";
25    head.appendChild(link);
26}

这段js代码的关键就是如何生成带extension id的URL,我们分别通过两种方式实现,一是调用chrome.runtime.getURL接口,二是调用chrome.i18n.getMessage("@@extension_id")这两种都能获得extension id。当在CSS文件中无法使用Chrome API获得extension id时,Chrome的开发人员还为我们提供了__MSG_@@extension_id__让Web可访问的CSS资源能够动态生成ID。我们在网页中引入Web可访问的CSS样式表test.css中就是用了__MSG_@@extension_id__作为第三个方框的背景图:

1#test3 {
2    background-image: url('chrome-extension://__MSG_@@extension_id__/images/test3.png');
3}

这样我们就能够载入所有Web可访问资源了:

chrome_extension_web_accessible_resources_demo3.png

再谈content script与网页上下文

前面我们已经知道页面的js和扩展content script注入的js运行在两个不同的上下文中。只有同一个上下文中的js是具有互操作性的。比如,同一个网页中无论是内联js还是外部链接引入的js,他们可以共享全局变量、函数等(但是要注意载入顺序,否则会出现前面的js引用后面还未载入的js内容而导致的引用为null的错误)。

虽然页面的js和扩展content script注入的js二者的运行上下文是隔离的,但是它们都可以操作页面DOM,这也是content script的特殊之处。

但是在Web可访问资源的场景下,还有一种特殊的javacript文件,它本身属于Chrome扩展,但是通过manifest.json中的Web可访问资源配置项暴露给了外面的网站,从而被外面网站所引用,这种特殊的js脚本可以叫做injected scripts through web accessible resources。其实,我们可以将这类js简单地理解成当前Web页面引入了一个跨域的js文件,并在Web页面的上下文中运行,因此injected scripts through web accessible resources与页面其他的js本质是一样的,此不过来源比较特殊,是从扩展引入的。因此,injected scripts through web accessible resources的运行上下文就是Web页面的上下文,可以操作DOM,也可以与页面本身的JS互操作。同时,这类JS也和Chrome扩展的上下文没有了关系,因此无法使用任何Chrome API。

而扩展本身的js如background.jspopup.js等,他们完全与Web页面的js上下文隔离开,只能通过Chrome API、content script间接访问页面的DOM元素。(devtools的js例外,正常也用不到)

Javascript可访问性总结如下表:

JS种类 可访问的Chrome API DOM访问情况 页面JS访问情况 附:调试方式
injected scripts through web accessible resources 和普通JS无任何差别,不能访问任何扩展API 可以访问 可以访问 开发者工具-控制台(和页面JS一样)
content scripts 只能访问 storage、i18n、runtime等部分API 可以访问 不可以 开发者工具-控制台-javascript上下文-切换为对应扩展
popup js 可访问绝大部分API,除了devtools系列 不可直接访问 不可以 扩展按钮-右键-审查元素-DevTools
background js 可访问绝大部分API,除了devtools系列 不可直接访问 不可以 扩展管理页面-查看视图-DevTools
devtools js 只能访问 devtools、runtime等部分API 可以 可以 -

消息传递(Message passing)

Chrome扩展不同组件的消息传递模式不同。(Todo)

消息传递方式总结

  js in Web page (including web accessible resources) content-script popup-js background-js
js in Web page (including web accessible resources) - window.postMessage externally_connectable
content-script window.postMessage - chrome.runtime.sendMessage chrome.runtime.connect
popup-js background-js externally_connectable chrome.tabs.sendMessage chrome.tabs.connect -

丰富的通知API(Rich notifications API)

Chrome扩展可以通过丰富的通知API和模板给用户推送系统通知。Chrome的通知有四类:

  1. 基本通知
  2. 带图片的通知
  3. 列表通知
  4. 进度条通知

所有种类的通知都包含一个标题、消息、一个小图标以及一个消息摘要。不过需要注意的是不同系统之间的消息接口可能不一样(比如MacOS的消息模式就不同)。需要指出,通知API需要在manifest.json中授予notifications权限。

Chrome扩展的通知主要使用chrome.nitification接口,典型的创建通知的方式如下:

1chrome.notifications.create(id, options, creationCallback);

其中,options是一个消息类型模板,依照上面提到的四种类型设置。四种模板使用如下:

 1//基本通知模板,这四项是必需的
 2var opt = {
 3  type: "basic",//类型是basic
 4  title: "Primary Title",
 5  message: "Primary message to display",
 6  iconUrl: "url_to_small_icon"
 7}
 8//带图片通知模板
 9var opt = {
10  type: "image",//类型是image
11  title: "Primary Title",
12  message: "Primary message to display",
13  iconUrl: "url_to_small_icon",
14  imageUrl: "url_to_preview_image"//多出来图片url这项
15}
16//列表通知模板
17var opt = {
18  type: "list",//类型是list
19  title: "Primary Title",
20  message: "Primary message to display",
21  iconUrl: "url_to_small_icon",
22  items: [{ title: "Item1", message: "This is item 1."},//列表项
23          { title: "Item2", message: "This is item 2."},
24          { title: "Item3", message: "This is item 3."}]
25}
26//进度条通知模板
27var opt = {
28  type: "progress",//类型是progress
29  title: "Primary Title",
30  message: "Primary message to display",
31  iconUrl: "url_to_small_icon",
32  progress: 42//进度条进度0-100
33}

一般系统中,通知还可以带至多两个action选项,并可以添加监听事件来回调函数。例如:chrome.notifications.onButtonClicked.addListener(replyBtnClick);

总体而言,Chrome的notification API比较简单,下面我们通过一个例子来体验下用法与效果。

实例:按时喝水

写在最前面,对于windows或macos系统,通知有可能不好用,因此这部分内容我觉得意义不大,不看也行。

在真正进入这个例子之前,我们先简要介绍下chrome.alarms API,其主要作用是安排代码定期运行或在将来的指定时间运行,需要授予alarms权限。

我们接下来要写的这个扩展是一个定时通知提醒我们喝水的小程序。其manifest.json文件如下:

 1{
 2  "name": "Drink Water Event Popup",
 3  "description": "Demonstrates usage and features of the event page by reminding user to drink water",
 4  "version": "1.0",
 5  "manifest_version": 3,
 6  "permissions": [
 7    "alarms",
 8    "notifications",
 9    "storage"
10  ],
11  "background": {
12    "service_worker": "background.js"
13  },
14  "action": {
15    "default_title": "Drink Water Event",
16    "default_popup": "popup.html"
17  },
18  "icons": {
19    "16": "drink_water16.png",
20    "32": "drink_water32.png",
21    "48": "drink_water48.png",
22    "128": "drink_water128.png"
23  }
24}

这其中需要说明是因为要定时运行,因此我们使用chrome.alarms API需要alarms,又因为需要通知用户因此需要notifications权限。

我们在popup.htmlpopup.js中设置定时运行程序。

 1<!DOCTYPE html>
 2<html>
 3  <head>
 4    <title>Water Popup</title>
 5    <style>
 6      body {
 7        text-align: center;
 8      }
 9
10      #hydrateImage {
11        width: 100px;
12        margin: 5px;
13      }
14
15      button {
16        margin: 5px;
17        outline: none;
18      }
19
20      button:hover {
21        outline: #80DEEA dotted thick;
22      }
23    </style>
24    <!--
25      - JavaScript and HTML must be in separate files
26     -->
27  </head>
28  <body>
29      <img src='./stay_hydrated.png' id='hydrateImage'>
30      <!-- An Alarm delay of less than the minimum 1 minute will fire
31      in approximately 1 minute increments if released -->
32      <button id="sampleMinute" value="1">Sample minute</button>
33      <button id="min15" value="15">15 Minutes</button>
34      <button id="min30" value="30">30 Minutes</button>
35      <button id="cancelAlarm">Cancel Alarm</button>
36   <script src="popup.js"></script>
37  </body>
38</html>

这个网页主题很简单,就是一幅图片后面跟了四个按钮。每个按钮在pop.js中为其添加了一个监听事件。如下:

 1function setAlarm(event) {
 2  let minutes = parseFloat(event.target.value);
 3  chrome.action.setBadgeText({text: 'ON'});
 4  chrome.alarms.create({delayInMinutes: minutes});
 5  chrome.storage.sync.set({minutes: minutes});//存储定时时长
 6  window.close();
 7}
 8
 9function clearAlarm() {
10  chrome.action.setBadgeText({text: ''});
11  chrome.alarms.clearAll();
12  window.close();
13}
14
15//An Alarm delay of less than the minimum 1 minute will fire
16// in approximately 1 minute increments if released
17document.getElementById('sampleMinute').addEventListener('click', setAlarm);
18document.getElementById('min15').addEventListener('click', setAlarm);
19document.getElementById('min30').addEventListener('click', setAlarm);
20document.getElementById('cancelAlarm').addEventListener('click', clearAlarm);

这段js内容就是给按钮加上点击的监听事件,分别设置不同的定时时间(通过按钮的value传递参数),最后一个是清除定时运行。设定或清除定时程序时,也会捎带把扩展图标上的badge设置上。

最后,我们来看下发出通知的background.js

 1//定时到时间后的操作发出通知
 2chrome.alarms.onAlarm.addListener(() => {
 3  chrome.action.setBadgeText({ text: '' });
 4  chrome.notifications.create({
 5    type: 'basic',
 6    iconUrl: 'stay_hydrated.png',
 7    title: 'Time to Hydrate',
 8    message: 'Everyday I\'m Guzzlin\'!',
 9    buttons: [
10      { title: 'Keep it Flowing.' }
11    ],
12    priority: 0
13  });
14});
15
16//如果点击通知的按钮则根据上一次的定时时长重置定时程序
17chrome.notifications.onButtonClicked.addListener(async () => {
18  const item = await chrome.storage.sync.get(['minutes']);
19  chrome.action.setBadgeText({ text: 'ON' });
20  chrome.alarms.create({ delayInMinutes: item.minutes });
21});

根据我的实际运行经验,定时程序是能运行的,因为能发现到时间了badge状态会改变,但是由于操作系统原因,通知并没有发送出来。所以这部分内容仅供参考吧。

匹配模式(match pattern)

匹配模式内容请参考整合版文档:Chrome扩展模式匹配

完成实例复现

经过前面的学习,我们已经拥有了Chrome扩展开发的基础,如果有兴趣的话,我们可以复现下面这个实例:

素材来源:https://github.com/KindEni/Chrome-Extension-Series/tree/main/Part%20Two

简介:这是要做一个Pomodoro时钟。Pomodoro一词来源于番茄工作法(Pomodoro Technique),是一种时间管理方法,在1980年代由Francesco Cirillo创立。该方法使用一个定时器来分割出一个一般为25分钟的工作时间和5分钟的休息时间,而那些时间段被称为pomodoros,为意大利语单词 pomodoro(中文:番茄)之复数。我们做的这个Pomodoro时钟要有倒计时、设定时间以及任务管理的功能。如果觉得这个工作方法有用,平时工作学习中也可以尝试使用这个扩展^_^。

参考文档

  1. Google Chrome Content Script https://developer.chrome.com/docs/extensions/mv3/content_scripts/#files
  2. Manifest - Web Accessible Resources https://developer.chrome.com/docs/extensions/mv3/manifest/web_accessible_resources/
  3. GoogleChrome/chrome-extensions-samples https://github.com/GoogleChrome/chrome-extensions-samples
  4. Message passing https://developer.chrome.com/docs/extensions/mv3/messaging/
  5. Rich notifications API https://developer.chrome.com/docs/extensions/mv3/richNotifications/
  6. Match patterns https://developer.chrome.com/docs/extensions/mv3/match_patterns/
  7. 番茄时钟扩展 https://github.com/KindEni/Chrome-Extension-Series/tree/main/Part%20Two