2023年11月25日发(作者:)

⾕歌浏览器Chrome插件扩展开发教程

下⽂是我看到的⼀篇很好的Chrome扩展开发攻略,感觉很有价值,内容详尽,排版精美,遂转载。

原⽂由⼩茗同学发表于:

1. 写在前⾯

另外,本⽂图⽚较多,且图⽚服务器带宽有限,右下⾓的⽬录滚动监听必须等到图⽚全部加载完毕之后才会触发,所以请耐⼼等待加载完

毕。

2. 前⾔

2.1. 什么是Chrome插件

严格来讲,我们正在说的东西应该叫Chrome扩展(Chrome Extension),真正意义上的Chrome插件是更底层的浏览器功能扩展,可能需

要对浏览器源码有⼀定掌握才有能⼒去开发。鉴于Chrome插件的叫法已经习惯,本⽂也全部采⽤这种叫法,但读者需深知本⽂所描述的

Chrome插件实际上指的是Chrome扩展。

Chrome插件是⼀个⽤Web技术开发、⽤来增强浏览器功能的软件,它其实就是⼀个由HTML、CSS、JS、图⽚等资源组成的⼀个.crx后缀

的压缩包.

个⼈猜测crx可能是如下3个字母的简写:

Chrome Extension

另外,其实不只是前端技术,Chrome插件还可以配合C++编写的dll动态链接库实现⼀些更底层的功能(NPAPI),⽐如全屏幕截图。

书签控制;

下载控制;

窗⼝控制;

标签控制;

⽹络请求控制,各类事件监听;

⾃定义原⽣菜单;

完善的通信机制;

等等;

2.3. 为什么是Chrome插件⽽不是Firefox插件

Chrome占有率更⾼,更多⼈⽤;

开发更简单;

应⽤场景更⼴泛,Firefox插件只能运⾏在Firefox上,⽽Chrome除了Chrome浏览器之外,还可以运⾏在所有webkit内核的国产浏览

器,⽐如360极速浏览器、360安全浏览器、搜狗浏览器、QQ浏览器等等;

除此之外,Firefox浏览器也对Chrome插件的运⾏提供了⼀定的⽀持;

3. 开发与调试

Chrome插件没有严格的项⽬结构要求,只要保证本⽬录有⼀个即可,也不需要专门的IDE,普通的web开发⼯具即可。

从右上⾓菜单->更多⼯具->扩展程序可以进⼊ 插件管理页⾯,也可以直接在地址栏输⼊ chrome://extensions 访问。

勾选即可以⽂件夹的形式直接加载插件,否则只能安装格式的⽂件。Chrome要求插件必须从它的Chrome应⽤商店安装,

4. 核⼼介绍

4.1

这是⼀个Chrome插件最重要也是必不可少的⽂件,⽤来配置所有和插件相关的配置,必须放在根⽬录。其

中,3个是必不可少的,是推荐的。

manifest_versionnameversiondescriptionicons

下⾯给出的是⼀些常见的配置项,均有中⽂注释,完整的配置⽂档请戳。

{

// 2

清单⽂件的版本,这个必须写,⽽且必须是

"manifest_version": 2,

//

插件的名称

"name": "demo",

//

插件的版本

"version": "1.0.0",

//

插件描述

"description": "简单的Chrome扩展demo",

//

图标,⼀般偷懒全部⽤⼀个尺⼨的也没问题

"icons":

{

"16": "img/",

"48": "img/",

"128": "img/"

},

// JS

会⼀直常驻的后台或后台页⾯

"background":

{

// 2JS

种指定⽅式,如果指定,那么会⾃动⽣成⼀个背景页

"page": ""

//"scripts": ["js/"]

},

// browser_actionpage_actionapp

浏览器右上⾓图标设置,必须三选⼀

"browser_action":

{

"default_icon": "img/",

//

图标悬停时的标题,可选

"default_title": "这是⼀个⽰例Chrome插件",

"default_popup": ""

},

//

当某些特定页⾯打开才显⽰的图标

/*"page_action":

{

"default_icon": "img/",

"default_title": "pageAction",

我是

"default_popup": ""

},*/

// JS

需要直接注⼊页⾯的

"content_scripts":

[

{

//"matches": ["*/*", "*/*"],

// ""

表⽰匹配所有地址

"matches": [""],

// JS

多个按顺序注⼊

"js": ["js/", "js/"],

// JSCSS

的注⼊可以随便⼀点,但是的注意就要千万⼩⼼了,因为⼀不⼩⼼就可能影响全局样式

"css": ["css/"],

// "document_start", "document_end", or "document_idle"document_idle

代码注⼊的时间,可选值:,最后⼀个表⽰页⾯空闲时,默认

// "document_start", "document_end", or "document_idle"document_idle

代码注⼊的时间,可选值:,最后⼀个表⽰页⾯空闲时,默认

"run_at": "document_start"

},

// content-script

这⾥仅仅是为了演⽰可以配置多个规则

{

"matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],

"js": ["js/"]

}

],

//

权限申请

"permissions":

[

"contextMenus", //

右键菜单

"tabs", //

标签

"notifications", //

通知

"webRequest", // web

请求

"webRequestBlocking",

"storage", //

插件本地存储

"*/*", // executeScriptinsertCSS

可以通过或者访问的⽹站

"*/*" // executeScriptinsertCSS

可以通过或者访问的⽹站

],

//

普通页⾯能够直接访问的插件资源列表,如果不设置是⽆法直接访问的

"web_accessible_resources": ["js/"],

//

插件主页,这个很重要,不要浪费了这个免费⼴告位

"homepage_url": "",

//

覆盖浏览器默认页⾯

"chrome_url_overrides":

{

//

覆盖浏览器默认的新标签页

"newtab": ""

},

// Chrome40

以前的插件配置页写法

"options_page": "",

// Chrome402Chrome

以后的插件配置页写法,如果个都写,新版只认后⾯这⼀个

"options_ui":

{

"page": "",

//

添加⼀些默认的样式,推荐使⽤

"chrome_style": true

},

//

向地址栏注册⼀个关键字以提供搜索建议,只能设置⼀个关键字

"omnibox": { "keyword" : "go" },

//

默认语⾔

"default_locale": "zh_CN",

// devtoolsHTMLJS

页⾯⼊⼝,注意只能指向⼀个⽂件,不能是⽂件

"devtools_page": ""

}

4.2.

content-scripts

所谓,其实就是Chrome插件中向页⾯注⼊脚本的⼀种形式(虽然名为script,其实还可以包括css的),借助

content-scriptscontent-

scripts

我们可以实现通过配置的⽅式轻松向指定页⾯注⼊JS和CSS(如果需要动态注⼊,可以参考下⽂),最常见的⽐如:⼴告屏蔽、页⾯

CSS定制,等等。

⽰例配置:

{

// JS

需要直接注⼊页⾯的

"content_scripts":

[

{

//"matches": ["*/*", "*/*"],

// ""

表⽰匹配所有地址

"matches": [""],

// JS

多个按顺序注⼊

"js": ["js/", "js/"],

// JSCSS

的注⼊可以随便⼀点,但是的注意就要千万⼩⼼了,因为⼀不⼩⼼就可能影响全局样式

"css": ["css/"],

// "document_start", "document_end", or "document_idle"document_idle

代码注⼊的时间,可选值:,最后⼀个表⽰页⾯空闲时,默认

"run_at": "document_start"

}

],

}

特别注意,如果没有主动指定(默认为),下⾯这种代码是不会⽣效的:

run_atdocument_startdocument_idle

document.addEventListener('DOMContentLoaded', function()

{

console.log('我被执⾏了!');

});

content-scriptsDOMJSJSinjected jscontent-

和原始页⾯共享,但是不共享,如要访问页⾯(例如某个JS变量),只能通过来实现。

scripts

不能访问绝⼤部分,除了下⾯这4种:

ion(getURL , inIncognitoContext , lastError , onRequest , sendRequest)

chrome.i18n

e(connect , getManifest , getURL , id , onConnect , onMessage , sendMessage)

e

其实看到这⾥不要悲观,这些API绝⼤部分时候都够⽤了,⾮要调⽤其它API的话,你还可以通过通信来实现让background来帮你调⽤

(关于通信,后⽂有详细介绍)。

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

4.3.

background

后台(姑且这么翻译吧),是⼀个常驻的页⾯,它的⽣命周期是插件中所有类型页⾯中最长的,它随着浏览器的打开⽽打开,随着浏览器的

关闭⽽关闭,所以通常把需要⼀直运⾏的、启动就运⾏的、全局的代码放在background⾥⾯。

background的权限⾮常⾼,⼏乎可以调⽤所有的Chrome扩展API(除了),⽽且它可以⽆限制跨域,也就是可以跨域访问任何

devtools

⽹站⽽⽆需要求对⽅设置CORS。

经过测试,其实不⽌是,所有的直接通过这种⽅式打开的⽹页都可以⽆限制跨域。

backgroundchrome-extension://id/

配置中,background可以通过page指定⼀张⽹页,也可以通过scripts直接指定⼀个JS,Chrome会⾃动为这个JS⽣成⼀个默认的⽹页:

{

// JS

会⼀直常驻的后台或后台页⾯

"background":

{

// 2JS

种指定⽅式,如果指定,那么会⾃动⽣成⼀个背景页

"page": ""

//"scripts": ["js/"]

},

}

需要特别说明的是,虽然你可以通过直接打开后台页,但是你打开的后台页和真正⼀直在后台运⾏的

chrome-extension://xxx/

那个页⾯不是同⼀个,换句话说,你可以打开⽆数个,但是真正在后台常驻的只有⼀个,⽽且这个你永远看不到它的界⾯,

只能调试它的代码。

4.4.

event-pages

这⾥顺带介绍⼀下,它是⼀个什么东西呢?鉴于background⽣命周期太长,长时间挂载后台可能会影响性能,所以Google⼜

event-pages

弄⼀个event-pages,在配置⽂件上,它与的唯⼀区别就是多了⼀个参数:

backgroundpersistent

{

"background":

{

"scripts": [""],

"persistent": false

},

}

它的⽣命周期是:在被需要时加载,在空闲时被关闭,什么叫被需要时呢?⽐如第⼀次安装、插件更新、有content-script向它发送消息,

等等。

除了配置⽂件的变化,代码上也有⼀些细微变化,个⼈这个简单了解⼀下就⾏了,⼀般情况下background也不会很消耗性能的。

4.5.

popup

popup是点击或者图标时打开的⼀个⼩窗⼝⽹页,焦点离开⽹页就⽴即关闭,⼀般⽤来做⼀些临时性的交互。

browser_actionpage_action

博客园⽹摘插件popup效果

popupdefault_popupsetPopup()

可以包含任意你想要的HTML内容,并且会⾃适应⼤⼩。可以通过字段来指定popup页⾯,也可以调⽤

法。

配置⽅式:

{

"browser_action":

{

"default_icon": "img/",

//

图标悬停时的标题,可选

"default_title": "这是⼀个⽰例Chrome插件",

"default_popup": ""

}

}

需要特别注意的是,由于单击图标打开popup,焦点离开⼜⽴即关闭,所以popup页⾯的⽣命周期⼀般很短,需要长时间运⾏的代码千万不

要写在popup⾥⾯。

在权限上,它和background⾮常类似,它们之间最⼤的不同是⽣命周期的不同,popup中可以直接通

获取background的window对象。

kgroundPage()

4.6.

injected-script

这⾥的injected-script是我给它取的,指的是通过DOM操作的⽅式向页⾯注⼊的⼀种JS。为什么要把这种JS单独拿出来讨论呢?⼜或者说

为什么需要通过这种⽅式注⼊JS呢?

这是因为content-script有⼀个很⼤的“缺陷”,也就是⽆法访问页⾯中的JS,虽然它可以操作DOM,但是DOM却不能调⽤它,也就是⽆

法在DOM中通过绑定事件的⽅式调⽤中的代码(包括直接写onclick和种⽅式都不⾏),但是,“在页⾯上

content-scriptaddEventListener2

添加⼀个按钮并调⽤插件的扩展API”是⼀个很常见的需求,那该怎么办呢?其实这就是本⼩节要讲的。

中通过DOM⽅式向页⾯注⼊代码⽰例:

content-scriptinject-script

// JS

向页⾯注⼊

function injectCustomJs(jsPath)

{

jsPath = jsPath || 'js/';

var temp = document.createElement('script');

temp.setAttribute('type', 'text/javascript');

// chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/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-scriptcontent-script

4.7.

homepage_url

开发者或者插件主页设置,⼀般会在如下2个地⽅显⽰:

5. Chrome插件的8种展⽰形式

5.1. browserAction(浏览器右上⾓)

通过配置可以在浏览器的右上⾓增加⼀个图标,⼀个可以拥有⼀个图标,⼀个,⼀个和⼀

browser_actionbrowser_actiontooltipbadge

popup

⽰例配置如下:

"browser_action":

{

"default_icon": "img/",

"default_title": "这是⼀个⽰例Chrome插件",

"default_popup": ""

}

5.1.1. 图标

browser_action图标推荐使⽤宽⾼都为19像素的图⽚,更⼤的图标会被缩⼩,格式随意,⼀般推荐png,可以通

字段配置,也可以调⽤⽅法。

manifestdefault_iconsetIcon()

5.1.2. tooltip

修改字段,或者调⽤⽅法。

browser_actionmanifestdefault_titlesetTitle()

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。

(tabId) 显⽰图标;

(tabId) 隐藏图标;

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

//

{

"page_action":

{

"default_icon": "img/",

"default_title": "我是pageAction",

"default_popup": ""

},

"permissions": ["declarativeContent"]

}

//

chrome.runtime.onInstalled.addListener(function(){

chrome.declarativeContent.onPageChanged.removeRules(undefined, function(){

chrome.declarativeContent.onPageChanged.addRules([

{

conditions: [

// pageAction

只有打开百度才显⽰

new chrome.declarativeContent.PageStateMatcher({pageUrl: {urlContains: ''}})

],

actions: [new chrome.declarativeContent.ShowPageAction()]

}

]);

});

});

效果图:

5.3. 右键菜单

通过开发Chrome插件可以⾃定义浏览器的右键菜单,主要是通过tMenusAPI实现,右键菜单可以出现在不同的上下⽂,

⽐如普通页⾯、选中的⽂字、图⽚、链接,等等,如果有同⼀个插件⾥⾯定义了多个菜单,Chrome会⾃动组合放到以插件名字命名的⼆级

菜单⾥,如下:

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

//

{"permissions": ["contextMenus"]}

//

chrome.contextMenus.create({

title: "测试右键菜单",

onclick: function(){alert('您点击了右键菜单!');}

});

效果:

5.3.2. 添加右键百度搜索

//

{"permissions": ["contextMenus" "tabs"]}

//

chrome.contextMenus.create({

title: '使⽤度娘搜索:%s', // %s

表⽰选中的⽂字

contexts: ['selection'], //

只有当选中⽂字时才会出现此右键菜单

onclick: function(params)

{

// locationbackgroundwindow

注意不能使⽤,因为是属于对象

chrome.tabs.create({url: '/s?ie=utf-8&wd=' + encodeURI(params.selectionText)});

}

});

效果如下:

5.3.3. 语法说明

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: '*./*' //

只在某些页⾯显⽰此右键菜单

});

// 删除某⼀个菜单项

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": "",

"history": "",

"bookmarks": ""

}

5.5. devtools(开发者⼯具)

5.5.1. 预热

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

是的,Chrome允许插件在开发者⼯具(devtools)上动⼿脚,主要表现在:

⾃定义⼀个和多个和Elements、Console、Sources等同级别的⾯板;

⾃定义侧边栏(sidebar),⽬前只能⾃定义Elements⾯板的侧边栏;

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

来⼀张官⽅图⽚:

每打开⼀个开发者⼯具窗⼝,都会创建devtools页⾯的实例,F12窗⼝关闭,页⾯也随着关闭,所以devtools页⾯的⽣命周期和devtools

窗⼝是⼀致的。devtools页⾯可以访问⼀组特有的DevTools API以及有限的扩展API,这组特有的DevTools API只有devtools页⾯才可以

访问,background都⽆权访问,这些API包括:

:⾯板相关;

tedWindow:获取被审查窗⼝的有关信息;

k:获取有关⽹络请求的信息;

⼤部分扩展API都⽆法直接被DevTools页⾯调⽤,但它可以像content-script⼀样直接调⽤ion和

eAPI,同时它也可以像content-script⼀样使⽤Message交互的⽅式与background页⾯进⾏通信。

5.5.3. 实例:创建⼀个devtools扩展

⾸先,要针对开发者⼯具开发插件,需要在清单⽂件声明如下:

{

// HTMLJS

只能指向⼀个⽂件,不能是⽂件

"devtools_page": ""

}

这个⾥⾯⼀般什么都没有,就引⼊⼀个js:

<html>

<head>head>

<body>

<script type="text/javascript" src="js/">script>

body>

html>

可以看出来,其实真正代码是,html⽂件是“多余”的,所以这⾥觉得有点坑,devtools_page⼲嘛不允许直接指定JS呢?

再来看的代码:

//

创建⾃定义⾯板,同⼀个插件可以创建多个⾃定义⾯板

// panel

⼏个参数依次为:标题、图标(其实设置了也没地⽅显⽰)、要加载的页⾯、加载成功后的回调

chrome.devtools.panels.create('MyPanel', 'img/', '', function(panel)

{

console.log('⾃定义⾯板创建成功!'); // log

注意这个⼀般看不到

});

//

创建⾃定义侧边栏

chrome.devtools.panels.elements.createSidebarPane("Images", function(sidebar)

{

// e('../'); //

指定加载某个页⾯

sidebar.setExpression('electorAll("img")', 'All Images'); //

通过表达式来指定

//ect({aaa: 111, bbb: 'Hello World!'}); //

直接设置显⽰某个对象

});

setPage时的效果:

// jQuery

检测

document.getElementById('check_jquery').addEventListener('click', function()

{

// DOMinspectedWindow

访问被检查的页⾯需要使⽤

// jQuery

简单例⼦:检测被检查页⾯是否使⽤了

chrome.devtools.inspectedWindow.eval("", 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("", function(result, isException)

{

chrome.devtools.panels.openResource(result, 20, function()

{

console.log('资源打开成功!');

});

});

});

//

审查元素

document.getElementById('test_inspect').addEventListener('click', function()

{

chrome.devtools.inspectedWindow.eval("inspect([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/

定报错,因为不⽀持相关特殊API,只能先⾃⼰写⼀些⽅法屏蔽这些错误,调试通了再放开。

5.6. option(选项页)

所谓options页,就是插件的设置页⾯,有2个⼊⼝,⼀个是右键图标有⼀个“选项”菜单,还有⼀个在插件管理页⾯:

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

我们先看⽼版的options:

{

// Chrome40

以前的插件配置页写法

"options_page": "",

}

这个页⾯⾥⾯的内容就随你⾃⼰发挥了,配置之后在插件管理页就会看到⼀个选项按钮⼊⼝,点进去就是打开⼀个⽹页,没啥好讲的。

效果:

再来看新版的optionsV2:

{

"options_ui":

{

"page": "",

//

添加⼀些默认的样式,推荐使⽤

的代码我们没有任何改动,只是配置⽂件改了,之后效果如下:

看起来是不是⾼⼤上了?

⼏点注意:

为了兼容,建议2种都写,如果都写了,Chrome40以后会默认读取新版的⽅式;

{

//

向地址栏注册⼀个关键字以提供搜索建议,只能设置⼀个关键字

"omnibox": { "keyword" : "go" },

}

然后中注册监听事件:

// 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 = '/search/index?tn=baiduimage&ie=utf-8&word=' + text;

else if(text.startsWith('百度搜索')) href = '/s?ie=UTF-8&wd=' + text.replace('百度搜索 ', '');

else if(text.startsWith('⾕歌搜索')) href = '/search?q=' + text.replace('⾕歌搜索 ', '');

else href = '/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提供了⼀个cationsAPI以便插件推送桌⾯通知,暂未找到cations和HTML5⾃带的Notification的显

著区别及优势。

在后台JS中,⽆论是使⽤cations还是Notification都不需要申请权限(HTML5⽅式需要申请权限),直接使⽤即可。

最简单的通知:

代码:

chrome.notifications.create(null, {

type: 'basic',

iconUrl: 'img/',

title: '这是标题',

message: '您刚才点击了⾃定义右键菜单!'

});

通知的样式可以很丰富:

这个没有深⼊研究,有需要的可以去看官⽅⽂档。

6. 5种类型的JS对⽐

Chrome插件的JS主要可以分为这5类:injected script、content-script、popup js、background js和devtools js,

6.1. 权限对⽐

6.2. 调试⽅式对⽐

JS类型调试⽅式图⽚说明

injected script直接普通的F12即可懒得截图

content-script打开Console,如图切换

popup-jspopup页⾯右键审查元素

background插件管理页点击背景页即可

JS类型调试⽅式图⽚说明

devtools-js暂未找到有效⽅法-

7. 消息通信

前⾯我们介绍了Chrome插件中存在的5种JS,那么它们之间如何互相通信呢?下⾯先来系统概况⼀下,然后再分类细说。需要知道的

是,popup和background其实⼏乎可以视为⼀种东西,因为它们可访问的API都⼀样、通信机制⼀样、都可以跨域。

7.1. 互相通信概览

注:-表⽰不存在或者⽆意义,或者待验证。

7.2. 通信详细介绍

7.2.1. popupbackground

popup可以直接调⽤background中的JS⽅法,也可以直接访问background的DOM:

//

function test()

{

alert('我是background');

}

//

var bg = chrome.extension.getBackgroundPage();

bg.test(); // bg

访问的函数

alert(bg.document.body.innerHTML); // bgDOM

访问

⼩插曲,今天碰到⼀个情况,发现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或者bgcontent主动发送消息

或者:

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);

});

接收:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse)

{

// ( ?"from a content script:" + :"from the extension");

if(request.cmd == 'test') alert(request.value);

sendResponse('我收到了你的消息!');

});

双⽅通信直接发送的都是JSON对象,不是JSON字符串,所以⽆需解析,很⽅便(当然也可以直接发送字符串)。

⽹上有些⽼代码中⽤的是age,没有完全查清⼆者的区别(貌似是别名),但是建议统⼀使⽤

age。

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

chrome.runtime.sendMessage({greeting: '你好,我是content-script呀,我主动发消息给后台!'}, function(response) {

console.log('收到来⾃后台的回复:' + response);

});

或者 :

// 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种⽅法可以实

现⼆者通讯:

可以通过ssage和ntListener来实现⼆者消息通讯;

通过⾃定义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种通信⽅式,⼀个是短连接(ssage和

ssage),⼀个是长连接(t和t)。

短连接的话就是挤⽛膏⼀样,我发送⼀下,你收到了再回复⼀下,如果对⽅不回复,你只能重新发,⽽长连接类似WebSocket会⼀直建⽴

连接,双⽅可以随时互发消息。

短连接上⾯已经有代码⽰例了,这⾥只讲⼀下长连接。

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: '哦,原来是你啊!'});

}

});

});

//

监听长连接

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. 其它补充

8.1. 动态注⼊或执⾏JS

虽然在background和popup中⽆法直接访问页⾯DOM,但是可以通过eScript来执⾏脚本,从⽽实现访问web页⾯

的DOM(注意,这种⽅式也不能直接访问页⾯JS)。

⽰例配置:

{

"name": "动态JS注⼊演⽰",

...

"permissions": [

"tabs", "*/*", "*/*"

],

...

}

JS:

// JS

动态执⾏代码

chrome.tabs.executeScript(tabId, {code: 'oundColor="red"'});

// JS

动态执⾏⽂件

chrome.tabs.executeScript(tabId, {file: ''});

8.2. 动态注⼊CSS

⽰例配置:

{

"name": "动态CSS注⼊演⽰",

...

"permissions": [

"tabs", "*/*", "*/*"

],

...

}

JS代码:

```js

// CSSTODO

动态执⾏代码,,这⾥有待验证

chrome.tabs.insertCSS(tabId, {code: 'xxx'});

// CSS

动态执⾏⽂件

chrome.tabs.insertCSS(tabId, {file: ''});

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. 本地存储

本地存储建议⽤e⽽不是普通的localStorage,区别有好⼏点,个⼈认为最重要的2点区别是:

e是针对插件全局的,即使你在background中保存的数据,在content-script也能获取到;

可以跟随当前登录⽤户⾃动同步,这台电脑修改的设置会⾃动同步到其它电脑,很⽅便,如果没有登录或者未联⽹

则先保存到本地,等登录了再同步⾄⽹络;

需要声明storage权限,有和2种⽅式可供选择,使⽤⽰例如下:

// 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来简单演⽰⼀下它的冰⼭⼀⾓:

//

{

//

权限申请

"permissions":

[

"webRequest", // web

请求

"webRequestBlocking", // web

阻塞式请求

"storage", //

插件本地存储

"*/*", // executeScriptinsertCSS

可以通过或者访问的⽹站

"*/*" // executeScriptinsertCSS

可以通过或者访问的⽹站

],

}

//

//

是否显⽰图⽚

var showImage;

chrome.storage.sync.get({showImage: true}, function(items) {

showImage = items.showImage;

});

// webwebRequestBlocking

请求监听,最后⼀个参数表⽰阻塞式,需单独声明权限:

chrome.webRequest.onBeforeRequest.addListener(details => {

// cancel

表⽰取消本次请求

if(!showImage && details.type == 'image') return {cancel: true};

//

简单的⾳视频检测

// typemedia

⼤部分⽹站视频的并不是,且视频做了防下载处理,所以这⾥仅仅是为了演⽰效果,⽆实际意义

if(details.type == 'media') {

chrome.notifications.create(null, {

type: 'basic',

iconUrl: 'img/',

title: '检测到⾳视频',

message: '⾳视频地址:' + details.url,

});

}

}, {urls: [""]}, ["blocking"]);

8.7. 国际化

插件根⽬录新建⼀个名为_locales的⽂件夹,再在下⾯新建⼀些语⾔的⽂件夹,如en、zh_CN、zh_TW,然后再在每个⽂件夹放⼊⼀个

,同时必须在清单⽂件中设置default_locale。

_内容:

{

"pluginDesc": {"message": "A simple chrome extension demo"},

"helloWorld": {"message": "Hello World!"}

}

_localeszh_内容:

{

"pluginDesc": {"message": "⼀个简单的Chrome插件demo"},

"helloWorld": {"message": "你好啊,世界!"}

}

在和CSS⽂件中通过__MSG_messagename__引⼊,如:

{

"description": "__MSG_pluginDesc__",

//

默认语⾔

"default_locale": "zh_CN",

}

JS中则直接

sage("helloWorld")

测试时,通过给chrome建⽴⼀个不同的快捷⽅式 --lang=en来切换语⾔,如:

8.8. API总结

⽐较常⽤⽤的⼀些API系列:

已安装的插件源码路径:C:Users⽤户名AppDataLocalGoogleChromeUser DataDefaultExtensions,每⼀个插件被放在以

插件ID为名的⽂件夹⾥⾯,想要学习某个插件的某个功能是如何实现的,看⼈家的源码是最好的⽅法了:

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

9.2. 特别注意background的报错

很多时候你发现你的代码会莫名其妙的失效,找来找去⼜找不到原因,这时打开background的控制台才发现原来某个地⽅写错了导致代码

没⽣效,正式由于background报错的隐蔽性(需要主动打开对应的控制台才能看到错误),所以特别注意这点。

9.3. 如何让popup页⾯不关闭

在对popup页⾯审查元素的时候popup会被强制打开⽆法关闭,只有控制台关闭了才可以关闭popup,原因很简单:如果popup关闭了控

制台就没⽤了。这种⽅法在某些情况下很实⽤!

9.4. 不⽀持内联JavaScript的执⾏

也就是不⽀持将js直接写在html中,⽐如:

报错如下: ```java 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 (''), or a

nonce ('') is required to enable inline execution. ``` 解决⽅法就是⽤JS绑定事件: ``` $('#btn').on('click', function()

{alert('测试')}); ``` 另外,对于A标签,这样写href="javascript:;"然后⽤JS绑定事件虽然控制台会报错,但是不受影响,当然强迫症患者

受不了的话只能写成href="#"了。

如果这样写:

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

报错如下:

Refused to execute JavaScript URL because it violates the following Content Security Policy directive: "script-src 'self' blob: filesystem: chrome-extension-r

esource:". Either the 'unsafe-inline' keyword, a hash (''), or a nonce ('') is required to enable inline execution.

9.5. 注⼊CSS的时候必须⼩⼼

由于通过content_scripts注⼊的CSS优先级⾮常⾼,⼏乎仅次于浏览器默认样式,稍不注意可能就会影响⼀些⽹站的展⽰效果,所以尽量

不要写⼀些影响全局的样式。

之所以强调这个,是因为这个带来的问题⾮常隐蔽,不太容易找到,可能你正在写某个⽹页,昨天样式还是好好的,怎么今天就突然不⾏

了?然后你⾟⾟苦苦找来找去,找了半天才发现竟然是因为插件⾥⾯的⼀个样式影响的!

10. 打包与发布

Chrome插件官⽅⽂档主页