在之前的文章里介绍了很多关于开发Chrome扩展程序时会用到的各部件,对每个部件的功能和用途做了比较详细的描述。不过单单知道怎么使用各个部件还不够,前面有提到每个部件的优缺点:Browser Action和Page Action不能长期保留数据状态;Background Page和Event Page没有用户界面;Content Scripts无法触发DOM原本的事件。所以就需要让这些部件能够联系在一起使用,用其优势不足其它的劣势。
通过阅读网上关于Chrome扩展程序的文档和文章,以及自身的试验和实践,对于Chrome扩展程序中所用到的交互方式主要可以分为三类:调用接口(Interface),消息传送(Message),脚本注入(Injection)。
话说回来其实三种方式都是调用Chrome扩展开发中直接提供的API,只是API的设计结构、使用方式、作用对象以及安全性等有些许差别,所以我才臆想成三类不同方式。
而从上图的分布,还可以看出我对于Chrome扩展程序各部件的分类可以归为:“近扩展(Close to Extension)”和“近页面(Close to Page)”。也就是一类主要作用在扩展程序上,另一类主要作用在页面本身。
调用接口
这类方式比较干脆,直接调用方法或引用属性来获取想要的内容和对象。这类方式一般发生在“近扩展”的部件上。因为在Chrome扩展程序内直接调用应该是完全安全和可靠的。
所以可以直接在Background Page或Event Page的脚本代码里直接访问chrome对象存储的browserAction
和pageAction
属性。当然在使用之前必须要在配置文件里申明对应的内容来告知该扩展程序需要使用这些对象。通过这些对象提供的方法可以直接设置不同的标题、图标,设置改变弹出框的样式。不过更多的还是用来绑定onClicked
事件的回调函数,因为Browser Action和Page Action不能保留这些事件对象,所以要在Background Page或Event Page定义来保证对象不会消失:
[codesyntax lang=”javascript” lines=”normal”]
chrome.browserAction.onClicked.addListener(function(tab) { // Do something when broser action is clicked. });
[/codesyntax]
另外Browser Action中还有setBadgeText()
方法也是比较常用的方法,它可以在扩展程序的图标上显示最长4个字符的内容来提醒用户。比如Gmail扩展程序就用它来显示未读邮件数量:
[codesyntax lang=”javascript” lines=”normal”]
chrome.browserAction.setBadgeText({ text: "" + unreadCount});
[/codesyntax]
要注意的是参数是一个对象而不是字符串。
另一面,在Browser Action和Page Action中也可以直接获取Background Page或Event Page对象(一个Chrome扩展程序只会有其中一个对象)。这些方法返回的是其脚本所对应的Window对象(因为JavaScript所以内容都是存在Window对象上的),如果脚本中定义了任何的全局变量或者全局方法,在获得该Window对象后就可以直接访问、调用甚至修改了。
[codesyntax lang=”javascript” lines=”normal”]
chrome.runtime.getBackgroundPage(function(eventWindow) { // clear unread count eventWindow.unreadCount = 0; });
[/codesyntax]
消息传送
这个方法是官方文档中有详细介绍的交互方式,它主要发生在“近扩展”部件和“近页面”部件之间,通过设定消息事件的监听方法来接收消息并返回响应内容。消息的传送方式分成“单次请求(One-time requests)”和“长连接(Long-lived connections)”,两者的差别类似于“HTTP请求”和“Socket链接”。另外因为Browser Action和Page Action不能保持消息监听方法,所以实际上消息传送的方法只是在Background Page或Event Page和Content Scripts之间发生。
Chrome扩展程序内的消息
对于具体的实现方式和所使用的方法,可以参见官方文档的介绍根据需求来选择消息传送方式。这里只举个“单次请求”的简单例子:
从Background Page或Event Page发送消息给Content Scripts需要用chrome
对象的tabs
属性上的sendMessage()
方法。方法需要指定接收消息的tab的id(因为提到过Content Scripts对于每个作用的页面都有独立的对象),JSON对象的消息内容,和可选的接受响应内容的回调函数。
[codesyntax lang=”javascript” lines=”normal”]
chrome.tabs.sendMessage(tabId, {greeting: "Watch your back."}, function (response) { console.log(response) });
[/codesyntax]
对于Content Scripts发出消息,则用到chrome
对象的runtime
属性上的sendMessage()
方法。该方法只要JSON对象的消息内容和可选的接受响应内容的回调函数即可。
[codesyntax lang=”javascript” lines=”normal”]
chrome.runtime.sendMessage({greeting: "Watch your back."}, function (response) { console.log(response) });
[/codesyntax]
对于接受消息的方法,两者是一样的,都是在chrome
对象的runtime
属性的onMessage
事件上注册事件监听的回调函数。该回调函数被调用时,会有三个参数传入,分别是发送过来的消息对象,保存消息发送者信息的对象,和用来返回响应内容的方法。
[codesyntax lang=”javascript” lines=”normal”]
chrome.runtime.onMessage.addListener( function(message, sender, sendResponse) { if (message.greeting) { sendResponse({greeting: "The pleasure is mine."}) } });
[/codesyntax]
这就是“单次请求”:发送消息,接受响应(如果有响应返回),结束过程。
前面也提到接受响应的回调函数是可选的。在我的开发Chrome扩展程序时,便让两边都注册onMessage
事件的监听对象来接受消息。当需要回复消息是,则直接通过响应的sendMessage
方法来发送新的消息出去,而不用response
方法来返回消息。这样的好处是简单,只要一个sendMessage
的方法就可以了;不足在于失去了消息和回复的对应关系,需要自己去匹配,接受到Content Scripts的消息后也要自己去判定来源自哪个tab。
Chrome扩展程序之间的消息
消息传送的交互方式除了可以Chrome扩展程序的内部进行,还可以在不同的扩展程序之间,不过这些只会在不同Chrome扩展程序的Background Page或Event Page之间进行。只是在接受消息方面,需要将用作监听对象的回调方法注册在chrome
对象的runtime
属性onMessageExternal
事件上(同样的用作长连接的也有外部版本)。
在事件发送上,依然使用之前runtime
的sendMessage()
方法,只是必须要消息发送的目标扩展程序的id。这个id在之前文章已经有提到,是chrome分配给扩展程序的标示。
页面到Chrome扩展程序的消息
消息传送甚至还可以在实际页面和chrome扩展程序之间进行,只是方向只能是从实际页面发送给扩展程序。同于扩展程序间的消息,在Background Page或Event Page中通过注册事件监听对象到onMessageExternal
事件上来获得消息内容。不过也可以通过页面像扩展程序发起长连接来实现双向互通。
要想页面可以发送消息,首先需要在配置文件中定义externally_connectable
字段,类似于Content Scripts定义,它决定哪些页面可以向该扩展程序发送消息。
[codesyntax lang=”javascript” lines=”normal”]
"externally_connectable": { "matches": ["*://*.example.com/*"] }
[/codesyntax]
有了该配置之后,在页面自身的脚本中就可以获得chrome.runtime
对象,然后通过sendMessage()
方法直接从页面向扩展程序发送消息内容。
但页面中是没有监听事件的方式,chrome扩展程序的“近扩展”部件是被禁止直接操作页面内容的,要想能够完成这任务还需要靠非常规的“脚本注入”这一手段。
脚本注入
其实Content Scripts已经算是一种“脚本注入”的方式:在配置文件里预先定义了需要注入到页面上执行的脚本文件和需要应用的样式代码。不过这些文件代码都是预定义好的,然后根据配置文件里约定的匹配加载到指定的页面上。
Chrome扩展程序还可以条件式的注入脚本到页面中,这就是在Background Page或Event Page里通过调用chrome.tabs.executeScript()
或chrome.tabs.insertCSS()
。
插入的脚本可以是代码段:
[codesyntax lang=”javascript” lines=”normal”]
chrome.browserAction.onClicked.addListener(function(tab) { chrome.tabs.executeScript({ code: 'document.body.style.backgroundColor="red"' }); });
[/codesyntax]
也可以是代码文件:
[codesyntax lang=”javascript” lines=”normal”]
chrome.browserAction.onClicked.addListener(function(tab) { chrome.tabs.executeScript({ file: 'make_red.js' }); });
[/codesyntax]
上边两个示例代码都是注册了在Browser Action的图标被点击时会激发的事件,该事件会向当前页面加入指定的代码或文件,这些代码会以Content Scripts的形式被执行,不同之处在于它们不是在页面加载后就执行,而是依据某个特定条件而插入执行。
之前提到Content Scripts可以访问和操作页面的DOM,但是无法得到页面自身JavaScript的对象和变量。但还是可以通过更进一步的“脚本注入”来曲线救国。因为DOM的操作不仅仅局限在对现有标签的获取和修改,还可以创建并插入新标签,这些表现也包括<script>
。
之前强调了在Content Scripts中是无法获得页面本身的变量信息,比如页面用了jQuery库,也无法获得jQuery或者$,但用下边的代码注入脚本,就可以在常规的console下输出当前页面所用的jQuery版本信息:
[codesyntax lang=”javascript” lines=”normal”]
var s = document.createElement('script'); s.textContent = 'console.log((typeof jQuery === "function") ? jQuery().jquery : "no jQuery");'; document.body.appendChild(s);
[/codesyntax]
进一步的还可以通过注入脚本文件来做更复杂的操作。
最后还是要提一下安全问题,脚本注入的方法固然很强大,不过假如注入时用到了页面中的一些内容,那很可能就留下了漏洞给其他人,使得他们可以随意假如可能造成破坏的脚本。这也是为什么对于Chrome扩展开发,虽然类似于普通的页面开发,但Google还是做了不少的基于安全考虑的限制政策。
比如在普通页面里添加JavaScript脚本,可以通过<script>
标签引用外部文件,可以内嵌代码在<script>
标签内,甚至可以直接在其他标签上内嵌事件代码。但是对于Chrome扩展开发,就只能通过引用外部文件来加载脚本,所有内嵌代码都会被忽略。另外那些可以将字符串内容直接运行的方法也被失效,比如eval()
和new Function()
。不过网页开发中使用这些方法以及内嵌脚本代码这些本来就是不好的习惯,所以不使用也不会是大问题。