chrome 插件开发
教程:
- https://segmentfault.com/a/1190000006949838
- https://segmentfault.com/a/1190000005071240
- https://blog.csdn.net/austin_link?t=1
- https://crxdoc-zh.appspot.com/extensions/getstarted (官方文档中文)
- https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html#%E6%A0%B8%E5%BF%83%E4%BB%8B%E7%BB%8D,这个写的非常详细认真
- http://www.cnblogs.com/champagne/p/, 这个是系列教程
一 注意点
- chrome 不允许扩展中的HTML页面直接嵌入 js脚本,所有的脚本必须作为外部src引入。
- manifest.json 是一个非常重要的配置文件,常用的配置项必须要了解。
- background.js / content-script.js , 这些文件名字不能改了,改了之后调用 chrome 开头的一些 api 就会报 null 的错误。
二 配置 manifest.json
"browser_action": {
//插件加载后生成图标
"default_icon": "cc.gif",
// 鼠标悬浮到图表上显示的文字
"default_title": "Hello CC",
// 点击图标展示的 html 页面
"default_popup": "popup.html"
},
// 运行插件需要的权限
"permissions": [
//指定插件生效的 Url 模式
"http://*/",
"bookmarks",
"tabs",
"history"
],
// 插件运行的后台环境
"background":{//background script即插件运行的环境
// 2 种方式
"page":"background.html"
// "scripts": ["js/jquery-1.9.1.min.js","js/background.js"]//数组.chrome会在扩展启动时自动创建一个包含所有指定脚本的页面
},
// document 插件向页面注入的 js/css 脚本.可以实现广告屏蔽、页面 css 定制
"content_scripts": [{
//满足什么条件的 url 执行该插件
"matches": ["http://*/*","https://*/*"],
// 注入js脚本
"js": ["js/jquery-1.9.1.min.js", "js/js.js"],
// 注入css
"css": ["css/custom.css"],
//插件执行的时间,"document_start","document_end",
// "document_idle",表示页面空闲时
"run_at": "document_start",
}]
- 与 browser_action 对应的还有一个 page_action,browser_action 对所有的页面生效,而 page_action 只针对特定的页面生效,page_action 与 browser_action 只能存在一个。
-
"manifest_version": 2
,因为一些乱七八糟的原因,这个值必须是2. - popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。需要特别注意的是,由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码千万不要写在popup里面。
在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。
background
background.后台(姑且这么翻译吧),是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面.
background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS。
配置中,background可以通过page指定一张网页,也可以通过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页
三 调试
扩展程序 -> 加载已解压的扩展程序
如果修改了扩展源文件,有的时候更新一下可以生效,有的时候需要把插件删除,重装一下。
四 content_scripts,注入 JS 和 CSS
在 content_scripts 配置的文件 js 文件中,可以直接操作dom,如果写了啥 chrome.tabs 之类的会显示无效 ....
content_scripts和原始页面共享DOM,但是不共享JS,如要访问页面JS(例如某个JS变量),只能通过injected js来实现。content-scripts不能访问绝大部分chrome.xxx.api,除了下面这4种:
chrome.extension(getURL , inIncognitoContext , lastError , onRequest , sendRequest)
chrome.i18n
chrome.runtime(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)
chrome.storage
其实看到这里不要悲观,这些API绝大部分时候都够用了,非要调用其它API的话,你还可以通过通信来实现让background来帮你调用(关于通信,后文有详细介绍)。
五 五种类型的 JS 对比
chrome 插件的 JS 主要可以分为这 5 类,injected script
, content-script
, popup js
, background.js
和 devtools.js
。
5.1 权限对比
js 种类 | 可访问的 API | DOM访问情况 | JS访问情况 | 直接访问 |
---|---|---|---|---|
injected js | 和普通的JS无任何差别,不能访问任何扩展API | 可以访问 | 可以访问 | 不可以 |
content script | 只能访问 extension/ runtime 等部分 API | 可以访问 | 不可以 | 不可以 |
popup js | 可以访问绝大部分 API,除了 devtools | 不可以直接访问 | 不可以 | 可以 |
backgroundjs | 可以访问绝大部分 API,除了 devtools | 不可以直接访问 | 不可以 | 可以 |
devtools js | 只能访问 devtools/ extension/ runtime 等部分 API | 可以 | 可以 | 不可以 |
六 消息通信
Chrome 插件中存在 5 种js,那么它们之间如何通信呢?popup 和 background 其实几乎可以视为同一种东西,因为它们可访问的 API 的 一样、通信机制都一样、都可以跨域。
6.1 互相通信概览
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 |
6.2 通信详细介绍
6.2.1 popup 和 background
popup 可以直接调用 background 中的 js 方法,也可以直接访问 background 的 DOM:
// background.js
function test(){
alert('我是 background');
}
// popup.js
let bg = chrome.extension.getBackgroundPage();
// 访问bg 的函数
bg.test();
// 访问 bg 的 DOM
alert(bg.document.body.innerHTML);
background 访问 popup, 需要 popup 已经打开:
let views = chrome.extension.getViews({type:'popup'});
if(views.length > 0){
console.log(views[0].location.href);
}
6.2.2 popup 和 background 向 content 主动发送消息
backgroud.js 或者 popup.js
function sendMessageToContentScript(message,callback){
chrome.tabs.query({active:true, currentWindow: true}, function(tabs){
chrome.tabs.sendMessage(tabs[0].id, message, function(response){
if(callback){
callback(response)
}
})
})
}
// 调用
sendMessageToContentScript({cmd:'test', value:'nihao'},function(response){
console.log("来自 content 的回复:" + response);
})
content-script.js 接受消息:
chrome.runtime.onMessage.addListener((request,sender,sendResponse){
if (request.cmd === "test"){
alert(request.value);
}
sendResponse("我收到了你的消息");
})
6.2.3 content-script 主动发消息给后台
content-script.js
chrome.runtime.sendMessage({greeting: '你好,我主动发消息'},(response)=>{
console.log("收到来自后台的回复"+ response);
})
background.js 或 popup.js
// 监听来自 content-script 的消息
chrome.runtime.onMessage.addListener((request, sender, sendResponse)=>{
console.log(request, sender, sendResponse);
sendResponse('我是后台' + JSON.stringify(request));
})
参数说明:
- request 是接受的数据
- sender 是一个对象,包含了许多关于发出者的信息:
- tab,关于 tab 的一些信息。
- frameId
- id , 猜测是唯一标识符
- url,发送者的url。
- sendResponse, 是个回调函数而已。
注意事项:
- content_scripts向popup主动发消息的前提是popup必须打开!否则需要利用background作中转;
- 如果background和popup同时监听,那么它们都可以同时收到消息,但是只有一个可以sendResponse,一个先发送了,那么另外一个再发送就无效;
6.2.4 injected script 和 content-script
content-script 和 页面内的脚本(injected-script 自然也属于页面内的脚本)之间唯一共享的东西就是 页面 的 DOM元素。
injected-script:
window.postMessgae({"test":"nihao"},"*")
content script:
window.addEventListener("message",(e)=>{
console.log(e.data);
})
6.3 长连接
Chrome插件中有2种通信方式,一个是短连接(chrome.tabs.sendMessage和chrome.runtime.sendMessage),一个是长连接(chrome.tabs.connect和chrome.runtime.connect)。
短连接的话就是挤牙膏一样,我发送一下,你收到了再回复一下,如果对方不回复,你只能重新发,而长连接类似WebSocket会一直建立连接,双方可以随时互发消息。
popup.js:
getCurrentTabId((tabId) => {
var port = chrome.tabs.connect(tabId, {name: 'test-connect'});
port.postMessage({question: '你是谁啊?'});
port.onMessage.addListener(function(msg) {
alert('收到消息:'+msg.answer);
if(msg.answer && msg.answer.startsWith('我是'))
{
port.postMessage({question: '哦,原来是你啊!'});
}
});
});
content-script.js:
// 监听长连接
chrome.runtime.onConnect.addListener(function(port) {
console.log(port);
if(port.name == 'test-connect') {
port.onMessage.addListener(function(msg) {
console.log('收到长连接消息:', msg);
if(msg.question == '你是谁啊?') port.postMessage({answer: '我是你爸!'});
});
}
});