一、资料来源
- 插件开发官方文档主页
https://developer.chrome.com/docs/extensions/
- manifest清单文件
https://developer.chrome.com/extensions/manifest
- permissions权限
https://developer.chrome.com/extensions/permissions
- api文档
https://developer.chrome.com/extensions/api_index
- 匹配规则
https://developer.chrome.com/extensions/match_patterns
二、前言
1. 什么是Chrome插件?
Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件,可以改变浏览器行为,添加新的功能,从而提高用户体验。需要的知识:HTML,JavaScript,CSS,谷歌浏览器插件接口。
2. Chrome插件有什么好处,能干什么?
提供已有功能和各种API,进行功能组合,从而增强浏览器网页的功能,改善浏览器体验,轻松实现属于自己的 “定制版” 浏览器。
通过Chrome插件,你可以实现包括但不限于以下相关功能:
- 书签控制
- 下载控制
- 窗口控制
- 标签控制
- 网络请求控制
- 各类事件监听
- 自定义原生菜单
- 完善的通信机制
三、开发与调试
- Chrome插件只要保证目录有一个`manifest.json`即可
- 可通过浏览器右上角 更过工具->扩展程序->管理扩展程序 进入插件管理页面,也可以地址栏输入 chrome://extensions/ 进入
- 需打开开发者模式,然后可以直接加载文件夹安装插件,否则只能安装`.crx`格式的文件。
四、学习内容
1. 基本模块了解
- manifest.json(Chrome 扩展的配置文件,必须文件)
- service-worker.js(浏览器在后台运行的基于事件的脚本,通常用于处理数据的事件管理器,使用场景例如:首次安装扩展做某些操作,定义右键菜单,content通信监听等)
- content-script.js(允许扩展程序与浏览器中的页面交互并修改页面。例如,可以在页面上插入新元素、更改网站的样式等)
- 扩展页面(popup.html插件弹窗页,options.html选项页,sidepanel.html侧面板)
2. manifest.json基础
{
"name": "demo",//插件名称
"description": "这是一个简单的插件demo!",//插件介绍
"version": "1.0",//插件版本
"manifest_version": 3,//V3版本
"background": {
"service_worker": "background.js" //后台服务js
},
"permissions": ["tabs", "storage", "contextMenus", "notifications", "activeTab", "scripting"],//权限
"action": {
"default_popup": "popup.html",//点击插件弹出的文件
"default_title": "DEMO测试",//地址栏下的title
"default_icon": {//地址栏下的图标设置
"16": "/images/demo16.png",
"32": "/images/demo32.png",
"48": "/images/demo48.png",
"128": "/images/demo128.png"
}
},
"icons": {//开发者插件图标
"16": "/images/demo16.png",
"32": "/images/demo32.png",
"48": "/images/demo48.png",
"128": "/images/demo128.png"
},
"options_page": "options.html",//右键插件图标的->选项页面
"homepage_url":"https://www.bilibili.com/",//插件主页,可跳转到配置的url
"content_scripts": [
{
//"matches": ["http://*/*", "https://*/*"],
// "<all_urls>" 表示匹配所有地址
"matches": ["<all_urls>"],
// 多个JS按顺序注入
"js": ["js/content-script.js"],
// 注入到匹配页面的 CSS
"css": ["css/custom.css"],
// 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
"run_at": "document_start",
"match_about_blank":false //是否注入父框架
}
],
//快捷键
"commands": {
"_execute_action": {
"suggested_key": {
"windows": "Ctrl+Shift+Y",
"mac": "Command+Shift+Y",
"chromeos": "Ctrl+Shift+U",
"linux": "Ctrl+Shift+J"
}
}
},
//使用 Google Chrome 的地址栏(也称为多功能框)注册关键字
"omnibox": { "keyword" : "test" },
//将内容与网页的主要内容一起托管在浏览器侧面板中
"side_panel": {
"default_path": "side_panel.html",
"openPanelOnActionClick": true //点击图标启用
}
}
3. 插件权限
- 基本介绍
大致权限分为必需权限与可选权限
- 权限配置
- permissions
声明必须权限
- optional_permissions
声明可选权限,需要在运行时由用户确认
- host_permissions
声明必须 host 权限,允许扩展程序访问的目标网站
- 注意项
- 不管是什么权限,都需要在 manifest.json 中声明
- 如果是必须权限,声明完就可以了,但如果是可选权限,则还需要在代码中请求用户授权
// 请求可选权限
chrome.permissions.request({
permissions: ['tabs'],
origins: ['https://www.google.com/']
}, (granted) => {
if (granted) {
doSomething();
} else {
doSomethingElse();
}
})
// 检查扩展程序当前权限
chrome.permissions.contains({
permissions: ['tabs'],
origins: ['https://www.google.com/']
}, (result) => {
if (result) {
// The extension has the permissions.
} else {
// The extension doesn't have the permissions.
}
});
// 删除权限
chrome.permissions.remove({
permissions: ['tabs'],
origins: ['https://www.google.com/']
}, (removed) => {
if (removed) {
// The permissions have been removed.
} else {
// The permissions have not been removed (e.g., you tried to remove
// required permissions).
}
});
4. 消息传递
内容脚本在网页而不是扩展的上下文中运行,因此它们通常需要某种方式与扩展的其余部分进行通信。扩展及其内容脚本之间的通信通过使用消息传递进行。任何一方都可以监听另一端发送的消息,并在同一通道上做出响应。
- 一次性消息传递
- runtime.sendMessage
- tabs.sendMessage
// content 向 background或popup传递
(async () => {
const response = await chrome.runtime.sendMessage({greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
// background或popup接收
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);
// background或popup 向content传递
(async () => {
const [tab] = await chrome.tabs.query({active: true, lastFocusedWindow: true});
const response = await chrome.tabs.sendMessage(tab.id, {greeting: "hello"});
// do something with response here, not outside the function
console.log(response);
})();
// content接收
chrome.runtime.onMessage.addListener(
function(request, sender, sendResponse) {
if (request.greeting === "hello")
sendResponse({farewell: "goodbye"});
}
);
- 长期连接
- runtime.connect
- tabs.connect
// content 发送及监听
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.question === "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
// 扩展程序向内容脚本
var port = chrome.tabs.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.question === "Madame who?")
port.postMessage({answer: "Madame... Bovary"});
});
// 内容或扩展页
chrome.runtime.onConnect.addListener(function(port) {
console.assert(port.name === "knockknock");
port.onMessage.addListener(function(msg) {
if (msg.joke === "Knock knock")
port.postMessage({question: "Who's there?"});
else if (msg.answer === "Madame")
port.postMessage({question: "Madame who?"});
else if (msg.answer === "Madame... Bovary")
port.postMessage({question: "I don't get it."});
});
});
- 扩展程序通信
- runtime.onMessageExternal
- runtime.onConnectExternal
// 监听
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.id === blocklistedExtension)
return; // don't allow this extension access
else if (request.getTargetData)
sendResponse({targetData: targetData});
else if (request.activateLasers) {
var success = activateLasers();
sendResponse({activateLasers: success});
}
});
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
// 发送
// The ID of the extension we want to talk to.
var laserExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(laserExtensionId, {getTargetData: true},
function(response) {
if (targetInRange(response.targetData))
chrome.runtime.sendMessage(laserExtensionId, {activateLasers: true});
}
);
// Start a long-running conversation:
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
- 从网页发送消息
扩展程序可以接收来自网站的消息,使用此功能需要在manifest.json中指定externally_connectable特定的url
// 配置示例
"externally_connectable": {
"matches": ["https://*.example.com/*"]
}
- 从网站向扩展程序发送
// The ID of the extension we want to talk to.
var editorExtensionId = "abcdefghijklmnoabcdefhijklmnoabc";
// Make a simple request:
chrome.runtime.sendMessage(editorExtensionId, {openUrlInEditor: url},
function(response) {
if (!response.success)
handleError(url);
});
// connect
var port = chrome.runtime.connect(laserExtensionId);
port.postMessage(...);
- 扩展程序接收
// onMessageExternal
chrome.runtime.onMessageExternal.addListener(
function(request, sender, sendResponse) {
if (sender.url === blocklistedWebsite)
return; // don't allow this web page access
if (request.openUrlInEditor)
openUrl(request.openUrlInEditor);
});
// onConnectExternal
chrome.runtime.onConnectExternal.addListener(function(port) {
port.onMessage.addListener(function(msg) {
// See other examples for sample onMessage handlers.
});
});
5. 安全相关
- 通过将侦听器限制为扩展程序所期望的内容、验证传入数据的发送者以及清理所有输入,保护扩展程序免受恶意脚本的侵害。
- 发送消息接收返回的信息避免使用eval,innerHTML 等操作,避免侵害。
6. 存储storage
使用 chrome.storage API 存储、检索和跟踪对用户数据的更改。
- storage.local(数据存储在本地,当扩展被删除时,数据将被清除。)
- storage.sync(如果启用同步,数据将同步到用户登录的任何 Chrome 浏览器。如果禁用,其行为类似于storage.local. 当浏览器离线时,Chrome 会在本地存储数据,并在重新上线时恢复同步。)
- storage.session(在浏览器会话期间将数据保存在内存中。默认情况下,它不会暴露给内容脚本,但可以通过设置更改此行为,在浏览器会话期间将数据保存在内存中。默认情况下,它不会暴露给内容脚本,但可以通过设置更改此行session.setAccessLevel)
- managed(managed存储区域要求其结构声明为JSON Schema,并由 Chrome 严格验证。)
需要权限清单
{
...
"permissions": [
"storage"
],
}
基本使用方式
// local
chrome.storage.local.set({ key: value }).then(() => {
console.log("Value is set");
});
chrome.storage.local.get(["key"]).then((result) => {
console.log("Value currently is " + result.key);
});
// sync
chrome.storage.sync.set({ key: value }).then(() => {
console.log("Value is set");
});
chrome.storage.sync.get(["key"]).then((result) => {
console.log("Value currently is " + result.key);
});
// session
chrome.storage.session.set({ key: value }).then(() => {
console.log("Value was set");
});
chrome.storage.session.get(["key"]).then((result) => {
console.log("Value currently is " + result.key);
});
// managed
// 先声明
{
"name": "My enterprise extension",
"storage": {
"managed_schema": "schema.json"
},
...
}
// Chrome 的 JSON Schema 格式有一些额外的要求:
// 顶级架构必须具有类型object。
// 顶层object不能有additionalProperties。所properties声明的是本次延期的政策。
// 每个模式必须有一个$ref值或恰好有一个值type。
其他使用方式
// 以storage.sync举例
// 删除指定key的一个或多个数据项
chrome.storage.sync.remove(keys, function() {...})
// 清空存储的所有数据项
chrome.storage.sync.clear(function(){…})
// 获取当前已经被使用的存储空间的数量(以字节为单位)
chrome.storage.sync.getBytesInUse(keys, function(integer bytesInUse) {...})
//对于某些敏感数据的变化,可以通过onChanged事件进行监听。存储格子中的任何变化都将触发该事件
chrome.storage.onChanged.addListener(function(changes, namespace) {
for (key in changes) {
var storageChange = changes[key];
console.log(
key, //数据的索引key
namespace, //数据的存储空间类型,枚举值"sync", "local", "managed"
storageChange.oldValue,//变化前的值
storageChange.newValue); //变化后的值
}
});
7. 注入脚本
- 静态声明
manifest.json 中使用静态内容脚本声明。
{
"name": "My extension",
...
"content_scripts": [
{
"matches": ["https://*.nytimes.com/*"],
"css": ["my-styles.css"],
"js": ["content-script.js"]
}
],
...
}
- 动态声明
使用chrome.scripting API
- 注册内容脚本。
- 获取已注册内容脚本列表。
- 更新已注册内容脚本列表。
- 删除注册的内容脚本。
chrome.scripting
.registerContentScripts([{
id: "session-script",
js: ["content.js"],
persistAcrossSessions: false,
matches: ["*://example.com/*"],
runAt: "document_start",
}])
.then(() => console.log("registration complete"))
.catch((err) => console.warn("unexpected error", err))
chrome.scripting
.updateContentScripts([{
id: "session-script",
excludeMatches: ["*://admin.example.com/*"],
}])
.then(() => console.log("registration updated"));
chrome.scripting
.getRegisteredContentScripts()
.then(scripts => console.log("registered content scripts", scripts));
chrome.scripting
.unregisterContentScripts({ ids: ["session-script"] })
.then(() => console.log("un-registration complete"));
- 编程方式
要以编程方式注入内容脚本,您的扩展需要其尝试注入脚本的页面的主机权限。主机权限可以通过请求作为扩展程序清单的一部分来授予(host_permissions),也可以通过manifest.json中permissions声明activeTab临时授予。
// 文件注入
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ["content-script.js"]
});
});
// 函数注入
// 请注意,注入的函数是调用中引用的函数的副本,而不是原始函数本身。因此,函数体必须是独立的;对函数外部变量的引用将导致内容脚本抛出
function injectedFunction(color) {
document.body.style.backgroundColor = color;
}
chrome.action.onClicked.addListener((tab) => {
chrome.scripting.executeScript({
target : {tabId : tab.id},
func : injectedFunction,
args : [ "orange" ]
});
});
其他使用
// 返回与给定过滤器匹配的此扩展的所有动态注册的内容脚本
chrome.scripting.getRegisteredContentScripts(
filter?: ContentScriptFilter,
callback?: function,
)
// 将 CSS 样式表插入到目标上下文中。
chrome.scripting.insertCSS(
injection: CSSInjection,
callback?: function,
)
// 注册一个或多个内容脚本。
chrome.scripting.registerContentScripts(
scripts: RegisteredContentScript[],
callback?: function,
)
// 从目标上下文中删除此扩展先前插入的 CSS 样式表。
chrome.scripting.removeCSS(
injection: CSSInjection,
callback?: function,
)
// 取消注册此扩展的内容脚本。
chrome.scripting.unregisterContentScripts(
filter?: ContentScriptFilter,
callback?: function,
)
// 更新此扩展的一个或多个内容脚本。
chrome.scripting.updateContentScripts(
scripts: RegisteredContentScript[],
callback?: function,
)
8. 国际化i18n
实现国际化需要创建固定文件夹_locales/_localeCode_国家语言前缀/messages.json
_locales/zh_CN/messages.json
_locales/en/messages.json
指定默认语言
//manifest.json
{
...
"default_locale":"zh_CN"
}