【干货】Chrome插件(扩展)开发全攻略(不点进来看看你确定后悔)<转>

【干货】Chrome插件(扩展)开发全攻略(不点进来看看你确定后悔)

 

写在前面

我花了将近一个多月的时间断断续续写下这篇博文,并精心写下完整demo,写博客的辛苦你们懂的,因此转载务必保留出处。本文全部涉及到的大部分代码均在这个demo里面:https://github.com/liuxianan/chrome-plugin-demo ,你们能够直接下载下来运行。javascript

另外,本文图片较多,且图片服务器带宽有限,右下角的目录滚动监听必须等到图片所有加载完毕以后才会触发,因此请耐心等待加载完毕。css

本文目录:html

demo部分截图:前端

前言

2.1. 什么是Chrome插件

严格来说,咱们正在说的东西应该叫Chrome扩展(Chrome Extension),真正意义上的Chrome插件是更底层的浏览器功能扩展,可能须要对浏览器源码有必定掌握才有能力去开发。鉴于Chrome插件的叫法已经习惯,本文也所有采用这种叫法,但读者需深知本文所描述的Chrome插件实际上指的是Chrome扩展。vue

Chrome插件是一个用Web技术开发、用来加强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包.java

我的猜想crx多是Chrome Extension以下3个字母的简写:jquery

另外,其实不仅是前端技术,Chrome插件还能够配合C++编写的dll动态连接库实现一些更底层的功能(NPAPI),好比全屏幕截图。git

因为安全缘由,Chrome浏览器42以上版本已经陆续再也不支持NPAPI插件,取而代之的是更安全的PPAPI。github

2.2. 学习Chrome插件开发有什么意义

加强浏览器功能,轻松实现属于本身的“定制版”浏览器,等等。web

Chrome插件提供了不少实用API供咱们使用,包括但不限于:

  • 书签控制;
  • 下载控制;
  • 窗口控制;
  • 标签控制;
  • 网络请求控制,各种事件监听;
  • 自定义原生菜单;
  • 完善的通讯机制;
  • 等等;

2.3. 为何是Chrome插件而不是Firefox插件

  1. Chrome占有率更高,更多人用;
  2. 开发更简单;
  3. 应用场景更普遍,Firefox插件只能运行在Firefox上,而Chrome除了Chrome浏览器以外,还能够运行在全部webkit内核的国产浏览器,好比360极速浏览器、360安全浏览器、搜狗浏览器、QQ浏览器等等;
  4. 除此以外,Firefox浏览器也对Chrome插件的运行提供了必定的支持;

开发与调试

Chrome插件没有严格的项目结构要求,只要保证本目录有一个manifest.json便可,也不须要专门的IDE,普通的web开发工具便可。

从右上角菜单->更多工具->扩展程序能够进入 插件管理页面,也能够直接在地址栏输入 chrome://extensions 访问。

勾选开发者模式便可以文件夹的形式直接加载插件,不然只能安装.crx格式的文件。Chrome要求插件必须从它的Chrome应用商店安装,其它任何网站下载的都没法直接安装,因此,其实咱们能够把crx文件解压,而后经过开发者模式直接加载。

开发中,代码有任何改动都必须从新加载插件,只须要在插件管理页按下Ctrl+R便可,以防万一最好还把页面刷新一下。

核心介绍

4.1. manifest.json

这是一个Chrome插件最重要也是必不可少的文件,用来配置全部和插件相关的配置,必须放在根目录。其中,manifest_versionnameversion3个是必不可少的,descriptionicons是推荐的。

下面给出的是一些常见的配置项,均有中文注释,完整的配置文档请戳这里

{ // 清单文件的版本,这个必须写,并且必须是2 "manifest_version": 2, // 插件的名称 "name": "demo", // 插件的版本 "version": "1.0.0", // 插件描述 "description": "简单的Chrome扩展demo", // 图标,通常偷懒所有用一个尺寸的也没问题 "icons": { "16": "img/icon.png", "48": "img/icon.png", "128": "img/icon.png" }, // 会一直常驻的后台JS或后台页面 "background": { // 2种指定方式,若是指定JS,那么会自动生成一个背景页 "page": "background.html" //"scripts": ["js/background.js"] }, // 浏览器右上角图标设置,browser_action、page_action、app必须三选一 "browser_action": { "default_icon": "img/icon.png", // 图标悬停时的标题,可选 "default_title": "这是一个示例Chrome插件", "default_popup": "popup.html" }, // 当某些特定页面打开才显示的图标 /*"page_action":  {  "default_icon": "img/icon.png",  "default_title": "我是pageAction",  "default_popup": "popup.html"  },*/ // 须要直接注入页面的JS "content_scripts": [ { //"matches": ["http://*/*", "https://*/*"], // "<all_urls>" 表示匹配全部地址 "matches": ["<all_urls>"], // 多个JS按顺序注入 "js": ["js/jquery-1.8.3.js", "js/content-script.js"], // JS的注入能够随便一点,可是CSS的注意就要千万当心了,由于一不当心就可能影响全局样式 "css": ["css/custom.css"], // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle "run_at": "document_start" }, // 这里仅仅是为了演示content-script能够配置多个规则 { "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"], "js": ["js/show-image-content-size.js"] } ], // 权限申请 "permissions": [ "contextMenus", // 右键菜单 "tabs", // 标签 "notifications", // 通知 "webRequest", // web请求 "webRequestBlocking", "storage", // 插件本地存储 "http://*/*", // 能够经过executeScript或者insertCSS访问的网站 "https://*/*" // 能够经过executeScript或者insertCSS访问的网站 ], // 普通页面可以直接访问的插件资源列表,若是不设置是没法直接访问的 "web_accessible_resources": ["js/inject.js"], // 插件主页,这个很重要,不要浪费了这个免费广告位 "homepage_url": "https://www.baidu.com", // 覆盖浏览器默认页面 "chrome_url_overrides": { // 覆盖浏览器默认的新标签页 "newtab": "newtab.html" }, // Chrome40之前的插件配置页写法 "options_page": "options.html", // Chrome40之后的插件配置页写法,若是2个都写,新版Chrome只认后面这一个 "options_ui": { "page": "options.html", // 添加一些默认的样式,推荐使用 "chrome_style": true }, // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字 "omnibox": { "keyword" : "go" }, // 默认语言 "default_locale": "zh_CN", // devtools页面入口,注意只能指向一个HTML文件,不能是JS文件 "devtools_page": "devtools.html" }

4.2. content-scripts

所谓content-scripts,其实就是Chrome插件中向页面注入脚本的一种形式(虽然名为script,其实还能够包括css的),借助content-scripts咱们能够实现经过配置的方式轻松向指定页面注入JS和CSS(若是须要动态注入,能够参考下文),最多见的好比:广告屏蔽、页面CSS定制,等等。

示例配置:

{ // 须要直接注入页面的JS "content_scripts": [ { //"matches": ["http://*/*", "https://*/*"], // "<all_urls>" 表示匹配全部地址 "matches": ["<all_urls>"], // 多个JS按顺序注入 "js": ["js/jquery-1.8.3.js", "js/content-script.js"], // JS的注入能够随便一点,可是CSS的注意就要千万当心了,由于一不当心就可能影响全局样式 "css": ["css/custom.css"], // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle "run_at": "document_start" } ], }

特别注意,若是没有主动指定run_atdocument_start(默认为document_idle),下面这种代码是不会生效的:

document.addEventListener('DOMContentLoaded', function() { console.log('我被执行了!'); });

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来帮你调用(关于通讯,后文有详细介绍)。

好了,Chrome插件给咱们提供了这么强大的JS注入功能,剩下的就是发挥你的想象力去玩弄浏览器了。

4.3. background

后台(姑且这么翻译吧),是一个常驻的页面,它的生命周期是插件中全部类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,因此一般把须要一直运行的、启动就运行的、全局的代码放在background里面。

background的权限很是高,几乎能够调用全部的Chrome扩展API(除了devtools),并且它能够无限制跨域,也就是能够跨域访问任何网站而无须要求对方设置CORS

通过测试,其实不止是background,全部的直接经过chrome-extension://id/xx.html这种方式打开的网页均可以无限制跨域。

配置中,background能够经过page指定一张网页,也能够经过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页:

{ // 会一直常驻的后台JS或后台页面 "background": { // 2种指定方式,若是指定JS,那么会自动生成一个背景页 "page": "background.html" //"scripts": ["js/background.js"] }, }

须要特别说明的是,虽然你能够经过chrome-extension://xxx/background.html直接打开后台页,可是你打开的后台页和真正一直在后台运行的那个页面不是同一个,换句话说,你能够打开无数个background.html,可是真正在后台常驻的只有一个,并且这个你永远看不到它的界面,只能调试它的代码。

4.4. event-pages

这里顺带介绍一下event-pages,它是一个什么东西呢?鉴于background生命周期太长,长时间挂载后台可能会影响性能,因此Google又弄一个event-pages,在配置文件上,它与background的惟一区别就是多了一个persistent参数:

{ "background": { "scripts": ["event-page.js"], "persistent": false }, }

它的生命周期是:在被须要时加载,在空闲时被关闭,什么叫被须要时呢?好比第一次安装、插件更新、有content-script向它发送消息,等等。

除了配置文件的变化,代码上也有一些细微变化,我的这个简单了解一下就好了,通常状况下background也不会很消耗性能的。

popup是点击browser_action或者page_action图标时打开的一个小窗口网页,焦点离开网页就当即关闭,通常用来作一些临时性的交互。

博客园网摘插件popup效果

popup能够包含任意你想要的HTML内容,而且会自适应大小。能够经过default_popup字段来指定popup页面,也能够调用setPopup()方法。

配置方式:

{ "browser_action": { "default_icon": "img/icon.png", // 图标悬停时的标题,可选 "default_title": "这是一个示例Chrome插件", "default_popup": "popup.html" } }

须要特别注意的是,因为单击图标打开popup,焦点离开又当即关闭,因此popup页面的生命周期通常很短,须要长时间运行的代码千万不要写在popup里面。

在权限上,它和background很是相似,它们之间最大的不一样是生命周期的不一样,popup中能够直接经过chrome.extension.getBackgroundPage()获取background的window对象。

4.6. injected-script

这里的injected-script是我给它取的,指的是经过DOM操做的方式向页面注入的一种JS。为何要把这种JS单独拿出来讨论呢?又或者说为何须要经过这种方式注入JS呢?

这是由于content-script有一个很大的“缺陷”,也就是没法访问页面中的JS,虽然它能够操做DOM,可是DOM却不能调用它,也就是没法在DOM中经过绑定事件的方式调用content-script中的代码(包括直接写onclickaddEventListener2种方式都不行),可是,“在页面上添加一个按钮并调用插件的扩展API”是一个很常见的需求,那该怎么办呢?其实这就是本小节要讲的。

content-script中经过DOM方式向页面注入inject-script代码示例:

// 向页面注入JS function injectCustomJs(jsPath) { jsPath = jsPath || 'js/inject.js'; var temp = document.createElement('script'); temp.setAttribute('type', 'text/javascript'); // 得到的地址相似:chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js temp.src = chrome.extension.getURL(jsPath); temp.onload = function() { // 放在页面很差看,执行完后移除掉 this.parentNode.removeChild(this); }; document.head.appendChild(temp); }

你觉得这样就好了?执行一下你会看到以下报错:

Denying load of chrome-extension://efbllncjkjiijkppagepehoekjojdclc/js/inject.js. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.

意思就是你想要在web中直接访问插件中的资源的话必须显示声明才行,配置文件中增长以下:

{ // 普通页面可以直接访问的插件资源列表,若是不设置是没法直接访问的 "web_accessible_resources": ["js/inject.js"], }

至于inject-script如何调用content-script中的代码,后面我会在专门的一个消息通讯章节详细介绍。

4.7. homepage_url

开发者或者插件主页设置,通常会在以下2个地方显示:

Chrome插件的8种展现形式

5.1. browserAction(浏览器右上角)

经过配置browser_action能够在浏览器的右上角增长一个图标,一个browser_action能够拥有一个图标,一个tooltip,一个badge和一个popup

示例配置以下:

"browser_action": { "default_icon": "img/icon.png", "default_title": "这是一个示例Chrome插件", "default_popup": "popup.html" }

5.1.1. 图标

browser_action图标推荐使用宽高都为19像素的图片,更大的图标会被缩小,格式随意,通常推荐png,能够经过manifest中default_icon字段配置,也能够调用setIcon()方法。

5.1.2. tooltip

修改browser_action的manifest中default_title字段,或者调用setTitle()方法。

5.1.3. badge

所谓badge就是在图标上显示一些文本,能够用来更新一些小的扩展状态提示信息。由于badge空间有限,因此只支持4个如下的字符(英文4个,中文2个)。badge没法经过配置文件来指定,必须经过代码实现,设置badge文字和颜色能够分别使用setBadgeText()setBadgeBackgroundColor()

chrome.browserAction.setBadgeText({text: 'new'}); chrome.browserAction.setBadgeBackgroundColor({color: [255, 0, 0, 255]});

效果:

5.2. pageAction(地址栏右侧)

所谓pageAction,指的是只有当某些特定页面打开才显示的图标,它和browserAction最大的区别是一个始终都显示,一个只在特定状况才显示。

须要特别说明的是早些版本的Chrome是将pageAction放在地址栏的最右边,左键单击弹出popup,右键单击则弹出相关默认的选项菜单:

而新版的Chrome更改了这一策略,pageAction和普通的browserAction同样也是放在浏览器右上角,只不过没有点亮时是灰色的,点亮了才是彩色的,灰色时不管左键仍是右键单击都是弹出选项:

具体是从哪一版本开始改的没去仔细考究,反正知道v50.0的时候仍是前者,v58.0的时候已改成后者。

调整以后的pageAction咱们能够简单地把它当作是能够置灰的browserAction

  • chrome.pageAction.show(tabId) 显示图标;
  • chrome.pageAction.hide(tabId) 隐藏图标;

示例(只有打开百度才显示图标):

// manifest.json { "page_action": { "default_icon": "img/icon.png", "default_title": "我是pageAction", "default_popup": "popup.html" }, "permissions": ["declarativeContent"] } // 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()] } ]); }); });

效果图:

5.3. 右键菜单

经过开发Chrome插件能够自定义浏览器的右键菜单,主要是经过chrome.contextMenusAPI实现,右键菜单能够出如今不一样的上下文,好比普通页面、选中的文字、图片、连接,等等,若是有同一个插件里面定义了多个菜单,Chrome会自动组合放到以插件名字命名的二级菜单里,以下:

5.3.1. 最简单的右键菜单示例

// manifest.json {"permissions": ["contextMenus"]} // background.js chrome.contextMenus.create({ title: "测试右键菜单", onclick: function(){alert('您点击了右键菜单!');} });

效果:

5.3.2. 添加右键百度搜索

// manifest.json {"permissions": ["contextMenus", "tabs"]} // background.js chrome.contextMenus.create({ title: '使用度娘搜索:%s', // %s表示选中的文字 contexts: ['selection'], // 只有当选中文字时才会出现此右键菜单 onclick: function(params) { // 注意不能使用location.href,由于location是属于background的window对象 chrome.tabs.create({url: 'https://www.baidu.com/s?ie=utf-8&wd=' + encodeURI(params.selectionText)}); } });

效果以下:

5.3.3. 语法说明

这里只是简单列举一些经常使用的,完整API参见:https://developer.chrome.com/extensions/contextMenus

chrome.contextMenus.create({ type: 'normal', // 类型,可选:["normal", "checkbox", "radio", "separator"],默认 normal title: '菜单的名字', // 显示的文字,除非为“separator”类型不然此参数必需,若是类型为“selection”,可使用%s显示选定的文本 contexts: ['page'], // 上下文环境,可选:["all", "page", "frame", "selection", "link", "editable", "image", "video", "audio"],默认page onclick: function(){}, // 单击时触发的方法 parentId: 1, // 右键菜单项的父菜单项ID。指定父菜单项将会使此菜单项成为父菜单项的子菜单 documentUrlPatterns: 'https://*.baidu.com/*' // 只在某些页面显示此右键菜单 }); // 删除某一个菜单项 chrome.contextMenus.remove(menuItemId); // 删除全部自定义右键菜单 chrome.contextMenus.removeAll(); // 更新某一个菜单项 chrome.contextMenus.update(menuItemId, updateProperties);

5.4. override(覆盖特定页面)

使用override页能够将Chrome默认的一些特定页面替换掉,改成使用扩展提供的页面。

扩展能够替代以下页面:

  • 历史记录:从工具菜单上点击历史记录时访问的页面,或者从地址栏直接输入 chrome://history
  • 新标签页:当建立新标签的时候访问的页面,或者从地址栏直接输入 chrome://newtab
  • 书签:浏览器的书签,或者直接输入 chrome://bookmarks

注意:

  • 一个扩展只能替代一个页面;
  • 不能替代隐身窗口的新标签页;
  • 网页必须设置title,不然用户可能会看到网页的URL,形成困扰;

下面的截图是默认的新标签页和被扩展替换掉的新标签页。

代码(注意,一个插件只能替代一个默认页,如下仅为演示):

"chrome_url_overrides": { "newtab": "newtab.html", "history": "history.html", "bookmarks": "bookmarks.html" }

5.5. devtools(开发者工具)

5.5.1. 预热

使用过vue的应该见过这种类型的插件:

是的,Chrome容许插件在开发者工具(devtools)上动手脚,主要表如今:

  • 自定义一个和多个和ElementsConsoleSources等同级别的面板;
  • 自定义侧边栏(sidebar),目前只能自定义Elements面板的侧边栏;

先来看2张简单的demo截图,自定义面板(判断当前页面是否使用了jQuery):

自定义侧边栏(获取当前页面全部图片):

5.5.2. devtools扩展介绍

主页:https://developer.chrome.com/extensions/devtools

来一张官方图片:

每打开一个开发者工具窗口,都会建立devtools页面的实例,F12窗口关闭,页面也随着关闭,因此devtools页面的生命周期和devtools窗口是一致的。devtools页面能够访问一组特有的DevTools API以及有限的扩展API,这组特有的DevTools API只有devtools页面才能够访问,background都无权访问,这些API包括:

  • chrome.devtools.panels:面板相关;
  • chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
  • chrome.devtools.network:获取有关网络请求的信息;

大部分扩展API都没法直接被DevTools页面调用,但它能够像content-script同样直接调用chrome.extensionchrome.runtimeAPI,同时它也能够像content-script同样使用Message交互的方式与background页面进行通讯。

5.5.3. 实例:建立一个devtools扩展

首先,要针对开发者工具开发插件,须要在清单文件声明以下:

{ // 只能指向一个HTML文件,不能是JS文件 "devtools_page": "devtools.html" }

这个devtools.html里面通常什么都没有,就引入一个js:

<!DOCTYPE html> <html> <head></head> <body> <script type="text/javascript" src="js/devtools.js"></script> </body> </html>

能够看出来,其实真正代码是devtools.js,html文件是“多余”的,因此这里以为有点坑,devtools_page干吗不容许直接指定JS呢?

再来看devtools.js的代码:

// 建立自定义面板,同一个插件能够建立多个自定义面板 // 几个参数依次为: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!'}); // 直接设置显示某个对象 });

setPage时的效果:

如下截图示例的代码:

// 检测jQuery document.getElementById('check_jquery').addEventListener('click', function() { // 访问被检查的页面DOM须要使用inspectedWindow // 简单例子:检测被检查页面是否使用了jQuery chrome.devtools.inspectedWindow.eval("jQuery.fn.jquery", function(result, isException) { var html = ''; if (isException) html = '当前页面没有使用jQuery。'; else html = '当前页面使用了jQuery,版本为:'+result; alert(html); }); }); // 打开某个资源 document.getElementById('open_resource').addEventListener('click', function() { chrome.devtools.inspectedWindow.eval("window.location.href", function(result, isException) { chrome.devtools.panels.openResource(result, 20, function() { console.log('资源打开成功!'); }); }); }); // 审查元素 document.getElementById('test_inspect').addEventListener('click', function() { chrome.devtools.inspectedWindow.eval("inspect(document.images[0])", function(result, isException){}); }); // 获取全部资源 document.getElementById('get_all_resources').addEventListener('click', function() { chrome.devtools.inspectedWindow.getResources(function(resources) { alert(JSON.stringify(resources)); }); });

5.5.4. 调试技巧

修改了devtools页面的代码时,须要先在 chrome://extensions 页面按下Ctrl+R从新加载插件,而后关闭再打开开发者工具便可,无需刷新页面(并且只刷新页面不刷新开发者工具的话是不会生效的)。

因为devtools自己就是开发者工具页面,因此几乎没有方法能够直接调试它,直接用 chrome-extension://extid/devtools.html"的方式打开页面确定报错,由于不支持相关特殊API,只能先本身写一些方法屏蔽这些错误,调试通了再放开。

5.6. option(选项页)

所谓options页,就是插件的设置页面,有2个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面:

在Chrome40之前,options页面和其它普通页面没什么区别,Chrome40之后则有了一些变化。

咱们先看老版的options

{ // Chrome40之前的插件配置页写法 "options_page": "options.html", }

这个页面里面的内容就随你本身发挥了,配置以后在插件管理页就会看到一个选项按钮入口,点进去就是打开一个网页,没啥好讲的。

效果:

再来看新版的optionsV2

{ "options_ui": { "page": "options.html", // 添加一些默认的样式,推荐使用 "chrome_style": true }, }

options.html的代码咱们没有任何改动,只是配置文件改了,以后效果以下:

看起来是否是高大上了?

几点注意:

  • 为了兼容,建议2种都写,若是都写了,Chrome40之后会默认读取新版的方式;
  • 新版options中不能使用alert;
  • 数据存储建议用chrome.storage,由于会随用户自动同步;

5.7. omnibox

omnibox是向用户提供搜索建议的一种方式。先来看个gif图以便了解一下这东西究竟是个什么鬼:

注册某个关键字以触发插件本身的搜索建议界面,而后能够任意发挥了。

首先,配置文件以下:

{ // 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字 "omnibox": { "keyword" : "go" }, }

而后background.js中注册监听事件:

// omnibox 演示 chrome.omnibox.onInputChanged.addListener((text, suggest) => { console.log('inputChanged: ' + text); if(!text) return; if(text == '美女') { suggest([ {content: '中国' + text, description: '你要找“中国美女”吗?'}, {content: '日本' + text, description: '你要找“日本美女”吗?'}, {content: '泰国' + text, description: '你要找“泰国美女或人妖”吗?'}, {content: '韩国' + text, description: '你要找“韩国美女”吗?'} ]); } else if(text == '微博') { suggest([ {content: '新浪' + text, description: '新浪' + text}, {content: '腾讯' + text, description: '腾讯' + text}, {content: '搜狐' + text, description: '搜索' + text}, ]); } else { suggest([ {content: '百度搜索 ' + text, description: '百度搜索 ' + text}, {content: '谷歌搜索 ' + text, description: '谷歌搜索 ' + text}, ]); } }); // 当用户接收关键字建议时触发 chrome.omnibox.onInputEntered.addListener((text) => { console.log('inputEntered: ' + text); if(!text) return; var href = ''; if(text.endsWith('美女')) href = 'http://image.baidu.com/search/index?tn=baiduimage&ie=utf-8&word=' + text; else if(text.startsWith('百度搜索')) href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text.replace('百度搜索 ', ''); else if(text.startsWith('谷歌搜索')) href = 'https://www.google.com.tw/search?q=' + text.replace('谷歌搜索 ', ''); else href = 'https://www.baidu.com/s?ie=UTF-8&wd=' + text; openUrlCurrentTab(href); }); // 获取当前选项卡ID function getCurrentTabId(callback) { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { if(callback) callback(tabs.length ? tabs[0].id: null); }); } // 当前标签打开某个连接 function openUrlCurrentTab(url) { getCurrentTabId(tabId => { chrome.tabs.update(tabId, {url: url}); }) }

5.8. 桌面通知

Chrome提供了一个chrome.notificationsAPI以便插件推送桌面通知,暂未找到chrome.notifications和HTML5自带的Notification的显著区别及优点。

在后台JS中,不管是使用chrome.notifications仍是Notification都不须要申请权限(HTML5方式须要申请权限),直接使用便可。

最简单的通知:

代码:

chrome.notifications.create(null, { type: 'basic', iconUrl: 'img/icon.png', title: '这是标题', message: '您刚才点击了自定义右键菜单!' });

通知的样式能够很丰富:

这个没有深刻研究,有须要的能够去看官方文档。

5种类型的JS对比

Chrome插件的JS主要能够分为这5类:injected scriptcontent-scriptpopup jsbackground jsdevtools js

6.1. 权限对比

JS种类 可访问的API DOM访问状况 JS访问状况 直接跨域
injected script 和普通JS无任何差异,不能访问任何扩展API 能够访问 能够访问 不能够
content script 只能访问 extension、runtime等部分API 能够访问 不能够 不能够
popup js 可访问绝大部分API,除了devtools系列 不可直接访问 不能够 能够
background js 可访问绝大部分API,除了devtools系列 不可直接访问 不能够 能够
devtools js 只能访问 devtools、extension、runtime等部分API 能够 能够 不能够

6.2. 调试方式对比

JS类型 调试方式
injected script 直接普通的F12便可
content-script 打开Console,如图切换
popup-js popup页面右键审查元素
background 插件管理页点击背景页便可
devtools-js 暂未找到有效方法

消息通讯

通讯主页:https://developer.chrome.com/extensions/messaging

前面咱们介绍了Chrome插件中存在的5种JS,那么它们之间如何互相通讯呢?下面先来系统概况一下,而后再分类细说。须要知道的是,popup和background其实几乎能够视为一种东西,由于它们可访问的API都同样、通讯机制同样、均可以跨域。

7.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

7.2. 通讯详细介绍

7.2.1. popup和background

popup能够直接调用background中的JS方法,也能够直接访问background的DOM:

// background.js function test() { alert('我是background!'); } // popup.js var bg = chrome.extension.getBackgroundPage(); bg.test(); // 访问bg的函数 alert(bg.document.body.innerHTML); // 访问bg的DOM

小插曲,今天碰到一个状况,发现popup没法获取background的任何方法,找了半天才发现是由于background的js报错了,而你若是不主动查看background的js的话,是看不到错误信息的,特此提醒。

至于background访问popup以下(前提是popup已经打开):

var views = chrome.extension.getViews({type:'popup'}); if(views.length > 0) { console.log(views[0].location.href); }

7.2.2. popup或者bg向content主动发送消息

background.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:'你好,我是popup!'}, function(response) { console.log('来自content的回复:'+response); });

content-script.js接收:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { // console.log(sender.tab ?"from a content script:" + sender.tab.url :"from the extension"); if(request.cmd == 'test') alert(request.value); sendResponse('我收到了你的消息!'); });

双方通讯直接发送的都是JSON对象,不是JSON字符串,因此无需解析,很方便(固然也能够直接发送字符串)。

网上有些老代码中用的是chrome.extension.onMessage,没有彻底查清两者的区别(貌似是别名),可是建议统一使用chrome.runtime.onMessage

7.2.3. content-script主动发消息给后台

content-script.js:

chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主动发消息给后台!'}, function(response) { console.log('收到来自后台的回复:' + response); });

background.js 或者 popup.js:

// 监听来自content-script的消息 chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) { console.log('收到来自content-script的消息:'); console.log(request, sender, sendResponse); sendResponse('我是后台,我已收到你的消息:' + JSON.stringify(request)); });

注意事项:

  • content_scripts向popup主动发消息的前提是popup必须打开!不然须要利用background做中转;
  • 若是background和popup同时监听,那么它们均可以同时收到消息,可是只有一个能够sendResponse,一个先发送了,那么另一个再发送就无效;

7.2.4. injected script和content-script

content-script和页面内的脚本(injected-script天然也属于页面内的脚本)之间惟一共享的东西就是页面的DOM元素,有2种方法能够实现两者通信:

  1. 能够经过window.postMessagewindow.addEventListener来实现两者消息通信;
  2. 经过自定义DOM事件来实现;

第一种方法(推荐):

injected-script中:

window.postMessage({"test": '你好!'}, '*');

content script中:

window.addEventListener("message", function(e) { console.log(e.data); }, false);

第二种方法:

injected-script中:

var customEvent = document.createEvent('Event'); customEvent.initEvent('myCustomEvent', true, true); function fireCustomEvent(data) { hiddenDiv = document.getElementById('myCustomEventDiv'); hiddenDiv.innerText = data hiddenDiv.dispatchEvent(customEvent); } fireCustomEvent('你好,我是普通JS!');

content-script.js中:

var hiddenDiv = document.getElementById('myCustomEventDiv'); if(!hiddenDiv) { hiddenDiv = document.createElement('div'); hiddenDiv.style.display = 'none'; document.body.appendChild(hiddenDiv); } hiddenDiv.addEventListener('myCustomEvent', function() { var eventData = document.getElementById('myCustomEventDiv').innerText; console.log('收到自定义事件消息:' + eventData); });

7.3. 长链接和短链接

其实上面已经涉及到了,这里再单独说明一下。Chrome插件中有2种通讯方式,一个是短链接(chrome.tabs.sendMessagechrome.runtime.sendMessage),一个是长链接(chrome.tabs.connectchrome.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: '我是你爸!'}); }); } });

其它补充

8.1. 动态注入或执行JS

虽然在backgroundpopup中没法直接访问页面DOM,可是能够经过chrome.tabs.executeScript来执行脚本,从而实现访问web页面的DOM(注意,这种方式也不能直接访问页面JS)。

示例manifest.json配置:

{ "name": "动态JS注入演示", ... "permissions": [ "tabs", "http://*/*", "https://*/*" ], ... }

JS:

// 动态执行JS代码 chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'}); // 动态执行JS文件 chrome.tabs.executeScript(tabId, {file: 'some-script.js'});

8.2. 动态注入CSS

示例manifest.json配置:

{ "name": "动态CSS注入演示", ... "permissions": [ "tabs", "http://*/*", "https://*/*" ], ... }

JS代码:

// 动态执行CSS代码,TODO,这里有待验证 chrome.tabs.insertCSS(tabId, {code: 'xxx'}); // 动态执行CSS文件 chrome.tabs.insertCSS(tabId, {file: 'some-style.css'});

8.3. 获取当前窗口ID

chrome.windows.getCurrent(function(currentWindow) { console.log('当前窗口ID:' + currentWindow.id); });

8.4. 获取当前标签页ID

通常有2种方法:

// 获取当前选项卡ID function getCurrentTabId(callback) { chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { if(callback) callback(tabs.length ? tabs[0].id: null); }); }

获取当前选项卡id的另外一种方法,大部分时候都相似,只有少部分时候会不同(例如当窗口最小化时)

// 获取当前选项卡ID function getCurrentTabId2() { chrome.windows.getCurrent(function(currentWindow) { chrome.tabs.query({active: true, windowId: currentWindow.id}, function(tabs) { if(callback) callback(tabs.length ? tabs[0].id: null); }); }); }

8.5. 本地存储

本地存储建议用chrome.storage而不是普通的localStorage,区别有好几点,我的认为最重要的2点区别是:

  • chrome.storage是针对插件全局的,即便你在background中保存的数据,在content-script也能获取到;
  • chrome.storage.sync能够跟随当前登陆用户自动同步,这台电脑修改的设置会自动同步到其它电脑,很方便,若是没有登陆或者未联网则先保存到本地,等登陆了再同步至网络;

须要声明storage权限,有chrome.storage.syncchrome.storage.local2种方式可供选择,使用示例以下:

// 读取数据,第一个参数是指定要读取的key以及设置默认值 chrome.storage.sync.get({color: 'red', age: 18}, function(items) { console.log(items.color, items.age); }); // 保存数据 chrome.storage.sync.set({color: 'blue'}, function() { console.log('保存成功!'); });

8.6. webRequest

经过webRequest系列API能够对HTTP请求进行任性地修改、定制,这里经过beforeRequest来简单演示一下它的冰山一角:

//manifest.json { // 权限申请 "permissions": [ "webRequest", // web请求 "webRequestBlocking", // 阻塞式web请求 "storage", // 插件本地存储 "http://*/*", // 能够经过executeScript或者insertCSS访问的网站 "https://*/*" // 能够经过executeScript或者insertCSS访问的网站 ], } // background.js // 是否显示图片 var showImage; chrome.storage.sync.get({showImage: true}, function(items) { showImage = items.showImage; }); // web请求监听,最后一个参数表示阻塞式,需单独声明权限:webRequestBlocking chrome.webRequest.onBeforeRequest.addListener(details => { // cancel 表示取消本次请求 if(!showImage && details.type == 'image') return {cancel: true}; // 简单的音视频检测 // 大部分网站视频的type并非media,且视频作了防下载处理,因此这里仅仅是为了演示效果,无实际意义 if(details.type == 'media') { chrome.notifications.create(null, { type: 'basic', iconUrl: 'img/icon.png', title: '检测到音视频', message: '音视频地址:' + details.url, }); } }, {urls: ["<all_urls>"]}, ["blocking"]);

8.7. 国际化

插件根目录新建一个名为_locales的文件夹,再在下面新建一些语言的文件夹,如enzh_CNzh_TW,而后再在每一个文件夹放入一个messages.json,同时必须在清单文件中设置default_locale

_locales\en\messages.json内容:

{ "pluginDesc": {"message": "A simple chrome extension demo"}, "helloWorld": {"message": "Hello World!"} }

_locales\zh_CN\messages.json内容:

{ "pluginDesc": {"message": "一个简单的Chrome插件demo"}, "helloWorld": {"message": "你好啊,世界!"} }

manifest.jsonCSS文件中经过__MSG_messagename__引入,如:

{ "description": "__MSG_pluginDesc__", // 默认语言 "default_locale": "zh_CN", }

JS中则直接chrome.i18n.getMessage("helloWorld")

测试时,经过给chrome创建一个不一样的快捷方式chrome.exe --lang=en来切换语言,如:

英文效果:

中文效果:

8.8. API总结

比较经常使用用的一些API系列:

  • chrome.tabs
  • chrome.runtime
  • chrome.webRequest
  • chrome.window
  • chrome.storage
  • chrome.contextMenus
  • chrome.devtools
  • chrome.extension

经验总结

9.1. 查看已安装插件路径

已安装的插件源码路径:C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions,每个插件被放在以插件ID为名的文件夹里面,想要学习某个插件的某个功能是如何实现的,看人家的源码是最好的方法了:

如何查看某个插件的ID?进入 chrome://extensions ,而后勾线开发者模式便可看到了。

9.2. 特别注意background的报错

不少时候你发现你的代码会莫名其妙的失效,找来找去又找不到缘由,这时打开background的控制台才发现原来某个地方写错了致使代码没生效,正式因为background报错的隐蔽性(须要主动打开对应的控制台才能看到错误),因此特别注意这点。

9.3. 如何让popup页面不关闭

在对popup页面审查元素的时候popup会被强制打开没法关闭,只有控制台关闭了才能够关闭popup,缘由很简单:若是popup关闭了控制台就没用了。这种方法在某些状况下很实用!

9.4. 不支持内联JavaScript的执行

也就是不支持将js直接写在html中,好比:

<input id="btn" type="button" value="收藏" onclick="test()"/>

报错以下:

Refused to execute inline event handler because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

解决方法就是用JS绑定事件:

$('#btn').on('click', function(){alert('测试')});

另外,对于A标签,这样写href="javascript:;"而后用JS绑定事件虽然控制台会报错,可是不受影响,固然强迫症患者受不了的话只能写成href="#"了。

若是这样写:

<a href="javascript:;" id="get_secret">请求secret</a>

报错以下:

Refused to execute JavaScript URL because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-resource:". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

9.5. 注入CSS的时候必须当心

因为经过content_scripts注入的CSS优先级很是高,几乎仅次于浏览器默认样式,稍不注意可能就会影响一些网站的展现效果,因此尽可能不要写一些影响全局的样式。

之因此强调这个,是由于这个带来的问题很是隐蔽,不太容易找到,可能你正在写某个网页,昨天样式仍是好好的,怎么今天就忽然不行了?而后你辛辛苦苦找来找去,找了半天才发现居然是由于插件里面的一个样式影响的!

打包与发布

打包的话直接在插件管理页有一个打包按钮:

而后会生成一个.crx文件,要发布到Google应用商店的话须要先登陆你的Google帐号,而后花5个$注册为开发者,本人太穷,就懒得亲自验证了,有发布需求的本身去整吧。

参考

11.1. 官方资料

推荐查看官方文档,虽然是英文,可是全且新,国内的中文资料都比较旧(注意如下所有须要FQ):

11.2. 第三方资料

部分中文资料,不是特别推荐:

附图

附图:Chrome高清png格式logo:

 

=================================================

转帖地址:http://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html

相关文章
相关标签/搜索