一篇文章教你顺利入门和开发chrome扩展程序(插件)

前言

关于chrome extension的开发经验总结或说明文档等资料不少,不少人在写,然而,我也是一员。可是,也许这篇文章,可能给你一些不同的感觉。
这里介绍的是80%你要开发扩展会碰到的问题html

前面部分大多数是一些基础介绍,和别人的资料大同小异,可是用的是通俗的语言或者我本身理解来描述的,不是拷贝官方的描述,否则的话,你干脆看官方文档就好啦,干吗还来我这里折腾对吧,也许这些通俗的描述,更方便你理解(固然不排除也会有官方的话语)
后面部分多为一些我在项目中总结的方法,这部分就是在别人的资料可能看不到的地方了,固然,这些方法也许不通用,由于毕竟是基于我项目里的,可是尽可能总结一套方法出来。前端

废话很少说,我们开始吧...vue


目录

WHAT

谷歌扩展(chrome extension),在认识以前,首先要明确一个观念,这种扩展程序,实际上不是一个exe、app之类的程序,下载了本地打开运行安装,本质上,它就是一个网页,写的用的都是前端的语言,高档点说是一个程序,通俗来说, 就是运行在浏览器上的一个网站,网页。jquery

我这种说法也许不对,不许确,不专业。可是起码,能把小白开发扩展的心态,调整好点,其实是一个不难的东西,就是在写页面而已。要知道,心态很差,后面就坚持不下去了。web

最基本组成

这里讲的是开发一个扩展(插件)最经常使用最基本的所需的东西,并不像官方说的那种分类。ajax

  • manifest.json
  • background script
  • content script
  • popup

严格上来说主要是background script 、 content script 和 popup,毕竟他们都是贯穿在manifest里的,把manifest写出来,只是为了凸显一下它的重要性chrome

(一)manifest.json

一个插件,必须都含有这个一个文件——manifest.json,位于根目录。顾名思义,这是一个扩展的组成清单,在这个清单里能大约看到该插件的一个“规则”。json

罗列和简单介绍一下一些经常使用的配置项,说以前,先看一个大体的文件,首先感官感觉一下先api

{
	// 必须
	"manifest_version": 2,
	"name": "插件名称a",
	"version": "1.1.2",

	// 推荐
	"default_locale": "en",
	"description": "插件的描述",
	"icons": {
		"16": "img/icon.png",	// 扩展程序页面上的图标
		"32": "img/icon.png",	// Windows计算机一般须要此大小。提供此选项可防止尺寸失真缩小48x48选项。
		"48": "img/icon.png",	// 显示在扩展程序管理页面上
		"128": "img/icon.png"	// 在安装和Chrome Webstore中显示
	},

	// 可选
	"background": {
		"page": "background/background.html",
		"scripts": ["background.js"],
		// 推荐
		"persistent": false
	},
	"browser_action": {
		"default_icon": "img/icon.png",	
		// 特定于工具栏的图标,至少建议使用16x16和32x32尺寸,应为方形,
		// 否则会变形
		"default_title": "悬浮在工具栏插件图标上时的tooltip内容",
		"default_popup": "hello.html"	// 不容许内联JavaScript。
	},
	"content_scripts": [ {
		"js": [ "inject.js" ],
		"matches": [ "http://*/*", "https://*/*" ],
		"run_at": "document_start"
	 } ],
	"permissions": [
		"contextMenus",
		"tabs",
		"http://*/*",
		"https://*/*"
	],
	"web_accessible_resources": [ "dist/*", "dist/**/*" ]
}
复制代码

上面有我写的一些注释,用于帮助你们更好的去理解。
那接下来开始说一下其中的配置项跨域

icons

extension程序的图标,能够有一个或多个。
48x48的图标用在extensions的管理界面(chrome://extensions);
128x128 的图标用在安装extension程序的时候;
16x16 的图标看成 extension 的页面图标,也能够显示在信息栏上。
图标通常为PNG格式, 由于最好的透明度的支持,不过WebKit支持任何格式,包括BMP,GIF,ICO等
注意: 以上写的图标不是固定的。随浏览器的环境的改变而变。如:安装时弹出的对话框变小。

browser_action与page_action

这两个配置项都是用来处理扩展在浏览器工具栏上的表现行为。 前者扩展能够适用于任何页面。后者扩展只能做用于某一页面,当打开该页面时触发该Google Chrome扩展,关闭页面则Google Chrome扩展也随之消失。

通俗的举个例子,一些扩展任何页面可用,就都会显示在工具栏上为可用状态,一些扩展只适用于某些页面,如你们很熟悉的vue tools调试器,在检测到页面用的是vue时,就会在工具栏显示出来并可用(非灰色)

default_popup

在用户点击扩展程序图标时,均可以设置弹出一个popup页面。而这个页面中天然是能够有运行的js脚本的(好比就叫popup.js)。它会在每次点击插件图标——popup页面弹出时,从新载入。

这个小小的设置,也就是上面我把它分为在基本组成里的popup

permissions

在background里使用一些chrome api,须要受权才能使用,例如要使用chrome.tabs.xxx的api,就要在permissions引入“tabs”

web_accessible_resources

容许扩展外的页面访问的扩展内指定的资源。通俗来说就是,扩展是一个文件夹A的,别人的网站是一个文件夹B,B要看A的东西,须要得到权限,而写在这个属性下的文件,就是授予了别人访问的权限。

(二)background script

background能够理解为插件运行在浏览器中的一个后台网站/脚本,注意它是与当前浏览页面无关的。
实际上这部份内容的配置状况也会写在manifest里,对应的是background配置项。单独拿出来说,是彰显它的份量很重,也是一个插件经常使用的配置。从其中几个配置项项去了解一下什么是background script

page

能够理解为这个后台网站的主页,在这个主页中,有引用的脚本,其中通常都会有一个专门来管理插件各类交互以及监听浏览器行为的脚本,通常都起名为background.js。这个主页,不必定要求有。

scripts

这里的脚本其实跟写在page里html引入的脚本目的同样,我的的理解是,page的html在没有的状况下,那么脚本就须要经过这个属性引入了;
若是在存在page的状况下,通常在这里引入的脚本是专门为插件服务的脚本,而那些第三方脚本如jquery仍是在page里引用比较好,或许这是一个众人的“潜规则”吧

persistent

所谓的后台脚本,在chrome扩展中又分为两类,分别运行于后台页面(background page)和事件页面(event page)中。二者区别在于,

前者(后台页面)持续运行,生存周期和浏览器相同,即从打开浏览器到关闭浏览器期间,后台脚本一直在运行,一直占据着内存等系统资源,persistent设为true;

后者(事件页面)只在须要活动时活动,在彻底不活动的状态持续几秒后,chrome将会终止其运行,从而释放其占据的系统资源,而在再次有事件须要后台脚原本处理时,从新载入它,persistent设为false。

保持后台脚本持久活动的惟一场合是扩展使用chrome.webRequest API来阻止或修改网络请求。webRequest API与非持久性后台页面不兼容。

(三) content script

这部分脚本,简单来讲是插入到网页中的脚本。它具备独立而富有包容性。

所谓独立,指它的工做空间,命名空间,域等是独立的,不会说跟插入到的页面的某些函数和变量发生冲突;

所谓包容性,指插件把本身的一些脚本(content script)插入到符合条件的页面里,做为页面的脚本,所以与插入的页面共享dom的,即用dom操做是针对插入的网页的,在这些脚本里使用的window对象跟插入页面的window是同样的。主要用在消息传递上(使用postMessage和onmessage)

实际上这部份内容的配置状况也会写在manifest里,对应的是content_scripts配置项。单独拿出来说,是彰显它的份量很重,也是一个插件经常使用的配置。从其中几个配置项项去了解一下什么是content script

js

要插入到页面里的脚本。例子很常见,例如在一个别人的网页上,你要打开你作的扩展,对别人的网页作一些处理或者获取一些数据等,那怎么跟别人的页面创建起联系呢?就是经过把js里的这些脚本嵌入都别人的网页里。

matches

必需。匹配规则组成的数组,用来匹配页面url的,符合条件的页面将会插入js的脚本。固然,有能够匹配的天然会有不匹配的——exclude_matches。匹配规则:

developer.chrome.com/extensions/…

上面的官方描述已经很清晰啦,我就很少说了。

run_at

js配置项里的脚本什么时候插入到页面里呢,这个配置项来控制插入时机。有三个选择项:

  • document_start
  • document_end
  • document_idle(默认)
document_start

style样式加载好,dom渲染完成和脚本执行前

document_end

dom渲染完成后,即DOMContentLoaded后立刻执行

document_idle

在DOMContentLoaded 和 window load之间,具体是什么时刻,要视页面的复杂程度和加载时间,并针对页面加载速度进行了优化。

popup

其实这部分,早就讲过了,就是在manifest里的browser_actionpage_action配置项里设置的


基础的通讯机制

上面讲述了基本的组成部分,那么这几部分,他们要进行交流合做,把他们组织起来,才能成就一个漂亮的扩展。那么这种交流,分为如下几种说明:

  • content script与background的通讯
  • popup与background的通讯
  • popup与content script的通讯
  • 插件iframe网站与插入网页的通讯

最后一点,是额外说的,可是倒是很重要的。毕竟不少扩展,也是以iframe的形式呈现的。

(一)content script与background的通讯

content-script向background发送消息

在content-script端

使用

chrome.runtime.sendMessege(
    message,
    function(response) {…}
)
复制代码

就能向background发送消息了,第一个参数message为发送的消息(基础数据类型),回调函数里的第一个参数为background接收消息后返回的消息(若有)

在background端

使用

chrome.runtime.onMessege.addListener(
    function(request, sender, sendResponse) {…}
)
复制代码

进行监听发来的消息,request表示发来的消息,sendResponse是一个函数,用于对发来的消息进行回应,如 sendResponse('我已收到你的消息:'+JSON.stringify(request));

这里须要注意的是,默认状况下sendResponse函数的执行是同步的,若是在这个监听消息的处理函数的同步执行流程里没有发现sendResponse,则默认返回undefined,假设咱们是要通过一个异步处理以后才调用sendResponse,已经为时已晚了。所以,咱们可能须要异步执行sendResponse,这时咱们在这个监听函数里的添加return true就能实现了。

还有,因为background监听全部页面上的content script上发来的消息,若是多个页面同时发送同种消息,background的onMessage只会处理最早收到的那个,其余的不了了之了。

background向content-script发送消息

咱们发现,一个插件里只有一个background环境,而content-script有多个(一个页面一个),那么background怎么向特定的content-script发送消息?

在background端

首先咱们须要知道要向哪一个content scripts发送消息,通常一个页面一份content scripts,而一个页面对应一个浏览器tab,每一个tab都有本身的tabId,所以首先要获取要发送消息的tab对应的tabId。

/** * 获取当前选项卡id * @param callback - 获取到id后要执行的回调函数 */
function getCurrentTabId(callback) {
    chrome.tabs.query({active: true, currentWindow: true}, function (tabs) {
        if (callback) {
            callback(tabs.length ? tabs[0].id: null);
        }
    });
}
复制代码

当知道了tabId后,就使用该api进行发送消息

chrome.tabs.sendMessage(tabId, message, function(response) {...});
复制代码

其中message为发送的消息,回调函数的response为content scripts接收到消息后的回传消息

在content scripts端

一样是使用

chrome.runtime.onMessege.addListener(function(request, sender, sendResponse) {…})
复制代码

进行来自background发来消息的监听并回传

(二)popup与background的通讯

通常地,popup与background的交流,常见于popup要获取background里的某些“东西”,固然咱们可使用上述的chrome.runtime.sendMessagechrome.runtime.onMessage的方式进行popup向background的交流,可是其实有更方便快捷的方式:

var bg = chrome.extension.getBackgroundPage();
bg.someMethod();    //someMethod()是background中的一个方法
复制代码

(三)popup与content script的通讯

这里的通讯,实际上跟background与content script的方式是同样的

(四)插件iframe网站与插入网页的通讯

其实这两个的通讯,算不上是chrome extension开发里的知识,它就是一个基础的js知识——ifame与父窗体的通讯。

同域的状况下,能够经过DOM操做达到通讯的目的,如获取dom元素,获取值赋值之类的。
在父窗体里,用window.contentWindow获取到iframe的window对象
在iframe里,用window.parent获取到父窗体的window对象

而在跨域下,上述的方法是行不通的,网上也有各类方法解决,可是在插件这块里,最方便的就是使用js的message机制了。
我这里说的message机制,就是使用window对象的postMessage()onmessage

通常插件展示都是在别人的网站上,所以没办法直接在别人的网站上添加postMessageonmessage的代码。这时候,重任就落在了插件的content script身上了(以前说了他们共用DOM)。因为content script是本身编写的,因此能够“随心所欲”了

iframe向父窗体发送消息

在iframe端

假设iframe类名为extension-iframe,这里设置类名而不是id名的初衷是,咱们不能保证设置的名称本来的网站会不会已经存在,设置类名能共存。发送消息使用
window.parent.postMessage(message, '*');
其中message为发送的消息

在父窗体端

因为一个页面,可能有来自页面自己的postMessage来的消息,也有可能来自该页面其余chrome extension发送来的消息,所以用onmessage来监听,要作好区分来源,这里使用如下方法

window.addEventListener('message', function (event, a, b) {
    // 若是没消息就退出
    if (!event.data) {
        return;
    }
    var iframes = document.getElementsByClassName('extension-iframe');
    var extensionIframe = null; // 存插件iframe节点对象
    var correctSource = false;  // 是否来源正确
    // 找出真正的插件生成的iframe
    for (var i = 0; i < iframes.length; i++) {
        if (iframes[i].contentWindow && (event.source === iframes[i].contentWindow)) {
            correctSource = true;
            extensionIframe = iframes[i];
            break;
        }
    }
    // 若是来源不是来自插件的,就退出
    if (!correctSource) {
        return;
    }
}, false);
复制代码

这里也不能百分百区分好是否是来自本身extension的消息,或许真的那么倒霉恰好有一个跟本身extension同类名的iframe也发了一个消息过来。所以还能够加多一层保障,在iframe发送消息的内容上作手脚,例如加个from,而后在这边判断一下等。固然,这样也不能百分百肯定,只能说保障更上一层楼了。
若是你们有好的点子,请务必告诉鄙人!受教受教!

父窗体向iframe发送消息

在父窗体端

使用 extensionIframe.contentWindow.postMessage(message, '*');
其中extensionIframe为插件的iframe节点对象,message为发送的消息,例如

{from: 'content-script', other: xxx}
复制代码

在iframe端

使用

window.addEventListener('message', function (event, a, b) {
    let result = event.data;
    if (result && (result.from === 'content-script') && (event.source === window.parent)) {...}
});
复制代码

在这里,在发送消息里增长了个from属性,进而进一步判断是否是来自父窗体本身插件的content script

插件内容发送ajax请求,个人一套“土办法”

咱们知道,在进行ajax请求,是有可能遇到跨域的。例如个人项目就是在任何一个页面插入iframe网站,而后有些操做就须要发请求了,这样必然存在跨域问题。
然而,若是开发插件还要开发者想办法解决跨域问题,那chrome extension就太逊了,并且,跨不跨域,还不是浏览器本身的主意,是浏览器自己的安全策略。

因此,chrome extension为了保证本身的优越性,容许在本身的程序里面,实现跨域请求,那彻底的chrome extension程序,无非就是在background里了。

所以,插件要实现一些ajax请求,都得统统搬到background里实现。这个事情,自己不是什么重大发现。接下来要说的是,我利用这个特性,按照某个规则,实现一套方便的请求流程。

这里以一个ifame网站嵌入到别人页面的这类形式的chrome extension为例子。

在插件生成的iframe网站里

首先在这个插件网站中,有一些按钮操做自己是要触发某些ajax请求的,可是因为上述缘由,不能直接在插件网站里发请求,而是先向父窗口发送消息,利用postMessage。例如

window.parent.postMessage({
    from: 'extension-iframe',
    type: 'loadTable',
    data: {
        pageIndex: 1,
        pageSize: 10,
        sortProp: '',
        sortOrder: 0
    }
}, '*');
复制代码

by the way,这里用window.parent.postMessage是为了解决iframe跨域通讯问题,固然若是是确保同域的状况下,其实能够直接用DOM操做告诉父窗口一些消息。

言归正传,在postMessage第一个参数对象里

属性名 描述
from 标记这条消息来自哪里
type 操做的名称,如发送该message的操做目的是为了加载表格
data 发送请求的data

在插件的content script里

监听发来的消息,这里①标注的代码为前面说过的区分来源,这里重点放在②部分的代码

window.addEventListener('message', function (event, a, b) {
    var responseData = event.data;
    if (!event.data) {
        return;
    }
    // 来自插件内嵌网站的消息
    if (responseData.from === 'extension-iframe') {
        // ① 判断是否本身插件的iframe
        var iframes = document.getElementsByClassName('extension-iframe');
        var extensionIframe = null;
        var correctSource = false;
        for (var i = 0; i < iframes.length; i++) {
            if (iframes[i].contentWindow && (event.source === iframes[i].contentWindow)) {
                correctSource = true;
                extensionIframe = iframes[i];
                break;
            }
        }
        if (!correctSource) {
            return;
        }
        // ② 加载表格、提交信息等请求操做
        // 该数组为iframe传来各个操做的名称,对应发来的消息的type属性
        var operators = ['loadTable', 'submit', 'getNonMarkedCount', 'getUrl'];
        // 若是跟操做匹配上了,就转发给background
        if (operators.indexOf(responseData.type) !== -1) {
            chrome.runtime.sendMessage({
                type: responseData.type,
                data: responseData.data
            },function (response) {
                // 返回请求后的数据给iframe网站
                extensionIframe.contentWindow.postMessage({
                    from: 'extension-content-script',
                    type:  responseData.type,
                    response: response
                }, '*');
            });
        }
    }
}, false);
复制代码

在插件的background script里

监听刚转发过来的消息

// 这是全部请求组成对象
var httpService = {
    loadTable: function (config) {
        return eodHttp.get('/brandimageservice/perspective/mark', config);
    },
    submit: function (config) {
        return eodHttp.post('/brandimageservice/perspective/mark', config);
    },
    ...
};

// 监听刚转发过来的消息
chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    // 该数组为iframe传来各个操做的名称,对应发来的消息的type属性
    var operators = ['loadTable', 'submit', 'getNonMarkedCount', 'getUrl'];
    if (operators.indexOf(request.type) !== -1) {
        // 这里的type恰好与请求的属性名一致
        httpService[request.type](request.data).then(res => {
            // 把请求的结果回传给content script
            sendResponse(res);
        }).catch(e => {
            // 这里作了请求拦截,若是不是canceled的请求报错,则把报错信息也回传给content script
            (e.status !== -1) && (sendResponse(e.data));
        });
    }
    // 此处return true是为了把sendResponse做为异步处理。
    return true;
});
复制代码

再返回到插件生成的iframe网站里

此次绕回到这个ifame里,最终的请求的数据仍是会流回这里。

window.addEventListener('message', function (event, a, b) {
    let result = event.data;
    if (result && (result.from === 'extension-content-script') && (event.source === window.parent)) {
        // 如下为请求返回内容
        let res = result.response;
        // 加载表格数据
        if (result.type === 'loadTable') {...}
    }
});
复制代码

这样,最终在iframe里获取到的请求数据仍是跟以前咱们日常开发调接口的状况是同样的。

总结

整个流程是 iframe -> content script -> background -> content script -> iframe
以background为中分线,前半截为发送请求,后半截为获取请求数据。这里巧妙的用法就是“type”这个字段由始至终都一直存在,都表明同样的意思。这样的写法的好处是,全部请求操做均可以共用这么一个流程,改一下type区分一下操做便可。

检测chrome extension是否已经安装

有一些chrome extension,可能不仅仅是经过点击浏览器工具栏上的插件图标来激活插件,也有一些需求是经过点击网站上某个按钮来激活插件(如自家的系统),那么这时候第一步须要的是,检测浏览器是否安装了要求的chrome extension,若是没有,进行提示等等。

在国内资料中进行搜索,每每会看到不少条教用navigator对象来查找安装的插件,多是我太弱鸡了,我发现并不能用来检测到本身添加的chrome extension。因而我只能另寻他法了,若是有大神知道如何用navigator对象来判断,麻烦指导一下。

思路

若是安装了某个插件,那么该插件的content script就会插入到页面上(没有content script的除外,可是通常没有content script的插件每每也没有以上这样的需求),所以判断是否安装了该插件,就变为判断content script是否插入到页面上。

方法一

在content script里写这么一个逻辑:往插入页面生成一个html元素标记,如<div class="extension-flag"><div>。而后在插入页面获取这个元素,若是获取到了,就证实content script存在了(不存在也就没有这个元素了),就证实已经安装了。

缺点: 建立的这个元素,必定要够“特别”,越能确保其独一无二越能证实是来自插件的。什么意思?假设恰好页面也有一个类名跟建立的同样的,那就要作进一步区分这究竟是不是来自该插件的了。

方法二

经过message机制,在合适的时机里,页面用postMessage发送消息给window,content script监听window消息,判断若是是要求检查是否安装的消息,则再用postMessage告知,只要收到这个消息,就证实已经安装了。

缺点:因为发送消息和接受消息再到发送消息,这个过程是异步的。因此要处理好什么时候发送检测的时机问题。

安装的一些注意事项

安装手段通常是有两种的,一种在谷歌商店上进行在线安装,一种是下载安装包离线安装。在线安装没什么好说的,那么说一下离线安装。

离线安装的关键是,你提供的下载包是什么?

开发者在开发扩展的时候,每每是直接安装在本地上的扩展所在文件夹。在chrome://extensions上开启开发者模式,点击“加载已解压的扩展程序”。这时候会发现第一次打开浏览器的时候会总是提示你这个扩展不安全之类的。固然咱们提供给用户下载的安装包确定不能是这个了。

一开始我傻不拉几的直接压缩本身的开发的扩展所在文件夹,而后发到服务器上给用户下载,结果呢,用户下载了,而后把压缩包拖动到chrome://extension里,发现chrome不容许安装,说什么基于安全什么的。也就是我这个扩展可能不安全不给我装。

后来才知道,不该该提供这种压缩包,而是在本身发布扩展的开发者信息中内心,把已发布的扩展下载下来提供给用户用才能够。

也许...只有我那么傻吧

最后

最后的最后,我说一下小细节的注意项吧,稍微不留神,可能就这样傻傻地写下了bug了...

扩展之间很容易相互影响

怎么理解?在通讯部分我讲过,在传递消息的时候,在消息里,我有用type字段来标明传递的内容类型。在开发完扩展的时候,发现有些同事的电脑能够正常使用有些却不行,后来调试代码发现,在postMessage函数里的type参数给别的扩展改造过了,受到了影响。

为何会这样呢?缘由是扩展都是经过嵌入本身的脚本到别人的网页里,所以在一个网页里的代码,特别是传递消息机制里,更容易受到牵连。

这个问题说明了什么?

  • 要安装权威可信的扩展
  • 开发一个值得让人信赖的扩展,开发一个尽可能考虑全面的扩展,不要给用户添麻烦

代码调试

对于content script的调试,日常咱们打开F12选择到source选项的时候,通常都会显示在"page"下,其实能够看到,还有个content script的选择,里边的就是各个扩展的内容脚本了。

对于backgroud script的调试,就在去到chrome://extensions页下,找到对应的扩展,而后点击背景视图,就能够看到backgroud script进行调试了,并且,还能在控制台调用chrome api呢。以及,请求也能够在这里看到。

信息传递的异步性

在扩展中,会用到不少消息传递,如上述的postMessage和chrome.runtime.sendMessage等之类的,你们必定要有一个观念,他们的交流并非同步,不是说我发了一个消息过去,就立刻收到而后作接下来的处理。

因此咱们写逻辑的时候必定要注意,这种异步性,会对你的逻辑处理产生什么效果。特别是也要考虑到content script的插入时机是否对这些通讯产生必定影响,如content script都没有准备好,就发了一些消息,而后就没有声响了。


关于chrome extensions的基本介绍和开发思路就介绍到这里,后续会有一篇文章专门来阐述一下,我项目中遇到的需求,遇到的问题以及对应的解决方案。

感兴趣的能够关注一下,感受文章写得对你有帮助的话,请点下赞。

转载请标注出处谢谢,写文章不易。

相关文章
相关标签/搜索