前言

这几天研究前端技术走火入魔,突发奇想琢磨着自己搞一个谷歌浏览器插件,因此找了好多教程,也算是有些收获,记录一下。

所谓chrome插件,严格来说应该叫做Chrome扩展(Chrome Extension),是一个用Web技术开发、用来增强浏览器功能的软件,它其实就是一个由HTML、CSS、JS、图片等资源组成的一个.crx后缀的压缩包。

Chrome提供了很多实用API供开发者使用,包括书签控制、下载控制、窗口控制、标签控制、网络请求控制、各类事件监听、自定义原生菜单、完善的通信机制等。

另外,Chrome插件还可以配合dll动态链接库(PPAPI)实现一些更底层的功能,例如全屏幕截图。

 

开发与调试

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

勾选开发者模式即可以文件夹的形式直接加载插件,否则只能安装.crx格式的文件(下图中插件图标上的红色标志就代表是未封装的插件)。

对插件代码作出修改后,都需要点击插件上的刷新按钮修改才能生效。

 

核心配置文件

Chrome插件没有严格的项目结构要求,只要保证根目录有一个内容规范的manifest.json文件即可识别为插件。

这个文件用来配置所有和插件相关的配置,必须放在根目录。其中,manifest_version、name、version3个是必不可少的,description和icons是推荐使用的。

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

{
    // 配置文件的版本,这个必须写,而且必须是2
    "manifest_version": 2,
    // 插件的名称
    "name": "yumefxTool",
    // 插件的版本
    "version": "1.0.0",
    // 插件描述
    "description": "Some useful tools for yumefx.",
    // 图标,一般偷懒全部用一个尺寸的也没问题
    "icons":
    {
        "16": "img/icon.png",
        "48": "img/icon.png",
        "128": "img/icon.png"
    }
}

但如果只有这些配置,即使被识别为插件,也是没有任何功能的空壳子,因此下面介绍的是实现插件丰富功能的方法。

 

content-scripts

content-scripts是Chrome插件向页面注入脚本的一种形式(虽然名为script,其实还可以包括css),借助content-scripts可以实现通过配置的方式轻松向指定页面注入JS和CSS,最常见的比如:广告屏蔽、页面CSS定制,等等。

manifest.json配置示例

{
    "content_scripts": 
    [
        {
            //"matches": ["http://*/*", "https://*/*"],
            // "<all_urls>" 表示匹配所有地址
            "matches": ["https://www.baidu.com/*"],
            // 多个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"
        }
    ],
}

特别注意,有时候注入的js获取不到元素,是因为html文档自上而下加载可能未加载完(或者懒加载)就开始获取元素。

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

 

background

background是一个常驻的页面,它的生命周期是插件中所有类型页面中最长的,它随着浏览器的打开而打开,随着浏览器的关闭而关闭,所以通常把需要一直运行的、启动就运行的、全局的代码放在background里面。

background的权限非常高,几乎可以调用所有的Chrome扩展API(除了devtools),而且它可以无限制跨域,也就是可以跨域访问任何网站而无需要求对方设置CORS。

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

background可以通过page指定一张网页;也可以通过scripts直接指定一个JS,Chrome会自动为这个JS生成一个默认的网页。配置示例如下:

{
    "background":
    {
    // 2种指定方式,如果指定JS,那么会自动生成一个背景页
        "page": "background.html"
        //"scripts": ["js/background.js"]
    },
}

特别注意,虽然可以通过chrome-extension://xxx/background.html直接打开后台页,但是打开的后台页和真正一直在后台运行的那个页面是两个不同的实例。

 

popup

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

上图为清理浏览器的插件click&clean的popup页面。

popup可以包含任意的HTML内容,并且会自适应大小。可以通过default_popup字段来指定popup页面,也可以调用setPopup()方法。配置方式:

{
    "browser_action":
    {
        //推荐png格式的19px*19px的图片,也可以通过setIcon()设置
        "default_icon": "img/icon.png",
        // 图标悬停时的标题,也可以通过setTitle()设置
        "default_title": "This is a niubility extension!",
        "default_popup": "popup.html"
    }
}

特别注意,由于单击图标打开popup,焦点离开又立即关闭,所以popup页面的生命周期一般很短,需要长时间运行的代码不要写在popup里面。

在权限上,它和background非常类似,它们之间最大的不同是生命周期的不同,popup中可以直接通过chrome.extension.getBackgroundPage()获取background的window对象。

popup.html中不能内联插入JavaScript代码,只能从外部引入js文件。

另外,插件图标上可以显示一些文本,称为badge,可以用来更新一些小的状态提示信息,空间有限(英文4个或者中文2个),只能使用代码设置。

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

例如click&clean插件的图标上的数字代表当前历史记录条数。

 

injected-script

injected-script,是一种特殊的content-script用法,是通过DOM操作的方式向页面注入JS的一种方法。为什么要把这种单独拿出来讨论呢?又或者说为什么需要通过这种方式注入JS呢?

这是因为content-script有一个很大的“缺陷”,也就是无法访问页面中的JS,虽然它可以操作DOM,但是DOM却不能调用它,也就是无法在DOM中通过绑定事件的方式调用content-script中的代码(包括直接写onclick和addEventListener2种方式都不行),但是,“在页面上添加一个按钮并调用插件的扩展API”是一个很常见的需求,那该怎么办呢?

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

//写到content-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中的代码,后面在消息通信章节会详细介绍。

 

event-pages

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

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

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

一般情况下background也不会很消耗性能的,因此不常用。

 

homepage_url

开发者或者插件主页设置,一般会在如下2个地方显示:

在配置文件中如下设置:

{
    "homepage_url": "https://www.yumefx.com",
}

 

右键菜单

Chrome插件可以自定义浏览器的右键菜单,主要是通过background调用chrome.contextMenusAPI实现,右键菜单可以出现在不同的上下文,比如普通页面、选中的文字、图片、链接,等等,如果有同一个插件里面定义了多个菜单,Chrome会自动组合放到以插件名字命名的二级菜单里,如下:

右键菜单示例:

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

// background.js
chrome.contextMenus.create(
    {
        type: "normal",
        title: "yumefxTool_01", 
        contexts: ["all"],
        onclick: function(info){alert("hi bitch")},
        documentUrlPatterns: "<all_urls>"
    },
    function(){
        console.log("yeah!");
    }
)
chrome.contextMenus.create(
    {
        type: "normal",
        title: "parentmenu", 
        contexts: ["link"],
        id: "parent"
    },function(){}
)
chrome.contextMenus.create(
    {
        type: "checkbox",
        title: "childmenu", 
        contexts: ["link"],
        parentId: "parent"
    },function(){}
)

type属性可选类型有”normal”, “checkbox”, “radio”, “separator”。

contexts属性可以定义在哪些位置点击右键弹出的菜单可以显示此菜单命令,可用”all”、”page”、”frame”、”selection”、”link”、”editable”、”image”、”video”、”audio”、”browser_action”等。

documentUrlPatterns属性定义哪些网页显示此菜单。

 

添加右键百度搜索

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

效果如下:

 

常用权限

上面出现的permission属性是因为一些操作需要获取权限才能使用,常用权限如下:

"permissions":
[
    "contextMenus", // 右键菜单
    "tabs", // 标签
    "notifications", // 通知
    "webRequest", // web请求
    "webRequestBlocking",
    "storage", // 插件本地存储
    "http://*/*", // 可以通过executeScript或者insertCSS访问的网站
    "https://*/*" // 可以通过executeScript或者insertCSS访问的网站
]

 

override

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

插件可以替代如下页面:

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

注意:

  • 一个插件只能替代一个页面;
  • 不能替代隐身窗口的新标签页;
  • 网页必须设置title,否则用户可能会看到网页的URL;

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

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

 

devtools

Chrome允许插件在开发者工具(devtools)上动手脚,主要表现在:

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

自定义面板(判断当前页面是否使用了jQuery):

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

每打开一个开发者工具窗口,都会创建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.extension和chrome.runtimeAPI,同时它也可以像content-script一样使用Message交互的方式与background页面进行通信。

自定义侧边栏代码:

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

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

//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!'}); // 直接设置显示某个对象
});

 

options

options页,就是插件的设置页面,即右键插件图标菜单中的选项:

下图为SwitchOmega插件的选项页面:

配置文件示例:

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

    // Chrome40以后的插件配置页写法
    "options_ui":
    {
        "page": "options.html",
        // 添加一些默认的样式,推荐使用
        "chrome_style": true
    }
}

注意:

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

 

notifications

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

在后台JS中,无论是使用chrome.notifications还是Notification都不需要申请权限(HTML5方式需要申请权限),直接使用即可。

最简单的通知:

代码:

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

通知的样式可以很丰富:

具体可以去官方文档查看更多细节

 

五种JS类型对比

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

 

权限对比

 

调试方式对比

 

转自小茗同学,有修改。

 


做你自己,说出你的感受,

因为那些介意的人对你不重要,

而对你重要的人不会介意。

——苏斯博士