本文将按照 Manifest V2 来讲解说明,若你需要 V3 ,请参见:Welcome to Manifest V3
对于现在开发 Chrome 扩展来说,是非常简单的一件事情,其只需要使用 JavaScript 即可开发,并且 Chrome 官方对你如何开发 Chrome 扩展程序并没有严格要求,
只需要在你项目的根目录保证存在 mainfest.json 文件即可,该文件是用来配置所有和插件相关的配置,必须放在根目录,它就相当于 webpack 的入口文件。
manifest.json 配置详情
任意位置创建一个项目
在项目根目录创建一个名为 manifest.json
的文件
使用任意 IDE 通过 JavaScript 进行项目开发
将项目打包成 .crx
文件安装到 Google 中
chrome://extensions/
打开扩展程序或直接使用文件夹的形式安装到 Google 中
chrome://extensions/
打开扩展程序现在你可以在 Google 中使用你的扩展程序了
content_scripts
字段配置的脚本文件。browser_action
或 page_action
字段的选项 default_popup
配置的页面background 通常翻译为:后台。
它是一个常驻的页面,其生命周期是扩展中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭。
所以它的作用的通常是:把需要一直运行的、启动就运行的以及全局代码放里面去执行。
并且 background 的权限非常高,几乎可以调用所有的 Chrome 扩展 API(除了 devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置 CORS
。
经过测试,其实不止是 background,所有的直接通过
chrome-extension://id/xx.html
这种方式打开的网页都可以无限制跨域。所以通常我们可以使用 chrome.extension API 的 getUrl() 将项目中的相对路径转为完成的 URL,通常会被转为这样的格式:chrome-extension://xx.xx
而正也正好使得相对路径所对应的 .js 或 .html 文件可以无限制跨域。
{
"background":{
// page 和 scripts 二选一
"page"?:"background.html",
// 项目中的任意脚本
"scripts"?:[
"background.js",
"xx.js"?,
"..."?
],
// 可选;设置该 background(后台)是否是持久化的,通常是 false(默认值)
"persistent"?: false
}
}
page
:指定一个 .html 文件
scripts
:指定一个 .js 文件
若你使用 scripts,则 chrome 会自动为该脚本生成一个默认网页
使用 page
的好处在于:你可以配置 background(后台)的页面
不论是使用 page
或者是 scripts
,它们都能搭配使用 persistent
;
也可以忽略不使用 persistent
,它的默认值为:false
chrome://extensions/
而这就是我们在 manifest.json
的 background
字段配置的 .html 或 .js。
通过点击【背景页】显示的 background 页面(后台页面)和真正在运行的 background 并非同一个,
弹出的后台页面只是供你调试用的,参见:此处
只有处于开发者模式下且启用了的扩展程序才能查看 【background page】
通常来说 background
字段选项 persistent
的值通常为 false,参见:此处
若你不主动查看 background.js,则即使它报错导致程序崩溃,它也并不会主动提示任何信息。
manifest.json
中存在以下三个字段:
但是这三个字段只能选择,也必须选择 1 个使用,不可选择多个,否则 Chrome 将会加载失败。
当某些特定页面打开才显示的图标z
{
"page_action": {
"default_popup": "html/pageaction.html",
"default_title": "鼠标移动到扩展程序时将提示的信息",
"default_icon": "img/sds.png",
// 也可以选择多张图片,可以为不同的比例设置不同的图片,将自动应用!
"default_icon": {
"16": "img/sds.png",
"32": "img/sds.png",
"48": "img/sds.png",
"128": "img/sds.png"
}
}
}
Tips:
For the best visual impact, follow these guidelines:
其用处类似于 page_action
字段,但是 browser_action
可以显示在任何页面!
{
"browser_action": {
"default_popup": "html/browseraction.html",
"default_title": "鼠标移动到扩展程序时将提示的信息",
"default_icon": "img/icon.png",
// 也可以选择多张图片,可以为不同的比例设置不同的图片,将自动应用!
"default_icon": {
"16": "images/icon16.png",
"24": "images/icon24.png",
"32": "images/icon32.png"
}
}
}
page_action
和 browser_action
两个字段用来设置扩展程序在用户的”眼中”是什么样子的。
即:设置扩展程序【向外展示的图标(头像)】、【点击扩展程序弹出的页面】以及【鼠标移动到扩展程序时弹出的提示】是什么。
在扩展程序界面(
chrome://extensions/
)显示的图标是icons
字段配置的。
(what-is-use-browser-action)
以上图片各个弹出框在 page_action
或 browser_action
字段中的对应选项如下所示:
browser_action
所在的弹出框则是 "default_popup"
选项配置的(popup)。鼠标移动到扩展程序时将提示的信息
弹出框则是由:"default_title"
选项配置。"default_icon"
选项配置的;popup
是点击 browser_action
或者 page_action
图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互,
对于 browser_action
和 page_action
来说,default_popup 选项无疑是非常重要的,它是配置 popup 的关键。
你可以在 page_action 和 browser_action 有什么用? 一节中找到有关于 popup 的描述。
在 popup
可以包含任意你想要的 HTML 内容,并且会自适应大小。这意味着你完全可以在 popup 中加载一些指定的脚本文件,来执行一些命令。
有两种方法:
default_popup
字段来指定 popup(推荐)setPopup()
方法。在权限上,它和 background 非常类似——几乎可以调用所有的 Chrome 扩展 API(除了 devtools),而且它可以无限制跨域;
它们之间最大的不同是生命周期的不同,popup中可以直接通过 chrome.extension.getBackgroundPage()
获取 background 的 window 对象。
{
// 需要直接注入页面的JS
"content_scripts":
[
{
// 指定注入的页面地址(必要)
"matches": [
"" ?, // 表示匹配所有地址,配置了这个就没必须要配置其他的
"http://www.whyhw.com/" // 指定注入的页面,注:最后面的 '/' 是必要的
],
// 多个 JS 按顺序注入(可选)
"js": ["js/jquery-1.8.3.js", "js/content-script.js"],
// 多个 CSS 按顺序注入(可选)
"css": ["css/custom.css","css/xx.css"],
// 代码注入的时间(可选)
"run_at": "document_start"
},
// 这里仅仅是为了演示content-script可以配置多个规则
{
"matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
"js": ["js/show-image-content-size.js"]
}
],
}
contentScripts 是在网页上下文中运行的文件。通过使用标准的文档对象模型(DOM),contentScripts 能够读取浏览器访问的网页的详细信息,对其进行更改并将信息传递给其父扩展。
manifest.json
的 content_scripts
字段的作用是:把脚本和 CSS 注入到指定页面。
contentScripts 虽然运行在指定网页的上下文中,可以访问指定页面的 DOM,但是却无法访问指定页面的其他脚本文件;指定页面的 DOM 也无法主动调用 contentScripts 中的代码。
=> 比如:
<body> <button onclick="func()">点我button> body>
<script>
const func = () => cs() // 任意一个页面的 DOM 通过事件绑定主动调用 contentScripts 中的函数
script>
const cs = () => console.log('contentScripts')
当我们执行 a.html 并点击其中的按钮时,
并不会在控制台打印 contentScripts,而是会报错,cs 函数未定义
这是由于 contentScripts 不会被真正注入到页面中(它不会存在于页面),导致无法在 DOM 中主动通过绑定事件的方式调用 content-script
中的代码,
包括直接写 onclick
和addEventListener
2种方式都不行;但是,“在页面上(a.html)添加一个按钮并调用指定插件的扩展API”是一个很常见的需求,
比如:你想为一个指定域名的网站设置指定扩展,此时:你可以让网站”配合“你,从而为它定制出一个独特的 Chrome 扩展。
所以这就需要一个解决方法,详见:contentScripts 缺陷的解决方法
解决方法的理论也很简单,即:在 contentScripts 中,通过某种方式向指定页面注入一个 / 多个脚本文件(或是其他的一些什么),
这样你就可以在页面上主动的调用注入到页面的脚本或访问页面中的其他脚本,因为此时,你注入的脚本文件就属于指定页面了,而并非单纯的如同 contentScripts 中那样,是运行在网页的上下文中。
如:
// contentScripts
// 向页面注入 inject.js
function injectCustomJs(jsPath) {
jsPath = jsPath || 'js/inject.js';
var tempScript = document.createElement('script');
tempScript.setAttribute('type', 'text/javascript');
tempScript.src = chrome.extension.getURL(jsPath);
document.head.appendChild(tempScript); // 将指定 js 注入(添加)指定页面中
// 当注入的脚本加载完毕后移除它
tempScript.onload = function () {
this.parentNode.removeChild(this);
};
}
/**
* HTML被完全加载以及解析时,执行处理器
* 这时候,我们才将之注入到页面,否则会因为 HTML 未加载,脚本就注入完成,导致 DOM 中获取失败。
*/
document.addEventListener('DOMContentLoaded', () => {injectCustomJs()})
// inject.js
const cs = () => console.log('inject.js')
在做完往如上配置后,还必须在 manifest.json
中显示配置:
{
// 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
"web_accessible_resources": ["js/inject.js"], // 在这里配置你注入的任意脚本!
}
该字段表示:想要在 web 中直接访问插件中的资源的话必须显示声明,否则会报错。
当一切都搞定后,此时,当我们点击 a.html
中的按钮时,控制台将会打印 inject.js;这就表明我们已经成功解决指定页面的 DOM 无法调用扩展的脚本。
更多信息,参见:inject-scripts
虽然在 contentScripts 缺陷的解决方法 一节中,我们解决了指定页面的 DOM 访问扩展的脚本以及 contentScripts 访问页面的其他脚本问题,
但是该解决方法存在一个问题,即:injectJS 无法访问 contentScripts,这是因为二者是不同的,injectJS 就相当于页面上的脚本,而 contentScripts 仍然是运行在网页的上下文中;
contentScripts 访问不了页面的其他脚本文件,也自然无法访问 injectJS,反过来也是一样的:injectJS 无法通过普通方式访问 contentScripts。
那么该怎么解决呢?
参见:消息通信之 injectJS 和 contentScripts
详见:injecteJS 和 contentScripts 通信方法
contentScripts 将先于指定页面加载
这意味着你需要首先在 contentScripts 中判断当前要注入 contentScripts 的 HTML 是否加载完毕,你才能获取到 DOM 元素(如:document.head
等)
manifest.json
的 permission
字段用来请求当前扩展程序运行时所需要的权限。
即:当你使用 Chrome API 时,可能需要访问或使用一些资源,如:storage
、notifications
等,但是若你未在 permission
字段去请求使用这些权限,那么你所使用的 API
是无法成功执行的。
{
"permissions": [
"contextMenus",
"tabs",
"notifications",
"webRequest",
"webRequestBlocking",
"storage",
"http://*/*",
"https://*/*",
...
],
}
当你配置了权限后,你可以在 Chrome 的如下位置找到扩展程序所需要的权限说明:
打开扩展程序界面(chrome://extensions/
)
点击右上角开发者模式
点击任意的一个扩展程序的详细信息
找到【权限】字段
在【权限】字段将显示所必需的权限:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4Ce8C1gp-1617358105318)(/img/bVcQI0O)]
(permissions)
manifest.json
的 homepage_url
字段用来配置扩展程序的主页。
其位置在 Chrome 的:
打开扩展程序界面(chrome://extensions/
)
点击右上角开发者模式
点击任意的一个扩展程序的详细信息
找到【打开扩展程序网站】字段
而这就是我们使用 homepage_url
字段配置的扩展程序主页在 Chrome 浏览器的位置。
TIP:若是没配置扩展程序主页,则无法找到【打开扩展程序网站】字段。
{
"homepage_url": "http://www.whyhw.com"
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E8ADvRIy-1617358105319)(/img/bVcQI05)]
(homepage-url)
消息通信:popup.js、background.js、inject.js、contentScripts.js、devtools.js 之间的通信(访问)
注:-
表示不存在或者无意义,或者待验证。
injected-script | content-script | popup-js | background-js | |
---|---|---|---|---|
injected-script | - | window.postMessage | - | - |
content-script | window.postMessage | - | chrome.runtime.sendMessage chrome.runtime.connect | chrome.runtime.sendMessage chrome.runtime.connect |
popup-js | - | chrome.tabs.sendMessage chrome.tabs.connect | - | chrome.extension.getBackgroundPage() |
background-js | - | chrome.tabs.sendMessage chrome.tabs.connect | chrome.extension.getViews | - |
devtools-js | chrome.devtools. inspectedWindow.eval | - | chrome.runtime.sendMessage | chrome.runtime.sendMessage |
如何查看以上信息?如:
popup 访问 background 通过 chrome.extension.getBackgroundPage()
background 访问 popup 通过 chrome.extension.getViews
即:左边(popup) 访问 顶部(background) 通过 xxx(chrome.extension.getBackgroundPage() )
总结:
chrome.runtime.sendMessage
和 chrome.runtime.connect
使得 [contentScripts 和 devtoolos] 向 [popup 和 background] 发送消息。
window.postMessage
使得 injectJS 和 contentScripts 互相发送消息。
chrome.tabs.sendMessage
和 chrome.tabs.connect
**使得 [popup 和 background] 向 contentScripts 发送消息。 **
chrome.extension.getBackgroundPage()
使得 popup 能获取到 background 的 JS Window 对象。
popup 访问 background 通过 chrome.extension.getBackgroundPage() 返回正在运行的 background 的 JS Window 对象。如果扩展程序没有后台网页则返回 null。
background 访问 popup 通过 chrome.extension.getViews() 返回一个数组,并通过该数组指定获取 popup 的JS Window 对象。
TIP:获取时,必须保持 popup 处于激活状态。
chrome.extension.getBackgroundPage()
返回运行在当前扩展程序中的后台网页的 JavaScript window 对象。
如果扩展程序没有后台网页则返回 null。
// popup.js 或 popup.html 的 script 中
var bg = chrome.extension.getBackgroundPage();
console.log(bg.bgPopup()) // 将在 background 页面打印 background 中的 popup 方法
// background.js(/ background.html)
function bgPopup() {return 'background 中的 popup 方法'}
NOTE:
const bgPopup = () => 'background 中的 popup 方法
这样形式的函数方法,无法存在于 Window 对象中。
即:bg.bgPopup() 将为 undefined
chrome.extension.getViews( { type:‘tab’ | ‘inforbar’ | ‘notification’ | ‘popp’}? )
返回一个数组,含有每一个在当前扩展程序中运行的页面的 JavaScript window 对象。
参数:一个对象,表示指定要获取的视图类型。
如果省略,则返回所有视图(包括后台页面和标签页)。
有效值为:“tab”(标签页)、“infobar”(信息栏)、“notification”(通知)、“popup”(弹出窗口)。
// background.js
var views = chrome.extension.getViews({ type: 'popup' });
// popup 处于激活状态才执行,因为我们已经指定获取 popup Window,若它未激活,则 length = 0
if (views.length > 0) {
console.log(views[0]); // popup 的 Window 对象
console.log(views[0].p()); // 将输出:background 将调用本方法
}
// popup.js
function bgPopup() {return 'background 将调用本方法'}
Note:
contentScripts 和页面内的脚本(inject JS 也属于页面内的脚本)通信存在两种方法:
window.postMessage
和 window.addEventListener
来实现二者消息通讯TIP:它们二者之间唯一共享的东西就是页面的DOM元素。
postMessage
和 addEventListener('message',handle)
实现// 如果 message 为 object,则建议是 JSON 格式。
postMessage(message: any) => {...}
// inject.js
window.postMessage({"test": "你好"}, '*'); // window 是可选的。
postMessage('Yomau');
// 以上做法,会发送两个信息
// contentScripts.js
[window.]addEventListener('message',(e)=>{ // window 是可选的
console.log(e); // MessageEvent {...}
console.log(e.data); // 从 inject.js 中发送的数据
}})
/** 输出两个信息(省略输出 (e) ):
* {"test": "你好"}
* Yomau
*/
以下为监听到的 message 事件的对象。
(postMessageANDaddEventListener)
// inject.js
// 创建一个自定义事件 myCustomEvent,并将事件对象(实例)赋值给 customEvent,可冒泡但是不可取消
var customEvent =
new Event("hiddenDiv", { "bubbles": true, "cancelable": false });
(function fireCustomEvent(data) {
hiddenDiv = document.getElementById('myCustomEventDiv');
hiddenDiv.innerText = data;
// 当单击页面上的某个按钮时,才会触发自定义事件(开始通信)
hiddenDiv.querySelector('.btn').addEventListener('click', () => {
// 向指定的事件目标派发一个事件,注意:派发的事件要为该事件的事件对象
div.dispatchEvent(customEvent); // 使得 div 元素触发事件 myCustomEvent
})
})('你好,我是普通JS!')
// contentScripts.js
document.addEventListener('DOMContentLoaded', () => {
var hiddenDiv = document.getElementById('myCustomEventDiv');
if (!hiddenDiv) { // 若页面中不存在元素则先创建一个。
hiddenDiv = document.createElement('div');
hiddenDiv.setAttribute('id', 'myCustomEventDiv')
hiddenDiv.style.display = 'none';
document.body.appendChild(hiddenDiv);
}
// 监听元素的 myCustomEvent 事件并添加事件监听器。
hiddenDiv.addEventListener('myCustomEvent', function () {
var eventData = document.getElementById('myCustomEventDiv').innerText;
console.log('收到自定义事件消息:' + eventData);
});
injectCustomJs(); // 将 inject.js 注入到页面的方法。
})
准备通信流程:
创建一个自定义事件(inject.js)
使得某个元素 (hiddenDiv)[达到某个条件时](可选)触发自定义事件(inject.js)
这里的某个条件是:点击页面上的某个按钮时。
监听自定义事件,并为之添加处理器。(contentScripts.js)
开始通信流程:
在 popup 中使用 chrome.tabs.query
判断当前标签页是否活动且是否存于当前窗口
若判断成功,则调用 chrome.tabs.sendMessage(integer tabId, any message, function responseCallback)
向指定的 contentScripts(以 ID 作为标识) 发送信息;
并在指定的 contentScripts 响应后执行对应的回调函数 => 接收的第一个参数为:contentScripts 响应的数据。
在 contentScripts 中使用 chrome.runtime.onMessage.addListener(callback) 监听 ‘message’ 事件。
即:当 popup 向 contentScripts 发送消息时,将触发 ‘message’ 事件,这就会导致 contentScripts 的事件处理器执行
注:callback 的第二个参数必须要能序列化成 JSON 格式。
并在事件处理器中通过 sendResponse(任意可以序列化成 JSON 的对象) 响应信息给 popup.
// popup.js
function sendMessageToContentScript(message, callback) {
// 当 popup 激活(用户点击了扩展程序小图标)于 Chrome 当前窗口中的活动标签页中,就执行对应的回调函数。
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
console.log(`popup 将向 ID=${tabs[0].id} 的标签发送消息:{
cmd:'${message.cmd}',value:'${message.value}'}`)
// 向 tabs[0].id 标签(相当于当前标签)中的 contentScripts 发送 message 信息,
// 当 id 为 tabs[0].id 的 contentScritps 响应时就执行 callback(用来接收响应)
// response:响应返回的数据
chrome.tabs.sendMessage(tabs[0].id, message, function (response) {
if (callback) callback(response);
});
});
}
console.log('popup 准备向 contentScripts 发送消息。')
sendMessageToContentScript({ cmd: 'test', value: '你好,我是popup!' }, (response) => {
console.log('收到来自 content 的响应:' + response);
});
// contentScripts
// 监听 'message' 事件;当消息从扩展程序或者内容脚本中发送时,就执行对应的事件处理器。
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
console.log(`contentScripts 成功接收到消息:{ cmd:'${message.cmd}',value:'${message.value}'}`)
// 当 popup 发送信息过来到 contentScripts 时,这里返回一个响应内容给 popup
// 数据将由 sendMessage() 的第三个参数:callback function 的第一参数接收。
sendResponse('contentScripts 已成功接收,现重新响应给发送者(popup)');
});
双方通信直接发送的都是JSON对象,不是JSON字符串,所以无需解析,很方便(当然也可以直接发送字符串)。
NOTE:
sendResponse():当您产生响应时调用(最多一次)的函数,且参数必须是可转化为 JSON 的对象。
若您在同一个文档中有一个以上的 onMessage 事件处理函数,只有其中一个可以发送响应。
且当事件处理函数返回时,该函数将失效,除非您在事件处理函数中返回 true,
表示您希望通过异步方式发送响应(这样,与另一端之间的消息通道将会保持打开状态,直到调用了 sendResponse)。
sendMessage(integer tabId, any message, function responseCallback):
使用 sendMessage() 发送消息时,当前扩展程序在指定标签页中的每一个内容脚本都会收到 runtime.onMessage 事件
网上有些老代码中用的是 chrome.extension.onMessage
,没有完全查清二者的区别(貌似是别名),但是建议统一使用 chrome.runtime.onMessage
。
此时你能在控制台中,看到你在 popup.js 中的输出内容(console.log()),
并执行 chrome.tab.xx 时输出的内容
打开你激活扩展程序时,所处的网页(标签)的控制台(F12),
在 Console 中你就可以看到你在 contentScripts 已经成功接收到消息,并且可以输出它。
同时我们也会使得 contentScripts 向 popup 发送响应。
当 contentScripts 接收并发送响应(必要的,虽然没有发送响应不会运行扩展基本运行)后,
popup 中的 sendMessage 的第三个参数 callback 将会执行并且第一个参数接收来自 contentScripts 的响应。