前言

什么是 Chrome 插件

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

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

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

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

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

增强浏览器功能,轻松实现属于自己的“定制版”浏览器,等等。

Chrome 插件提供了很多实用 API 供我们使用,包括但不限于:

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

为什么是 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 即可,以防万一最好还把页面刷新一下。

替换 npm 源—使用淘宝镜像

淘宝团队提供了一个官方的 npm 镜像服务,几乎是实时同步 npm 官方库。

  1. 执行以下命令替换淘宝镜像:

npm config set registry ``https://registry.npmmirror.com

  1. 验证是否生效:

npm config get registry

输出应为:

https://registry.npmmirror.com/

cnpm:加速下载第三方包(可选)

cnpm 是 npm 的中国镜像,帮助解决在中国大陆由于网络问题导致的 npm 包下载缓慢的问题。使用 cnpm 可以大幅提高安装速度。安装和使用 cnpm 的步骤如下:

1. 安装 cnpm

可以通过 npm 来安装 cnpm。打开终端或命令提示符,然后运行以下命令:

npm install -g cnpm --registry=https:<em>//registry.npmmirror.com</em>

这条命令会全局安装 cnpm,并设置包的下载源为 https://registry.npmmirror.com,这是一个 npm 的中国镜像源。

2. 使用 cnpm

安装完 cnpm 后,你可以像使用 npm 一样使用 cnpm。

# 例如,安装一个包, 这里的 [package_name] 是你想要安装的 npm 包名。
cnpm install [package_name]
# 安装依赖,等同于 npm install
cnpm i

原生开发和使用框架。初学来说,先熟悉原生开发。

核心介绍

插件结构示例

my-extension/
├── manifest.json            ← 插件配置文件(必须)
├── background.js            ← 后台脚本
├── content.js               ← 注入到网页的脚本
├── popup.html               ← 弹出页面
├── popup.js                 ← 弹出页面逻辑
├── popup.css                ← 弹出页面样式
├── icons/                   ← 插件图标
└── options.html / .js / .css← 设置页(可选)

manifest.json

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

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

{
        // 清单文件的版本,这个必须写,而且必须是3
        "manifest_version": 3,
        // 插件的名称
        "name": "demo",
        // 插件的版本
        "version": "1.0.0",
        // 插件描述
        "description": "简单的Chrome扩展demo",
        // 图标,一般偷懒全部用一个尺寸的也没问题
        "icons":
        {
                "16": "img/icon.png",
                "48": "img/icon.png",
                "128": "img/icon.png"
        },
        // 会一直常驻的后台服务脚本,V3 中只能使用 service_worker
        "background":
        {
                // V3中不再支持"page",只能使用service_worker
                "service_worker": "js/background.js"
        },
        // 浏览器右上角图标设置,browser_action、page_action、app统一使用action字段
        "action": 
        {
                "default_icon": "img/icon.png",
                // 图标悬停时的标题,可选
                "default_title": "这是一个示例Chrome插件",
                "default_popup": "popup.html"
        },
        // 需要直接注入页面的JS
        "content_scripts": 
        [
                {
                        // "<all_urls>" 表示匹配所有地址
                        "matches": ["<all_urls>"],
                        // 多个JS按顺序注入
                        "js": ["js/jquery-1.8.3.js", "js/content-script.js"],
                        // JS的注入可以随便一点,但是CSS的注意就要千万小心了,因为一不小心就可能影响全局样式
                        "css": ["css/custom.css"],
                        // 代码注入的时间
                        "run_at": "document_start"
                },
                {
                        "matches": ["*://*/*.png", "*://*/*.jpg", "*://*/*.gif", "*://*/*.bmp"],
                        "js": ["js/show-image-content-size.js"]
                }
        ],
        // 权限申请
        "permissions":
        [
                "contextMenus",
                "tabs",
                "notifications",
                "storage",
                "scripting"
        ],
        // host权限必须单独列出
        "host_permissions": [
                "http://*/*",
                "https://*/*"
        ],
        // 插件中可以被网页访问的资源列表
        "web_accessible_resources": [
                {
                        "resources": ["js/inject.js"],
                        "matches": ["<all_urls>"]
                }
        ],
        // 插件主页
        "homepage_url": "https://www.baidu.com",
        // 覆盖浏览器默认页面,V3 中仍然支持
        "chrome_url_overrides":
        {
                "newtab": "newtab.html"
        },
        // 插件设置页
        "options_page": "options.html",
        // V3仍支持新版UI设置页
        "options_ui":
        {
                "page": "options.html",
                "open_in_tab": true
        },
        // 地址栏关键字
        "omnibox": { "keyword" : "go" },
        // 默认语言
        "default_locale": "zh_CN",
        // 开发者工具面板页面
        "devtools_page": "devtools.html"
}

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 注入功能,剩下的就是发挥你的想象力去玩弄浏览器了。

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,但是真正在后台常驻的只有一个,而且这个你永远看不到它的界面,只能调试它的代码。

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

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 对象。

event-pages

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

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

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

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

injected-script

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

这是因为 content-script 有一个很大的“缺陷”,也就是无法访问页面中的 JS,虽然它可以操作 DOM,但是 DOM 却不能调用它,也就是无法在 DOM 中通过绑定事件的方式调用 content-script 中的代码(包括直接写 onclickaddEventListener 2 种方式都不行),但是,“在页面上添加一个按钮并调用插件的扩展 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 中的代码,后面我会在专门的一个消息通信章节详细介绍。

homepage_url

开发者或者插件主页设置