Content Scripts
Content scripts是在Web页面内运行的javascript脚本。通过使用标准的DOM,它们可以获取浏览器所访问页面的详细信息,并可以修改这些信息。
限制:
- 不能使用除了
chrome.extension
之外的chrome.*
的接口 - 不能访问它所在扩展中定义的函数和变量
- 不能访问web页面或其它
content script
中定义的函数和变量
这些限制其实并不像看上去那么糟糕。Content scripts
可以使用messages
机制与它所在的扩展通信,来间接使用chrome.*
接口,或访问扩展数据。Content scripts
还可以通过共享的DOM来与web页面通信
声明
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["http://www.google.com/*"], // 匹配模式,声明哪些网站会注入脚本
"css": ["mystyles.css"], // 注入的样式
"js": ["jquery.js", "myscript.js"] //注入的js脚本
}
],
...
}
使用 content_scripts
字段,一个扩展可以向一个页面注入多个content_script脚本;每个content script
脚本可以包括多个javascript
脚本和css
文件。content_script
字段中的每一项都可以包括下列属性:
字段名 | 类型 | 说明 |
---|---|---|
matches | array of strings | 必须。 定义哪些页面需要注入content script。具体见匹配模式 |
css | array of strings | 可选。需要向匹配页面中注入的CSS文件。这些文件将在页面的DOM树创建和显示之前,按照定义的顺序依次注入。 |
js | array of strings | 可选。 需要向页面中注入的javascript文件,按定义顺序注入。 |
run_at | string | 可选。 控制content script注入的时机。可以是document_start, document_end或者document_idle。默认是document_idle。如果是document_start, 文件将在所有CSS加载完毕,但是没有创建DOM并且没有运行任何脚本的时候注入。如果是document_end,则文件将在创建完DOM之后,但还没有加载类似于图片或frame等的子资源前立刻注入。如果是document_idle,浏览器会在document_end和发出window.onload事件之间的某个时机注入。具体的时机取决与文档加载的复杂度,为加快页面加载而优化。 |
all_frames | boolean | 缺省是false,也就是只在最上层frame中运行。 |
include_globs | array of string | 可选(包含)。控制将content_script注入到哪些匹配的页面中。模拟Greasemonkey中的@include关键字。 |
exclude_globs | array of string | 可选(排除)。控制将content_script注入到哪些匹配的页面中。模拟Greasemonkey中的@exclude关键字。 |
Include和exclude语句
一个content script
被注入页面的条件是: 页面url
匹配 matches
模式中任意一项以及 include_globs
中任一项,并且不匹配任何 exclude_matches
或 exclude_globs
模式。由于 matches
属性是必选的,exclude_matches
、include_globs
以及 exclude_globs
都只能用来限制哪些匹配的页面会被影响。
另外, 这两个属性与matches
属性的语法是不同的, 它们更灵活一些。 在这两个属性中可以包含*
号和?
号作为通配符。 其中*
可以匹配任意长度的字符串,而?
匹配任意的单个字符。
声明式demo
将h2标题的颜色改为蓝色,一般刷新页面就会生效
"content_scripts": [
{
"matches": [
"https://*/*"
],
"js": [
"jquery.js",
"a.js"
]
}
]
//a.js
$('h2').css('color', 'blue')
编程时注入
搞了大半天一直不行,最后才查到v3
中已经废除了executeScript()
和insertCSS()
,统一使用chrome.scripting.executeScript
;并且必须在配置中声明如下权限。真tm服了,国内看个最新文档都看不了,不明白为啥这也要被墙。
"permissions": [
"scripting",
"tabs"
],
注入文件,以js为例
$('#zhuru').click(e => {
//先获取tab的id
chrome.tabs.query({
active: true, currentWindow: true }, function (tabs) {
let tabId = tabs.length ? tabs[0].id : null
chrome.scripting.executeScript({
target: {
tabId: tabId },
files: ['jquery.js', 'a.js']
});
});
})
注入css
const css = 'body { background-color: red; }';
chrome.tabs.query({
currentWindow: true,
active: true
}, function (tabs) {
console.log(tabs[0].id)
chrome.scripting.insertCSS({
target: {
tabId: tabs[0].id },
css: css,
});
})
注入函数
chrome.scripting.executeScript({
target: {
tabId: tabId },
func: text
});
//注入页面执行的函数
function text() {
alert("我是测试js代码")
}
注意事项:
1.在使用scripting时注意需要等页面加载完再注入
2.使用函数注入方法时,注入的函数无法接收全局变量,应该使用args参数定义一下变量
const color ="red"
function changeBackgroundColor(backgroundColor) {
document.body.style.backgroundColor=backgroundColor;
}
chrome.scripting.executeScript({
target: {
tabId: tabId},
func: changeBackgroundColor,
args: [color],
});
3.如果想要在iframe页面也执行的话需要增加:allFrames: true
chrome.scripting.executeScript({
target: {
tabId: tabId, allFrames: true},
files: ['script.js'],
});
//指定iframeID执行
const frameIds = [frameId1, frameId2];
chrome.scripting.executeScript({
target: {
tabId: tabId, frameIds: frameIds},
files: ['script.js'],
});
执行环境
Content script是在一个特殊环境中运行的,这个环境成为isolated world(隔离环境)。它们可以访问所注入页面的DOM,但是不能访问里面的任何javascript变量和函数。 对每个content script来说,就像除了它自己之外再没有其它脚本在运行。 反过来也是成立的: 页面里的javascript也不能访问content script中的任何变量和函数。
隔离环境使得content script可以修改它的javascript环境而不必担心会与这个页面上的其它content script冲突。 例如,一个content script可以包含JQuery v1而页面可以包含JQuery v2, 它们之间不会产生冲突。
另一个重要的优点是隔离环境可以将页面上的脚本与扩展中的脚本完全隔离开。这使得开发者可以在content script中提供更多的功能,而不让web页面利用它们。
与嵌入的页面通信
尽管content script的执行环境与所在的页面是隔离的,但它们还是共享了页面的DOM。 如果页面需要与content script通信(或者通过content script与扩展通信), 就必须通过这个共享的DOM。
下面这个例子是通过自定义的DOM事件和把数据放到固定的地方来实现的:
http://foo.com/example.html
===========================
var customEvent = document.createEvent('Event');
customEvent.initEvent('myCustomEvent', true, true);
function fireCustomEvent(data) {
hiddenDiv = document.getElementById('myCustomEventDiv');
hiddenDiv.innerText = data
hiddenDiv.dispatchEvent(customEvent);
}
contentscript.js
================
var port = chrome.extension.connect();
document.getElementById('myCustomEventDiv').addEventListener('myCustomEvent', function() {
var eventData = document.getElementById('myCustomEventDiv').innerText;
port.postMessage({
message: "myCustomEvent", values: eventData});
});
在上面的例子中,html页面(不属于扩展)创建了一个自定义事件, 当它向DOM中的一个特定元素写入事件数据后就会激活并派发这个自定义事件。 Content script在这个特定元素上监听这个自定义事件, 从这个元素中获取数据,并向扩展进程post一个消息。 通过这种方式, 页面建立了与扩展的通信链接。 这个方法也适用于反向的通信。
引用扩展里的文件
通过chrome.extension.getURL()来获取扩展里文件的URL。可以像使用其它url一样使用这些URL,如下面的例子所示:
//Code for displaying /images/myimage.png:
var imgURL = chrome.extension.getURL("images/myimage.png");
document.getElementById("someImage").src = imgURL;
跨域请求
跨域请求 XMLHttpRequest
普通网页能够使用XMLHttpRequest对象发送或者接受服务器数据,但是它们受限于同源策略。扩展可以不受该限制。任何扩展只要它先获取了跨域请求许可,就可以进行跨域请求。
注意:页面内容脚本不能直接发起跨域请求。然而,任何一个页面内容脚本都可以发送消息给父扩展,请求父扩展发起一次跨域请求
扩展所属域
每个正在运行的扩展都存在于自己独立的安全域里。当没有获取其他权限时,扩展能够使用XMLHttpRequest获取来自安装该扩展的域的资源。例如,假设有一个扩展包含一个叫config.json的JSON配置文件,该文件位于config_resources目录,那么该扩展能够使用下面这段代码获取文件内容:
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = handleStateChange; // Implemented elsewhere.
xhr.open("GET", chrome.extension.getURL('/config_resources/config.json'), true);
xhr.send();
如果某个扩展希望访问自己所属域以外的资源,比如说来自http://www.google.com的资源(假设该扩展不是来自www.google.com),浏览器不会允许这样的请求,除非该扩展获得了相应的跨域请求允许。
获取跨域请求允许
通过添加域名或者域名匹配到manifest文件的permissions段,该扩展就拥有了访问除了自己所属域以外的其他域的访问权限。
跨域可以使用完整域名,也可以使用模式匹配
{
"name": "My extension",
...
// v2
"permissions": [
"http://www.google.com/"
],
//v3,通过使用host_permissions属性来声明
"host_permissions": [
"https://*/*"
],
...
}
国际化
略,感兴趣的可以查看 http://chrome.cenchy.com/i18n.html
NPAPI 插件
略,感兴趣的可以查看 http://chrome.cenchy.com/npapi.html
消息传递
自从content script内容运行在网页环境而不是在扩展中,我们经常需要一些方法和其余的扩展进行通信。例如,一个 RSS阅读扩展可能会使用content scripts去检测RSS阅读扩展应该提供给哪个页面,然后通知后台页面以便显示一个页面交互图标。
通过使用消息传递机制在扩展和content scripts中通信,任何一方可以收到另一方传递来的消息,并且在相同的通道上答复。这个消息可以包含 任何一个有效的JSON对象(null, boolean, number, string, array, or object)。这里有一些简单的API对于 一次简单的请求,还有更多复杂的API,它们可以帮助你长时间保持连接在一个共享的环境中交换大量的信息,它也可以帮助你传递消息给另外一个你知道ID的扩展 .
一次简单的请求
如果你仅仅是发送一个简单的消息给你的扩展的另一部分,可以使用chrom.runtime.sendMessage
或者 chrome.tabs.sendMessage
这使你可以将JSON可序列消息从内容脚本(注入到页面的脚本)发送到扩展,反之亦然。可选的回调参数运行你处理来自另一侧的响应。
从内容脚本发送消息到扩展
//在你往页面里注入的js文件里发送消息,这个就是前面讲的 Content Scripts
chrome.runtime.sendMessage({
greeting: "hello" }, function (response) {
alert(response.farewell)
});
// 在扩展的js里接收消息,这里指的是background.js,常驻扩展的后台
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
//判断发送方是否是tab页
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
//如果请求消息是...,一般使用对象
if (request.greeting === "hello")
//像发送方发送消息,表面已经收到了消息
sendResponse({
farewell: "goodbye" });
}
);
先发送消息,接收方收到了
接收方收到了给发送方回消息
注意: 如果想要在控制台看到扩展的日志消息需要声明 background.js
并创建background.html
(应该也可以取别的名字,但是js和html名字必须一致)
"background": {
"service_worker": "background.js"
},
然后通过如下代码打开后台页面,这样才能在后台页面的控制台里看到消息
$('#open_background').click(e => {
window.open(chrome.runtime.getURL('background.html'));
});
从扩展发消息到内容脚本
与从内容脚本发消息到扩展相似,从扩展发消息到内容脚本需要将消息发送到哪一个tab(选项卡)
//在扩展主页面引用的js里发消息
$('#once').click(e => {
chrome.tabs.query({
active: true, currentWindow: true }, function (tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
greeting: "hello" }, function (response) {
//如果内容脚本收到消息并向扩展回复时显示一下
alert(response.farewell)
});
});
})
//在内容脚本里接收消息
chrome.runtime.onMessage.addListener(
function (request, sender, sendResponse) {
console.log(sender.tab ?
"from a content script:" + sender.tab.url :
"from the extension");
//脚本收到消息后,向扩展回复消息
if (request.greeting === "hello")
sendResponse({
farewell: "goodbye" });
}
);
长时间的保持连接
有时候持续长时间的保持会话会比一次简单的请求有用。你可以建立一个长时间存在的通道从content script到扩展, 反之亦然,使用chrome.runtime.connect()
或者chrome.tabs.connect()
方法,你可以把这个通道命名,为了更方便区分不同类型的连接。
一个有用的例子就是自动填写表单扩展。content script可以建立一个通道在登录页面和扩展之间,同时发出一条消息给扩展,告诉扩展需要填写的内容。共享的连接允许扩展保持共享状态,从而连接几个来自content script.的消息。
当建立连接,两端都有一个端口对象,用于通过该链接发送和接收消息。
从content script建立一个通道,发送和接受消息:
//在注入的脚本里使用如下代码
//定义一个端口,名字是knockknock
var port = chrome.runtime.connect({
name: "knockknock" });
//通过端口发送消息
port.postMessage({
joke: "Knock knock" });
//通过端口回复消息
port.onMessage.addListener(function (msg) {
if (msg.question === "Who's there?") {
port.postMessage({
answer: "Madame" });
} else if (msg.aa === 'ok') {
alert("不聊了")
}
});
在扩展里接收消息
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (msg) {
if (msg.joke === "Knock knock") {
port.postMessage({
question: "Who's there?" });
} else if (msg.answer === 'Madame') {
port.postMessage({
aa: "ok" });
}
});
})
总结
1、如果是内容脚本发送消息,那么接收方的消息监听要写在background.js
里,保持监听一直存在。
2、如果扩展发消息,一般现在扩展的主页面里,这样可以有一些交互(比如点击按钮);接收消息的监听写在内容脚本里。
其他
关于扩展之间的消息传递以及其他消息传递内容一般用不上,暂时不学习,感兴趣的可以自己在官方文档中查看。
可选权限
chrome.permissions
用于实现可选权限。在您扩展的运行过程中,而不是在安装的时候请求权限。这能帮助用户清楚为什需要此权限,且仅在必要的时候才运行扩展使用此权限。
声明
{
"name": "Permissions Extension",
...
// 声明必须权限
"permissions": [
"activeTab",
"contextMenus",
"storage"
],
// 声明哪些权限是可选权限
"optional_permissions": [
"topSites",
],
// 声明主机权限,应该是用于说明权限在哪些网站有用
"host_permissions": [
"https://www.developer.chrome.com/*"
],
...
"manifest_version": 3
}
其他常用可选权限如下(也可以当必须权限使用):
未介绍的可以看:在扩展清单中声明 API 权限
activeTab
activeTab 权限使扩展程序在用户调用扩展程序(例如单击浏览器按钮)时能够临时访问当前活动的标签页,对标签页的访问将一直持续到标签页导航或关闭
alarms
类似于定时器,传统定时器在backgroun.js
里无效
bookmarks
是用来操作chrome收藏夹栏的api,可以用于获取、修改、创建收藏夹内容。
clipboardRead
剪切板读取权限,如果扩展使用 document.execCommand(‘paste’),则为必需。
clipboardWrite
剪切板写入权限,指示扩展使用 document.execCommand(‘copy’) 或 document.execCommand(‘cut’)。
contextMenus
右键菜单权限,提供对 API 的 chrome.contextMenus 扩展访问权限。
desktopCapture
chrome.desktopCapture可以被用来对整个屏幕,浏览器或者某个页面截图(实时)。
downloads
chrome.downloads是用来操作chrome中下载文件相关的api,可以创建下载,继续、取消、暂停,甚至可以打开下载文件的目录或打开下载的文件。
这个api在manifest中需要申请downloads权限,如果想要打开下载的文件,还需要申请downloads.open权限。
请求可选权限
通过调用permissions.request()请求权限,并且需要获得用户授权:
document.querySelector('#my-button').addEventListener('click', function(event) {
// Permissions must be requested from inside a user gesture, like a button's
// click handler.
chrome.permissions.request({
permissions: ['tabs'],
origins: ['http://www.google.com/']
}, function(granted) {
// The callback argument will be true if the user granted the permissions.
if (granted) {
doSomething();
} else {
doSomethingElse();
}
});
});
检查扩展的当前权限
检查扩展是否拥有特定的权限,可以通过permission.contains()实现:
chrome.permissions.contains({
permissions: ['tabs'],
origins: ['http://www.google.com/']
}, function(result) {
if (result) {
// The extension has the permissions.
} else {
// The extension doesn't have the permissions.
}
});
删除权限
您应该删除不再需要的权限。当某个用户已授权权限被删除后,使用permissions.request()再次添加此权限时不会再提示用户。
chrome.permissions.remove({
permissions: ['tabs'],
origins: ['http://www.google.com/']
}, function(removed) {
if (removed) {
// The permissions have been removed.
} else {
// The permissions have not been removed (e.g., you tried to remove
// required permissions).
}
});
事件
onAdded
chrome.permissions.onAdded.addListener(function(Permissions permissions) {
...});
在扩展获得新的权限时触发。
onRemoved
chrome.permissions.onRemoved.addListener(function(Permissions permissions) {
...});
当移出权限时触发