chrome extension 开发入门指南

2,208 阅读9分钟

说明:本文主要引用字节内网公开文章,结合自身理解转化而来。目标是从小白 到 chrome extension 开发者。实际用例可参考 Chrome Extensions samples

一、什么是Chrome extenstion

Chrome extension 是 依靠Chrome 浏览器对外提供的接口实现功能增强的扩展(插件是非约定的通俗说法?本文统一为扩展)。实际是由HTML、CSS、JS、图片等资源构成的.crx压缩包。

插件管理

在 Chrome 浏览器中可以通过点击右上角菜单 -> 更多工具 -> 扩展程序进入扩展管理页面,也可以直接在地址栏输入 chrome://extensions 访问

  • 通过直接拖动可安装 .ctx 格式的压缩包
  • 勾选 开发者模式 即可以文件夹的形式直接加载插件。

相关名词解释

具体图示可看下文体系结构

  • Chrome App : Chrome应用。Chrome已经不再提供支持。
  • Browser Action: 展示在浏览器地址栏右侧,除图标外,还可以有弹出页面、徽标等。
  • Chrome runtime - 在一个已经加载了的扩展中,能够监听到输入组件触发的事件或者发生在扩展上的其他事情的脚本代表着扩展运行时。这些脚本包括popup scripts以及events scripts(也称background scripts)。但是,由于popup scripts只有在popup被打开的时候才会执行,所以你将更依赖于扩展中的event script(该脚本将在后台长期运行)去监听每个事件(从任何输入组件或组件自身触发的,包括当组件被安装、卸载、更新等等的时候)。
  • ContextMenu - 上下文菜单。在浏览器网页上右键展示出的菜单。
  • Background Script(Event Script) - Chrome插件的运行时脚本
  • Content Script - 内容脚本,注入到业务页面的脚本,在隔离环境运行,可以获取、操作DOM。content scripts 获取Chrome扩展API的能力非常有限,因为他们不代表扩展运行时(这也说明了,并不是代不代表扩展运行时与能不能获取扩展API没有必然关系)。他们只能获取这些API:chrome.runtime,chrome.extension和chrome.storage。但是content script也是可以获取所有标准的JavaScript API的。另外,他们可以与扩展运行时进行交互(通过messaging API)。
  • Popup Script - Chrome插件的UI脚本。
  • Webpage Script - 业务脚本

二、体系结构

image.png

扩展按文件功能可划分为 manifest, background, popup, content(script)

结合具体页面:

image.png

本文以 manifest.json 配置文件的结构进行对扩展的“部件”进行说明

(一)配置文件

manifest.json 是每个 Chrome 扩展都必须有的配置文件,且需放在根目录。其中 manifest_version、name、version 3个是必不可少的属性。完整配置请看 这里

// 示例如下
{
    "manifest_version": 3,
    "name": "demo-helloWrold",
    "description": "demo-helloWrold",
    "version": "1.0.1",
    "browser_action": {
        "default_icon": "icon.png",
        "default_title": "hello world!",
        "default_popup": "popup.html"
    }
}

(二)核心部分

1. content-script

content-script 是 Chrome 扩展想页面注入脚本的一种形式(虽名为content-script,但还是可以包括css的,即影响Chrome浏览器的content css 层)。content-script 和 原始页面共享 DOM,但不共享JS,即如果要访问页面JS(如某个变量),只能通过 injected JS 的方式实现

// manifest.json配置
"content_scripts": [
    {
        "matches": ["<all_urls>"],
        "js": ["content_script.js"],
        // 代码注入的时间,可选值: "document_start", "document_end", or "document_idle",最后一个表示页面空闲时,默认document_idle
        "run_at": "document_start"
    }
]

// content_script.js
var body = document.body
body.style.backgroundColor = 'red';

2. Injected-script

为了解决 content-script 中无法访问页面中的JS的问题(如在页面上添加一个按钮并调用扩展的API),可以通过 DOM 方式向页面注入script实现,即 Injected-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);
}

为了能访问以上的 JS 资源文件(js/inject.js),需要在配置里添加访问

{
    // 普通页面能够直接访问的插件资源列表,如果不设置是无法直接访问的
    "web_accessible_resources": ["js/inject.js"],
}

3. background

background 是随着浏览器打开而打开,关闭而关闭的常驻页面,所以通常把一直需要运行的、启动就运行的或全局的代码放在 background 里面。

在配置中background可以通过page指定一张网页,也可以通过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页

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

}

4. Event-pages

鉴于background生命周期太长,长时间挂载后台可能会影响性能,所以Google 增加了 event-pages。在被需要时加载,在空闲时被关闭,什么叫被需要时呢?比如第一次安装、插件更新、有content-script向它发送消息等等。
在配置文件上,它与background的唯一区别就是多了一个persistent参数:

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

5. popup

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

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

扩展展示形式

1. browerAction

通过配置browser_action可以在浏览器的右上角增加一个图标,一个browser_action可以拥有一个图标,一个tooltip,一个badge和一个popup image.png

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

2. pageAction

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

// 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()]
            }
        ]);
    });
});

3. 右键菜单

右键菜单主要是通过chrome.contextMenusAPI实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等。

// manifest.json
{"permissions": ["contextMenus""tabs"]}

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

4. omnibox

omnibox是向用户提供搜索建议的一种方式。注册某个关键字以触发插件自己的搜索建议界面,然后可以任意发挥了。

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

5. 桌面通知

Chrome提供了一个chrome.notificationsAPI以便插件推送桌面通知, 在后台JS中,不需要申请权限,直接使用即可

image.png

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

三、消息通信

(一)概述

image.png 不同部分的JS相互之间通信,可以概括为以下表格

标题injected-scriptcontent-scriptpopup-jsbackground-js
injected-script-window.postMessage--
content-scriptwindow.postMessage-chrome.runtime.sendMessage chrome.runtime.connectchrome.runtime.sendMessage chrome.runtime.connect
popup-js-chrome.tabs.sendMessage chrome.tabs.connect-chrome.extension. getBackgroundPage()
background-js-chrome.tabs.sendMessage chrome.tabs.connectchrome.extension.getViews-

(二)详细说明

Chrome extension中有2种通信方式,一个是短连接(chrome.tabs.sendMessagechrome.runtime.sendMessage),一个是长连接(chrome.tabs.connectchrome.runtime.connect

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

background访问popup如下(前提是popup已经打开)

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

2. popup和background向content发送消息

// background.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){
    if(request.cmd == 'test') alert(request.value);
    sendResponse('我收到了你的消息!');
});

3. content-script发送消息给background

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

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

4. content-script和injected-script

content-script和页面内的脚本(injected-script自然也属于页面内的脚本)之间唯一共享的东西就是页面的DOM元素,有2种方法可以实现二者通讯

  • 可以通过window.postMessagewindow.addEventListener来实现二者消息通讯
// injected-script
window.postMessage({"test": '你好!'}, '*');

// content-script
window.addEventListener("message", function(e){
        console.log(e.data);
}, false);
  • 通过自定义DOM事件来实现
// 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
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);
});

5. 动态注入或执行JS

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

// manifest.json
{
       "permissions": [
                "tabs", "http://*/*", "https://*/*"
        ],
}

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

6. 动态注入css

// manifest.json
{
       "permissions": [
                "tabs", "http://*/*", "https://*/*"
        ],
}

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

7. 本地存储

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

四、其他常用api

  • chrome.runtime:使用 chrome.runtime API 获取后台网页、返回清单文件详情、监听并响应扩展程序生命周期内的事件,还可以使用该 API 将相对路径的 URL 转换为完全限定的 URL。

  • chrome.webRequest: 使用 webRequestAPI 监控与分析流量,还可以实时地拦截、阻止或修改请求

chrome.webRequest.onBeforeSendHeaders.addListener(
    function(details){
        // console.log("details:", details)
        details.requestHeaders.push({name: 'test-header', value: "test-demo"})
        return {requestHeaders: details.requestHeaders};
    },
    {urls: ["<all_urls>"]},
    ["blocking", "requestHeaders"]
);
  • chrome.storage:是针对插件全局的,即使在background中保存的数据,在content-script也能获取到。(modHeader插件storage了参数和webRequest增加请求头)

  • chrome.tabs:使用chrome.tabs API 与浏览器标签页交互。可以使用该 API 创建、修改或重新排列浏览器中的标签页

    • chrome.tabs.sendMessage(integer tabId, any message, function responseCallback): 向指定标签页中的内容脚本发送一个消息,当发回响应时执行一个可选的回调函数。当前扩展程序在指定标签页中的每一个内容脚本都会收到runtime.onMessage事件。
  • chrome.windows :使用chrome.windowsAPI 与浏览器窗口交互。您可以使用该模块创建、修改和重新排列浏览器中的窗口。

  • chrome.declarativeContent:根据网页内容进行某些操作。根据网页的 URL 和它的内容匹配的 CSS 选择器来显示扩展程序的pageAction

五、参考文档

  1. Chrome Developers
  2. chrome插件开发(内网)
  3. Creating-Google-Chrome-Extensions
  4. Web Extensions
  5. Chrome Extensions samples
  6. 其他教学文章