Chrome 插件开发指南
开发与调试
chrome插件没有严格的项目结构要求,只有保证本目录有一个 manifest.json
即可,从浏览器菜单-更多工具-扩展程序
可以进入插件管理页面。或直接输入地址 chrome://extensions访问。
勾选开发者模式可以用文件夹的形式直接加载插件,否则只能安装.crx 格式的文件。
mac 系统下插件安装目录为: ~/Library/Application Support/Google/Chrome/Default/Extensions
核心介绍
manifest.json
用来配置插件相关的配置信息,必须放在根目录。且以下属性是必不可少的。完整属性可以查看官方文档。
1
2
3
4
5{
"manifest_version" : 2,
"name" : "test",
"version" : "1.0.0"
}content-scripts (插件与页面交互)
是 chrome 插件向页面注入脚本的一种形式,我们可以通过manifest.json配置轻易的向页面注入 js 和 css,最常见的是广告屏蔽,页面样式定制等等.
1
2
3
4
5
6
7
8
9
10{
"content-scripts" : [
{
"matches" : ["http://*/*", "<all_urls>"],
"js": ["js/xxx.js","....js"],
"css": ["css/xx.css"],
"run_at": "document_start" // 可选 document_start/end/idle(默认空闲)
}
]
}content-scripts 与原始页面共享DOM,但不共享JS,如果想要访问页面JS某个变量,只能通过 injected js 来实现,content-scripts不能访问绝大部分chrome.xxx.api, 除了以下四种。
- chrome.extension(getURL,inIncognitoContext, lastError,onRequest,sendRequest) - chrome.i18N - chrome.runtime(connect,getManifest,getURL,id, onConnect,onMessage,sendMessage) - chrome.storage
background
后台是一个常驻的页面,它随着浏览器的开关而开关.通常把需要一直运行的代码放在 background 里面.
他的权限非常高,可以调用 chrome 的扩展 API(除了 devtools), 而且它可以无限制跨域.配置中,background 可以通过 page 指定一个页面,也可以通过 scripts 指定一个 js,chrome 会自动为这个 js 生成一个默认页面.1
2
3
4
5
6{
"background":{
"page": "xxx.html"
// scripts: ["js/xxx.js"]
}
}event-pages
鉴于 background 生命周期和浏览器同步,长时间挂载后台影响性能,而 event-pages 与 background 唯一的区别就是多了一个 persistent 参数,它会在需要时被加载,空闲时被关闭.一般 background 用的比较多.popup
popup 是点击插件图标时打开的一个窗口网页,焦点离开网页就关闭,一般做一些交互使用.
popup 可以包含任意你想要的 HTML,并且会自适应大小,可以通过 default_popup 来指定页面,也可以调用 setPopup()方法.1
2
3
4
5"browser_action" : {
"default_icon": "img/xx.png",
"default_title": "悬停时的标题",
"default_popup": "xx.html"
}popup 的生命和周期很短,需要长时间运行的代码不要放在 popup 里.popup 中可以通过 chrome.extension.getBackgroundPage() 获取 background 的 window 对象.
injected-script
content-script 无法访问页面中的 js,虽然它可以操作 dom,但 dom 却不能调用它,也就是在 dom 的事件中无法调用 content-script 中的代码,但是在页面中添加一个按钮并调用插件的 api 是很常见的需求,我们可以再 content-script 中通过 DOM 方式向页面注入 inject-script.1
2
3
4
5
6
7
8
9
10// content-script
function injectCustomJs(jsPath){
jsPath = jsPath || 'js/inject.js';
var temp = document.creatElement("script");
temp.src = chrome.extension.getURL(jsPath); // 类似于 chrome-extension://xxxx/js/inject.js
temp.onload = function(){
this.parentNode.removeChild(this);
}
document.head.appendChild(temp);
}代码会报错,因为在 web 中直接访问插件中的资源必须显示声明才行,在配置文件中增加以下配置:
1
2
3
4{
// 普通页面能够直接访问的插件资源列表,不设置无法直接访问.
"web_accessible_resources": ["js/inject.js"]
}inject-script 如何调用 content-script 中的代码,要用到消息通信.
homepage_url
开发者网站
Chrome 插件的 8 种展示形式
browserAction 浏览器右上角图标
1
2
3
4
5
6
7{
"browser_action" : {
"default_icon": "img/xx.png", // 19*19
"default_title": "悬停时的标题",
"default_popup": "xx.html"
}
}通过 setIcon 更改 icon, setTitle()更改鼠标 hover 时的标题.setBadgeText()来更改图标上的文本信息.
pageAction 地址栏右侧
当某些特定页面打开时会在地址栏右边显示的图标.(新版吧位置放到了浏览器右边,可以把它看成置灰的 browserAction.
例如当打开百度时才显示图标.1
2
3
4
5
6
7
8
9
10
11
12
13
14// background.js
chrome.runtime.onInstalled.addListener(function(){
chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [
// 只有打开百度才显示pageAction
new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: 'baidu.com'}})
],
actions: [new chrome.declarativeContent.ShowPageAction()]
}
]);
});
});右键菜单
通过 chrome.contextMenus API,右键菜单可以出现在不同的上下文.1
2
3
4
5
6
7// manifest.json
{"permissions": ["contextMenus"]}
// background.js
chrome.contextMenus.create({
title: "测试右键菜单",
onclick: function(){alert('您点击了右键菜单!');}
});覆盖特定页面
使用 override 页可以将 chrome 默认的一些特定页面替换掉.一个插件只能替代一个默认页.1
2
3
4
5
6"chrome_url_overrides":
{
"newtab": "newtab.html",
"history": "history.html",
"bookmarks": "bookmarks.html"
}devtools(开发者工具)
例如 vue.js devtools , chrome 可以再 devtools 上新增一个面板.
devtools 页面会随着开发者工具的开关而开关.可以访问 Devtools API ,而其他比如 background 无权访问.- chrome.devtools.panels: 面板相关
- chrome.devtools.inspectedWindow: 获取被审查窗口的信息
chrome.devtools.network: 获取有关网络请求的信息
1
2
3{
"devtools_page": "XXX.html"
}这个 html 一般什么都没有,只有一个 script 标签引用 js 文件
<script src='js/devtools.js'></script>
,
再看一下 devtools 的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 创建自定义面板,同一个插件可以创建多个自定义面板
// 几个参数依次为:panel标题、图标(其实设置了也没地方显示)、要加载的页面、加载成功后的回调
chrome.devtools.panels.create('MyPanel', 'img/icon.png', 'mypanel.html', function(panel)
{
console.log('自定义面板创建成功!'); // 注意这个log一般看不到
});
// 创建自定义侧边栏
chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar)
{
// sidebar.setPage('../sidebar.html'); // 指定加载某个页面
sidebar.setExpression('document.querySelectorAll("img")', 'All Images'); // 通过表达式来指定
//sidebar.setObject({aaa: 111, bbb: 'Hello World!'}); // 直接设置显示某个对象
});
// 访问被检查的页面DOM需要使用inspectedWindow
chrome.devtools.inspectedWindow.eval("jQuery.fn.jquery", function(result, isException)
{
var html = '';
if (isException) html = '当前页面没有使用jQuery。';
else html = '当前页面使用了jQuery,版本为:'+result;
alert(html);
});
选项页 option
选项页是插件的设置页面,有两个入口,一个是右键图标菜单,一个是插件管理页面.1
2
3
4
5
6
7
8{
// "options_page": "options.html" ,(老版本写法)
"options_ui": {
"page": "optionxxx.html",
"open_in_tab": true, // 在当前 tab 打开
"chrome_style": true // 添加了一些默认样式
}
}不能使用 alert, 数据存储建议使用 chrome.storage, 因为会随用户自动同步
omnibox
注册某个关键字触发插件自己的搜索建议界面.1
2
3
4{
// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字
"omnibox": { "keyword" : "go" },
}然后在 background 注册监听事件:
1
2
3
4
5
6
7
8
9
10chrome.omnibox.onInputChanged.addListener((text, suggest) => {
console.log('inputChanged: ' + text);
if(!text) return;
if(text === 'xxx'){
suggest([
{content: '百度搜索 ' + text, description: '百度搜索 ' + text},
{content: '谷歌搜索 ' + text, description: '谷歌搜索 ' + text},
]);
}
})桌面通知:
chrome 提供了一个 chrome.notification API 方便插件推送桌面通知.1
2
3
4
5
6chrome.notifications.create(null,{
type: "basic",
iconUrl: "img/xx.ping",
title: "标题",
message: "内容"
})
消息通信
popup 和 background
popup 可以直接调用 background 的 js 方法,也可以访问 background 的 DOM.1
2
3
4
5
6
7
8// background.js
function test(){
alert("background");
}
// popup.js
var bg = chrome.extension.getBackgroundPage();
bg.test();
bg.document.body.innerHTML; // dompopup 或 bg 向 content 发消息
1
2
3
4
5
6
7
8
9
10
11
12
13// bg,popup.js
function sendMessageToContent(message,callback){
chrome.tabs.query({active:true,currentWindow: true},function(tabs){
chrome.tabs.sendMessage(tabs[0].id,message,function(res){
if(callback) callback(res);
})
})
}
// content-script.js 接收
chrome.runtime.onMessage.addListener(function(req,send,res){
if(req.cmd == 'test') alert(req.value);
send("我收到了你的消息")
})content 向 bg/popup 主动发消息
1
2
3
4
5
6
7
8
9
10// content-script.js
chrome.runtime.sendMessage({
greeting: "hello"
}, function(res){
console.log("收到回复" + res)
})
// bg 或 popup.js
chrome.runtime.onMessage.addListener(function(req,send,res){
send("我收到了你的消息")
})injected script 和 content script
content-script 和页面内 脚本(injected-script)之间唯一共享的就是页面 DOM 元素.有两种方式实现通信,一是通过 window.postMessage 和 window.addEventListener 实现消息通信(推荐),二是通过 自定义 dom 事件.1
2
3
4
5
6// injected - script
window.postMessage({"test": 'hello'},'*');
// content- script
window.addEventListener("message",function(e){
console.log(e.data)
},false)长连接和短连接
chrome 插件中有两种通信方式,一种是短连接(chrome.tabs.sendMessage和 chrome.runtime.sendMessage),一个是长连接(chrome.tabs.connect 和 chrome.runtime.connect).1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 长连接
// popup.js
var port = chrome.tabs.connect(tabId,{name: 'test-connect'});
port.postMessage({xxx:'xxx'});
port.onMessage.addListener(function(msg){
alert('收到消息' + msg.answer)
// ...
})
// content-script
chrome.runtime.onConnect.addListener(function(port){
if(port === 'test-connect'){
port.onMessage.addListener(function(msg){
alert("收到长连接",msg)
})
}
})
补充
获取当前窗口 id
1
2
3chrome.windows.getCurrent(function(cw){
console.log(cw.id)
})获取当前标签页 id
1
2
3
4
5function getCurrentTabId(callback){
chrome.tabs.query({active:true,currentWindow:true},function(tabs){
if(callback) callback(tabs.length ? tabs[0].id : null)
})
}定期执行代码
1
2
3
4
5// 配置
"permissions": ["alarms"]
// 创建方法
chrome.alarms.create(name,info);// name 为任务名, info 包含以下属性 when 何时,dalayInMinutes 延迟时间, periodInMinutes 非 null表示时间间隔,单位 min
chrome.alarms.onAlarm.addListener(xxx); // 触发事件本地存储
chrome.storage 是针对插件全局的,即使在 background 中保存的数据,在 content-script 也能获取到.chrome.storage.sync 可以跟随当前登录用户自动同步.需要声明 storage 权限,有 sync 和 local 两种方式选择.1
2
3
4
5
6
7
8// 读取数据,第一个参数是要读取的 key 以及默认值
chrome.storage.sync.get({color: 'red',age:18},function(items){
console.log(items)
})
// 保存数据
chrome.storage.sync({color:'blue'},function(){
console.log('save success')
})快捷键唤醒 popup
1
2
3
4
5
6
7"commands": {
"_execute_browser_action":{
"suggested_key":{
"default": "Alt+Shift+J" // 快捷键唤醒
}
}
}webRequest
通过 webrequest API 可以对 HTTP 请求进行修改1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//权限申请
"permissions": [
"webRequest", // web请求
"webRequestBlocking", // 阻塞式 web 请求
"storage",// 插件本地储存
"http://*/*" //可以通过 executeScript 或 insertCss 访问的网站
]
// web 请求监听
chrome.webRequest.onBeforeRequest.addListener(details =>{
let showImage = false ; // 不展示图片
if(!showImage && details.type === 'image'){
return {
cancel: true
}
}
if(details.type === 'media'){
//...
}
},{urls: ["<all_urls>"]},["blocking"]);国际化
插件根目录新建一个_locales 的文件夹,在新建一些语言文件夹如 en,zh_CN,zh_TW,然后在每个语言文件夹放入一个 messages.json,同时在文件中设置 default_locale.测试时,通过给chrome建立一个不同的快捷方式chrome.exe –lang=en来切换语言1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// en/message.json
{
"pluginDesc": {"message": "A simple chrome extension demo"},
"helloWorld": {"message": "Hello World!"}
}
// zh_CN/message.json
{
"pluginDesc": {"message": "一个简单的Chrome插件demo"},
"helloWorld": {"message": "你好啊,世界!"}
}
// 在 manifest.json 和 css 文件中通过 __MSG_messagename__引入
{
"description": "__MSG_pluginDesc__",
// 默认语言
"default_locale": "zh_CN",
}
// js 中使用
chrome.i18n.getMessage("helloWorld")