之前介绍过一个基于Chrome插件开发的无埋点可视化工具
那么,这篇文章就系统介绍一下Chrome插件开发,代码链接附文后。
什么是Chrome插件
严格来讲,应该叫Chrome扩展(Chrome Extension),真正意义上的Chrome插件需要对浏览器源码有一定掌握才有能力去开发。
Chrome插件是一个用Web技术开发、用来增强浏览器功能的软件。它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。
开发与调试
Chrome插件没有严格的项目结构要求,只要保证根目录有一个manifest.json。
从右上角菜单->更多工具->扩展程序可以进入插件管理页面,也可以直接在地址栏输入chrome://extensions访问。
勾选开发者模式,添加项目文件夹
开发中,代码有任何改动都必须重新加载一下插件
manifest.json
manifest.json是插件的配置文件
一个简单的manifest.json
{
// 清单文件的版本
"manifest_version": 2,
// 插件名称
"name": "demo",
// 插件版本
"version": "0.0.1",
// 插件描述
"description": "content-scripts",
// 图标,一般偷懒全部用一个尺寸的也没问题
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
},
// 直接注入页面的js
"content_scripts": [
{
// <all_urls> 表示匹配所有地址
"matches": ["<all_urls>"],
"js": ["contentScripts/contentScript.js"],
"css": ["contentScripts/contentScript.css"],
// 代码注入的时间,可选值: document_start、document_end、document_idle(页面空闲时),默认document_idle
"run_at": "document_end"
}
]
}
1.content-scripts
content-scripts是Chrome插件向页面注入脚本的一种形式(也可以是css)。
我们借助content-scripts可以自由地修改页面,实现常见的功能,比如:广告屏蔽、页面CSS定制,等等。
content-scripts和原始页面共享DOM,但是不共享JS。如要访问页面JS(例如某个JS变量),只能通过injected js来实现。
content-scripts只能访问部分Chrome扩展API,你可以通过通信让background来帮你调用一些API。
示例
在manifest.json中添加
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["contentScripts/contentScript.js"],
"css": ["contentScripts/contentScript.css"],
"run_at": "document_start"
}
]
这样在contentScript.js里就可以写js脚本,在contentScript.css里可以写css。
2.background
background是一个常驻页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭。所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。
background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools)。而且它可以跨域访问资源而无需对方设置CORS。
示例
在manifest.json中添加
"background": {
"scripts": ["backgrounds/background.js"]
}
3.event-pages
鉴于background生命周期太长,长时间挂载在后台可能会影响性能,所以还有一个event-pages。在配置文件上,它就比background多了一个persistent参数。
它的生命周期是:在被需要时加载,在空闲时被关闭。比如在第一次安装、插件更新、有content-script向它发送消息时加载。
这个简单了解一下就行,一般情况下background也不会很消耗性能。
示例
在manifest.json中添加
"background": {
"scripts": ["eventPages/eventPage.js"],
"persistent": false
}
4.popup
popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。
它和background的权限非常类似。
示例
在manifest.json中添加
"browser_action": {
"default_icon": "icon.png",
"default_title": "popup",
"default_popup": "popup/popup.html"
}
在popup.html里就可以编写窗口网页的代码。
5.injected-script
injected-script并不是一个官方概念,它是指在content-scripts中通过DOM操作的方式向页面注入的JS。
content-script无法访问页面中的JS,但inject-script可以。
示例
在manifest.json中添加
"web_accessible_resources": ["injectedScripts/injected.js"],
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["contentScripts/contentScript.js"],
"run_at": "document_end"
}
]
注意需要在web_accessible_resources设置injected.js访问路径,否则无法访问injected.js。
const injectJS = (jsPath = "injectedScripts/injected.js") => {
const script = document.createElement("script");
script.src = chrome.extension.getURL(jsPath);
script.onload = () => {
script.remove();
};
document.body.appendChild(script);
};
injectJS();
这样在injected.js里写的代码就可以访问页面里的JS了。
6.homepage_url
开发者或者插件主页设置
示例
在manifest.json中添加
"homepage_url": "https://juejin.cn/",
7.pageAction
pageAction,指的是只有当某些特定页面打开才显示的图标,否则是置灰的。可以简单地把它看成是可以置灰的browserAction。
示例
在manifest.json中添加
{
"manifest_version": 2,
"name": "3",
"version": "0.0.1",
"description": "popup",
"icons": {
"16": "icon.png",
"48": "icon.png",
"128": "icon.png"
},
"page_action": {
"default_icon": "icon.png",
"default_title": "pageAction",
"default_popup": "popup/popup.html"
},
"background": {
"scripts": ["backgrounds/background.js"]
},
"permissions": ["declarativeContent"]
}
可以在background中决定是否置灰插件图标
比如设置只有在包含baidu.com字符的url下才能使用插件。
chrome.runtime.onInstalled.addListener(function() {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [
new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlContains: "baidu.com" }
})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}
]);
});
});
8.右键菜单
Chrome插件可以自定义浏览器的右键菜单,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等。
示例
在manifest.json中添加
"background": {
"scripts": ["backgrounds/background.js"]
},
"permissions": ["contextMenus", "tabs"]
比如选中文本后,右键点击"使用度娘搜索"菜单项,打开一个新tab页面使用百度搜索该文本。
// background.js
chrome.contextMenus.create({
type: "normal", // 类型,可选:["normal", "checkbox", "radio", "separator"],默认 normal
title: "使用度娘搜索:%s", // 显示的文字,除非为“separator”类型否则此参数必需,如果类型为“selection”,可以使用%s显示选定的文本
contexts: ["selection"], // 上下文环境,可选:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],默认page
onclick: function(params) {
// 注意不能使用location.href,因为location是属于background的window对象
chrome.tabs.create({
url:
"https://www.baidu.com/s?ie=utf-8&wd=" + encodeURI(params.selectionText)
});
}
});
9.override(覆盖特定页面)
使用override页可以将Chrome默认的一些特定页面改为插件提供的页面。
可替代的页面如下:
- 历史记录
- 新标签页
- 书签
示例
在manifest.json中添加
"chrome_url_overrides": {
"newtab": "overrides/newtab.html"
}
就可以newtab.html替换新标签页了
10.devtools(开发者工具)
Chrome插件可以修改开发者工具(devtools)。主要表现在:
- 自定义和Elements、Console、Sources等同级别的面板
- 自定义侧边栏(sidebar),目前只能自定义Elements面板的侧边栏
devtools页面的生命周期和devtools窗口是一致的。devtools页面可以访问一组特有的DevTools API以及有限的扩展API。
官网的一张图很好说明了devtools和content scripts、backgroud的关系。
示例
代码较多,可以查看文末的源码链接。
11.option
options页就是插件的设置页面,有2个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面。
示例
在manifest.json中添加
"options_page": "options/options.html",
在options.html编写设置页面的代码
12.新版option
新版option采用了弹窗的形式。
示例
在manifest.json中添加
"options_ui": {
"page": "options/options.html",
"chrome_style": true
},
13.omnibox
omnibox是向用户提供搜索建议的一种方式。
14.桌面通知
chrome.notifications API可以推送桌面通知。
示例
在manifest.json中添加
"permissions": ["notifications"],
调用chrome.notifications API即可
// background.js
chrome.notifications.create(null, {
type: "basic",
iconUrl: "icon.png",
title: "桌面通知",
message: Math.random().toString()
});
5种类型的JS对比
Chrome插件的JS主要可以分为这5类:injected script、content-script、popup js、background js和devtools js。
权限对比
调用方式对比
互相通信概览
注:-表示不存在或者无意义,或者待验证。
popup和background其实几乎可以视为一种东西,因为它们可访问的API都一样、通信机制一样、都可以跨域。
15.popup向background发消息
16.background向popup发消息
17.popup或者background向content script发送消息
以background为例
18.content script向background发消息
19.injected script和content script通信
20.长连接和短连接
Chrome插件中有2种通信方式,一种是短连接(chrome.tabs.sendMessage和chrome.runtime.sendMessage),一种是长连接(chrome.tabs.connect和chrome.runtime.connect)。
示例
以popup.js与background长连接通信为例
// background.js
chrome.runtime.onConnect.addListener(function(port) {
if (port.name == "test-connect") {
port.onMessage.addListener(function(data) {
console.log("我是content script,收到popup的消息:", data.msg);
if (data.msg === "我是popup,发送了消息") {
port.postMessage({ msg: "我是content script,发送了消息" });
}
});
}
});
// popup.js
function getCurrentTabId(callback) {
chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) {
if (callback) callback(tabs.length ? tabs[0].id : null);
});
}
getCurrentTabId(tabId => {
if (!tabId) {
return;
}
const port = chrome.tabs.connect(tabId, { name: "test-connect" });
port.postMessage({ msg: "我是popup,发送了消息" });
port.onMessage.addListener(function(data) {
if (data.msg && data.msg === "我是content script,发送了消息") {
port.postMessage({ msg: "我是popup,收到了content script的消息" });
}
});
});
21.本地存储
本地存储建议用chrome.storage而不是普通的localStorage,好处是:
-
chrome.storage是针对插件全局的,比如你在background中保存数据,在content-script中也能获取到。
-
chrome.storage.sync可以跟随当前登录用户自动同步,比如这台电脑修改的设置会自动同步到其它电脑,如果没有登录或未联网则先保存到本地,等登录了再同步至网络。
示例
在manifest.json中添加
"permissions": ["storage"]
以chrome.storage为例
// content script
const get = (key, cb) => {
chrome.storage.local.get({ [key]: "" }, items => {
const value = items[key];
cb(value);
});
};
const set = (key, value, cb) => {
chrome.storage.local.set({ [key]: value }, () => {
cb();
});
};
const key = "key";
set(key, "local", function() {
get(key, function(value) {
console.log("get value: ", value);
});
});
22.webRequest
通过webRequest系列API可以对HTTP请求进行修改、定制。
示例
在manifest.json中添加
"permissions": [
"webRequest",
"webRequestBlocking",
"notifications",
"http://*/*",
"https://*/*"
],
该功能能拦截所有的image请求
// background.js
// web请求监听,最后一个参数表示阻塞式,需单独声明权限:webRequestBlocking
chrome.webRequest.onBeforeRequest.addListener(
details => {
if (details.type === "image") {
chrome.notifications.create(null, {
type: "basic",
iconUrl: "icon.png",
title: "检测到图片",
message: "图片地址:" + details.url
});
}
},
{ urls: ["<all_urls>"] },
["blocking"]
);
23
国际化
示例
在manifest.json中添加
"default_locale": "en",
在根目录创建_locales文件,添加多语言文案
_locales
en
messages.json
zh_CN
messages.json
// en/messages.json
{
"pluginDesc": { "message": "A simple chrome extension demo" }
}
// zh_CN/messages.json
{
"pluginDesc": { "message": "一个简单的Chrome插件demo" }
}
可以通过chrome.i18n.getMessage("pluginDesc")获取相应文案。