Chrome Extension(扩展程序)开发
基本概念
chrome 扩展是什么?
一个应用(扩展)其实是压缩(.crx 后缀)在一起的一组文件,包括 HTML,CSS,Javascript 脚本,图片文件,还有其它任何需要的文件。 应用(扩展)本质上来说就是 web 页面,它们可以使用所有的浏览器提供的 API,从 XMLHttpRequest 到 JSON 到 HTML5 全都有。
应用(扩展)可以与 Web 页面交互,或者通过 content script 或 cross-origin XMLHttpRequests 与服务器交互。应用(扩展)还可以访问浏览器提供的内部功能,例如标签或书签等。
chrome 扩展用处
增强浏览器的功能,可以向页面注入 js 脚本作一些操作,实现属于自己的“定制版”浏览器。
提供了很多 API 供我们来使用:
开发与调试
Chrome 插件没有严格的项目结构要求,只要保证本目录有一个 manifest.json 即可,也不需要专门的 IDE,普通的 web 开发工具即可。
从右上角菜单->更多工具->扩展程序可以进入插件管理页面,也可以直接在地址栏输入 chrome://extensions 访问。
勾选开发者模式即可以文件夹的形式直接加载插件,否则只能安装.crx 格式的文件。Chrome 要求插件必须从它的 Chrome 应用商店安装,其它任何网站下载的都无法直接安装,所以,其实我们可以把 crx 文件解压,然后通过开发者模式直接加载。
开发中,代码有任何改动都必须重新加载插件,只需要在插件管理页按下 Ctrl+R 即可,以防万一最好还把页面刷新一下。
核心结构
这是一个 Chrome 插件最重要也是必不可少的文件,用来配置所有和插件相关的配置,必须放在根目录。
字段说明:
1 | { |
content-scripts,是 Chrome 插件中向页面注入脚本的一种形式(虽然名为 script,其实还可以包括 css 的),借助 content-scripts 我们可以实现通过配置的方式轻松向指定页面注入 JS 和 CSS,最常见的比如:广告屏蔽、页面 CSS 定制,等等。
配置 🌰:
1 | { |
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
背景页,是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在 background 里面。
background 的权限非常高,几乎可以调用所有的 Chrome 扩展 API(除了 devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置 CORS。不止是 background,所有的直接通过 chrome-extension://id/xx.html 这种方式打开的网页都可以无限制跨域。
1 | { |
需要特别说明的是,虽然可以通过 chrome-extension://xxx/background.html 直接打开后台页,但是打开的后台页和真正一直在后台运行的那个页面不是同一个,换句话说,可以打开无数个 background.html,但是真正在后台常驻的只有一个,而且永远看不到它的界面,只能调试它的代码。
- event-pages
鉴于 background 生命周期太长,长时间挂载后台可能会影响性能,所以 Google 又弄一个 event-pages,在配置文件上,它与 background 的唯一区别就是多了一个 persistent 参数。
1 | { |
它的生命周期是:在被需要时加载,在空闲时被关闭,什么叫被需要时呢?比如第一次安装、插件更新、有 content-script 向它发送消息,等等。
除了配置文件的变化,代码上也有一些细微变化,这个简单了解一下就行了,一般情况下 background 也不会很消耗性能的。
popup 是点击 browser_action 或者 page_action 图标时打开的一个小窗口网页,焦点离开网页就立即关闭,一般用来做一些临时性的交互。
popup 可以包含任意你想要的 HTML 内容,并且会自适应大小。可以通过 default_popup 字段来指定 popup 页面,也可以调用 setPopup()方法。
配置 🌰:
1 | { |
需要特别注意的是,由于单击图标打开 popup,焦点离开又立即关闭,所以 popup 页面的生命周期一般很短,需要长时间运行的代码千万不要写在 popup 里面。
在权限上,它和 background 非常类似,它们之间最大的不同是生命周期的不同,popup 中可以直接通过 chrome.extension.getBackgroundPage()获取 background 的 window 对象。
这里的 inject-script 是我给它取的,指的是通过 DOM 操作的方式向页面注入的一种 JS。为什么要把这种 JS 单独拿出来讨论呢?又或者说为什么需要通过这种方式注入 JS 呢?
这是因为 content-script 有一个很大的“缺陷”,也就是无法访问页面中的 JS,虽然它可以操作 DOM,但是 DOM 却不能调用它,也就是无法在 DOM 中通过绑定事件的方式调用 content-script 中的代码(包括直接写 onclick 和 addEventListener 两种方式都不行),但是,“在页面上添加一个按钮并调用插件的扩展 API”是一个很常见的需求,那该怎么办呢?其实这就是本小节要讲的。
在 content-script 中通过 DOM 方式向页面注入 inject-script 代码示例:
1 | // 向页面注入JS |
在 web 中直接访问插件中的资源的话必须显示声明才行,配置文件中增加如下
1 | { |
展现形式
通过配置 browser_action 可以在浏览器的右上角增加一个图标,一个 browser_action 可以拥有一个图标,一个 tooltip,一个 badge 和一个 popup。
- icon(图标)
browser_action 图标推荐使用宽高都为 19 像素的图片,更大的图标会被缩小,格式随意,一般推荐 png,可以通过 manifest 中 default_icon 字段配置,也可以调用 setIcon()方法。 - tooltip
修改 browser_action 的 manifest 中 default_title 字段,或者调用 setTitle()方法。
- badge
所谓 badge 就是在图标上显示一些文本,可以用来更新一些小的扩展状态提示信息。因为 badge 空间有限,所以只支持 4 个以下的字符(英文 4 个,中文 2 个)。badge 无法通过配置文件来指定,必须通过代码实现,设置 badge 文字和颜色可以分别使用 setBadgeText()和 setBadgeBackgroundColor()。
1 | chrome.browserAction.setBadgeText({ text: "new" }); |
所谓 pageAction,指的是只有当某些特定页面打开才显示的图标,它和 browserAction 最大的区别是一个始终都显示,一个只在特定情况才显示。
通过开发 Chrome 插件可以自定义浏览器的右键菜单,主要是通过 chrome.contextMenusAPI 实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等,如果有同一个插件里面定义了多个菜单,Chrome 会自动组合放到以插件名字命名的二级菜单里,如下:
1 | // manifest.json |
使用 override 页可以将 Chrome 默认的一些特定页面替换掉,改为使用扩展提供的页面。
扩展可以替代如下页面:
- 历史记录:从工具菜单上点击历史记录时访问的页面,或者从地址栏直接输入 chrome://history
- 新标签页:当创建新标签的时候访问的页面,或者从地址栏直接输入 chrome://newtab
- 书签:浏览器的书签,或者直接输入 chrome://bookmarks
注意:
- 一个扩展只能替代一个页面;
- 不能替代隐身窗口的新标签页;
- 网页必须设置 title,否则用户可能会看到网页的 URL,造成困扰;
1 | "chrome_url_overrides": |
Chrome 允许插件在开发者工具(devtools)上动手脚,主要表现在:
- 自定义一个和多个和 Elements、Console、Sources 等同级别的面板;
- 自定义侧边栏(sidebar),目前只能自定义 Elements 面板的侧边栏;
- devtools 扩展介绍
- 每打开一个开发者工具窗口,都会创建 devtools 页面的实例,F12 窗口关闭,页面也随着关闭,所以 devtools 页面的生命周期和 devtools 窗口是一致的。devtools 页面可以访问一组特有的 DevTools API 以及有限的扩展 API,这组特有的 DevTools API 只有 devtools 页面才可以访问,background 都无权访问,这些 API 包括:
- chrome.devtools.panels:面板相关;
- chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;
- chrome.devtools.network:获取有关网络请求的信息;
options 页,就是插件的设置页面,有 2 个入口,一个是右键图标有一个“选项”菜单,还有一个在插件管理页面:大部分扩展 API 都无法直接被 DevTools 页面调用,但它可以像 content-script 一样直接调用 chrome.extension 和 chrome.runtimeAPI,同时它也可以像 content-script 一样使用 Message 交互的方式与 background 页面进行通信。
1 | { |
omnibox 是向用户提供搜索建议的一种方式。
omnibox 应用程序界面允许向 Google Chrome 的地址栏注册一个关键字,地址栏也叫 omnibox。
当用户输入你的扩展关键字,用户开始与你的扩展交互。每个击键都会发送给你的扩展,扩展提供建议作为相应的响应。
建议可以被格式化多种方式。当用户接受建议,你的扩展被通知可以执行动作。
配置字段:
1 | { |
在 background.js 注册监听事件:
1 | chrome.omnibox.onInputChanged.addListener((text, suggest) => { |
通知用户发生了一些重要的事情。桌面通知会显示在浏览器窗口之外。
Chrome 提供了一个 chrome.notificationsAPI 以便插件推送桌面通知,暂未找到 chrome.notifications 和 HTML5 自带的 Notification 的显著区别及优势。
在后台 JS 中,无论是使用 chrome.notifications 还是 Notification 都不需要申请权限(HTML5 方式需要申请权限),直接使用即可。
五类 JS 文件区别
js 类型 | 可访问的 API | DOM 访问情况 | js 访问情况 | 跨域 | 调试方式 |
---|---|---|---|---|---|
content script | 只能访问 extension、runtime 等部分 API | ✔ | × | × | 打开 Console,如图切换 |
popup js | 可访问绝大部分 API,除了 devtools 系列 | 不可以直接访问 | × | ✔ | popup 页面右键选择“审查弹出内容” |
background js | 可访问绝大部分 API,除了 devtools 系列 | 不可以直接访问 | × | ✔ | 插件管理页面点击背景页 |
devtools js | 只能访问 devtools、extension、runtime 等部分 API | ✔ | ✔ | × | 暂时没有找到有效的调试方法 |
injected script | 和普通 JS 无任何差别,不能访问任何扩展 API | ✔ | ✔ | × | 直接普通的 F12 即可 |
消息通信
通信方式概览
- | 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 |
popup&&background
- popup 访问 background
popup 可以直接调用 background 的 JS 方法,并且可以直接访问 background 的 DOM
1 | // background.js |
- background 访问 popup
注意:前提是 popup 已经打开时
1 | var views = chrome.extension.getViews({ type: "popup" }); |
(popup/background)&&content-script
- popup.js/background.js
1 | function sendMessageToContentScript(message, callback) { |
- content-script 接收消息
1 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { |
双方通信直接发送的都是 JSON 对象,不是 JSON 字符串,所以无需解析,很方便(当然也可以直接发送字符串)。
content-script&&(background/popup)
- content-script.js
1 | chrome.runtime.sendMessage( |
- background.js/popup.js 接收消息
1 | // 监听来自content-script的消息 |
- content_script 向 popup 主动发消息的前提是 popup 必须打开!否则需要利用 background 作中转。
- 如果 background 和 popup 同时监听,那么它们都可以同时收到消息,但是只有一个可以 sendResponse,一个先发送了,那么另外一个再发送就无效。
inject-script&&content-script
content-script 和页面内的脚本(injected-script 自然也属于页面内的脚本)之间唯一共享的东西就是页面的 DOM 元素,有 2 种方法可以实现二者通讯:
- 1.可以通过 window.postMessage 和 window.addEventListener 来实现二者消息通讯(推荐)
- inject-script:
1 | window.postMessage({ test: "Hello,Content" }, "*"); |
- content-script:
1 | window.addEventListener( |
- 2.通过自定义 DOM 事件来实现
- inject-script:
1 | var customEvent = document.createEvent("Event"); |
- content-script:
1 | var hiddenDiv = document.getElementById("myCustomEventDiv"); |
补充
动态注入 JS、CSS
虽然在 background 和 popup 中无法直接访问页面 DOM,但是可以通过 chrome.tabs.executeScript 来执行脚本,从而实现访问 web 页面的 DOM(注意,这种方式也不能直接访问页面 JS)。
manifest.json 配置 🌰:
1 | { |
1 | // 动态执行JS代码 |
获取当前窗口 ID
1 | chrome.windows.getCurrent(function (currentWindow) { |
获取当前标签页 ID
1 | // 获取当前选项卡ID |
本地存储
本地存储建议用 chrome.storage 而不是普通的 localStorage,区别有好几点,最重要的 2 点区别是:
- chrome.storage 是针对插件全局的,即使你在 background 中保存的数据,在 content-script 也能获取到。
- chrome.storage.sync 可以跟随当前登录用户自动同步,这台电脑修改的设置会自动同步到其它电脑,很方便,如果没有登录或者未联网则先保存到本地,等登录了再同步至网络。
需要声明 storage 权限,有 chrome.storage.sync 和 chrome.storage.local2 种方式可供选择,使用示例 🌰 如下:
1 | // 读取数据,第一个参数是指定要读取的key以及设置默认值 |
webRequest
通过 webRequest 系列 API 可以对 HTTP 请求进行任性地修改、定制。
生命周期图:
1 | //manifest.json |
示例 🌰:
1 | // 每次请求前触发,可以拿到 requestBody 数据,同时可以对本次请求作出干预修改 |
上面示例中提到,使用 webRequestAPI 是无法拿到 responseBody 的,想要拿到的话只能采取一些变通方法,例如:
- 重写 XmlHttpRequest 和 fetch,增加自定义拦截事件,缺点是实现方式可能有点脏,重写不好的话可能影响正常页面。
- devtools 的 chrome.devtools.network.onRequestFinishedAPI 可以拿到返回的 body,缺点是必须打开开发者面板。
- 使用 chrome.debugger.sendCommand 发送 Network.getResponseBody 命令来获取 body 内容,缺点也很明显,会有一个恼人的提示。
具体的实现方式可以查看https://stackoverflow.com/questions/18534771/chrome-extension-how-to-get-http-response-body
国际化
插件根目录新建一个名为_locales 的文件夹,再在下面新建一些语言的文件夹,如 en、zh_CN、zh_TW,然后再在每个文件夹放入一个 messages.json,同时必须在清单文件中设置 default_locale。
- _locales\en\messages.json:
1 | { |
- _locales\zh_CN\messages.json:
1 | { |
- 在 manifest.json 和 CSS 文件中通过MSG_messagename引入,如:
1 | { |
- 在 JS 中则直接 chrome.i18n.getMessage(“helloWorld”)。
- 测试时,通过给 chrome 建立一个不同的快捷方式 chrome.exe –lang=en 来切换语言
常见的 API
所有的 Chrome API 都是以 chrome 对象开头,🌰 如:chrome.tabs
- bookmarks 操纵书签的 API
- browserAction 获取扩展图标、标题、文字、弹出页等
- browsingData 控制浏览器的浏览数据,从本地文件
- commands 给扩展添加快捷键
- contextMenus 添加选项到右键弹出菜单
- cookies 控制 cookies
- desktopCapture 捕获屏幕、个人窗口或标签内容
- downloads 下载控制
- events 事件相关 API
- extension 获取扩展的各部分,也能与各部分交换信息
- extensionTypes 扩展的类型声明
- gcm 启用 google 云消息服务,收发消息
- history 历史记录控制
- i18n 多语言国际化支持
- idle 取得机器闲置状态
- management 管理扩展与应用
- notifications 通知控制
- pageAction 具体的页面下控制扩展图标、标题、文字、弹出页等相关内容
- permissions 获取拥有的权限
- power 请求系统常亮
- runtime 获取运行时相关信息,包括后台页、manifest 等等
- sessions 查询或恢复浏览会话
- storage 存储相关
- tabs 与标签页交互
- vpnProvider 实现 vpn 客户端需要使用的东西
- webRequest 拦截、修改、阻塞请求
- windows 创建、修改、重排窗口
踩坑
查看已安装插件路径
查看本地已安装的插件源码路径 C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions,每一个插件被放在以插件 ID 为名的文件夹里面,想要学习某个插件的某个功能是如何实现的,看人家的源码是最好的方法了。
Mac 系统插件本地缓存位于:/Users/用户名/Library/Application Support/Google/Chrome/Default/Extensions 文件夹
如何查看某个插件的 ID?进入 chrome://extensions ,然后勾选开发者模式即可看到了。
background 的报错
很多时候你发现你的代码会莫名其妙的失效,找来找去又找不到原因,这时打开 background 的控制台才发现原来某个地方写错了导致代码没生效,这是由于 background 报错的隐蔽性(需要主动打开对应的控制台才能看到错误)
如何让 popup 页面不关闭
在对 popup 页面审查元素的时候 popup 会被强制打开无法关闭,只有控制台关闭了才可以关闭 popup,原因很简单:如果 popup 关闭了控制台就没用了。这种方法在某些情况下很实用!
不支持内联 JavaScript 的执行
就是不支持将 js 直接写在 html 中,例如 🌰:
1 | <input id="btn" type="button" value="按钮" onclick="handleBtn()" /> |
只能通过 js 绑定事件:
1 | $("#btn").on("click", function () { |
另外,对于 A 标签,这样写 href=”javascript:;”然后用 JS 绑定事件虽然控制台会报错,但是不受影响,当然强迫症患者受不了的话只能写成 href=”#”了。
1 | <a href="javascript:;" id="get_res">请求</a> |
注入 CSS
由于通过 content_scripts 注入的 CSS 优先级非常高,几乎仅次于浏览器默认样式,稍不注意可能就会影响一些网站的展示效果,所以尽量不要写一些影响全局的样式。
之所以强调这个,是因为这个带来的问题非常隐蔽,不太容易找到,可能你正在写某个网页,昨天样式还是好好的,怎么今天就突然不行了?然后你辛辛苦苦找来找去,找了半天才发现竟然是因为插件里面的一个样式影响的!
原文:http://blog.haoji.me/chrome-plugin-develop.html#dong-tai-zhu-ru-huo-zhi-xing-JS