愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Chrome Extension 开发总结(二):通信机制

一、前言

Chrome Extension中,存在多种类型的脚本,它们具有不一样的API访问权限、域访问权限和DOM访问权限,因此他们能够各司其职,完成不一样的角色职责。有时候,当当前的脚本不支持某一特定的权限时,与具备相应权限的脚本进行通信,就尤为重要。所以本文主要介绍Chrome Extension中的通信机制。


二、脚本权限

在了解通信机制之前,让我们先总结下各种脚本类型,分别具有哪些权限,按Chrome API访问情况DOM访问情况跨域访问情况原页面JS访问情况,可以汇总为以下的表格:


三、互相通信总览

关于content scriptpopup scriptbackground script,他们之间互相通信的一个总体概览如图所示:


三、详解通信机制

1、popup与background

popupbackground非常类似,他们几乎可以访问一样的API、有一样的通信机制且都可以直接跨域。在popup中,可以直接调用background中的js方法和直接访问background的DOM,如下:

// background.js
function foo() {
    alert('Hello')
}

// popup.js
const bg = chrome.extension.getBackgroundPage()
bg.foo()
alert(bg.document.body.innerHTML)

background访问popup的方法则如下(在popup已经打开的情况下):

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

2、popup/background与content script

popup scriptbackground script,他们与content script之间通信的方式是一样的,以下先来看看采用sendMessage的方式:

// popup.js或者background.js中
function sendMessageToContentScript(message, callback) {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
        chrome.tabs.sendMessage(tabs[0].id, message, (response) => {
            if (callback) {
                callback(response)
            }
        })
    })
}

sendMessageToContentScript({ key: 'test', payload: 'Hi, this is a message from popup.js'}, (response) => {
    console.log('this is a message from content script', response)
})

// content.js中接收
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.key === 'test') {
        alert(request.payload)
    } 
    sendResponse('Hi, I am content.js and I have received your message')
})

而如果content-script想要主动发一个消息给background script或者popup script,那么可以这样子做:

// content.js
chrome.runtime.sendMessage({ payload: 'Hello, I am content.js' }, (response) => {
    // response 是 background 或者 popupjs 响应后的回复
});

// background.js或者popup.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    console.log('Recived message from content.js', response)
    sendResponse('Send a message to reply')
})

注意:

  • content scriptpopup主动发消息时,要求popup必须处于打开状态,否则就只能通过background中转了
  • 对于来自content script的消息,backgroundpopup都可以监听,并且可以同时收到消息,但是只有一个可以sendRequest(先发先得,后发无效)

3、injected script与content script

content scriptinjected script所共享的东西是页面的DOM元素,因此消息通信机制主要基于DOM,可以是:

  • 通过window.postMessagewindow.addEventListener实现通信
  • 通过自定义DOM事件实现通信

其中第一种方法的示例为:

// injected script中
window.postMessage({ message: 'xxx' }, '*')

// content script中
window.addEventListener('message', (e) => {
    console.log(e.data)
}, false)

而第二种方法的示例为:

// injected script
const customEvent = document.createEvent('Event')
customEvent.initEvent('myCustomEvent', true, true)
function fireCustomEvent(data) {
    hiddenDiv = document.getElementById('myCustomEventDiv')
    hiddenDiv.innerText = data
    hiddenDiv.dispatchEvent(customEvent)
}
fireCustomDiv('Hello')

// content script种
const hiddenDiv = document.getElementById('myCustomEventDiv')
if (!hiddenDiv) {
    hiddenDiv = document.createElement('div')
    hiddenDiv.style.display = 'none'
    document.body.appendChild(hiddenDiv)
}
hiddenDiv.addEventListener('myCustomeEvent', () => {
    const eventData = document.getElementById('myCustomEventDiv').innerText
    console.log('Received data', eventData)
})

4、长连接与短连接

在通信机制中,我们前面已经学到了chrome.tabs.sendMessage这种方式,我们会发现这种方式是“你发送一下,我回复一下”的形式,在发送完成后,这次通信就结束了。所以可以说,chrome.tabs.sendMessage这种方式是短连接,那么与之对应的,还有另外一种通信方式,即为长连接,长连接主要通过chrome.tabs.connectchrome.runtime.connect这两个API来完成,示例代码如下:

// popup.js
getCurrentTabId((tabId) => {
    const port = chrome.tabs.connect(tabId, { name: 'test' })
    port.postMessage({ message: 'xxx' })
    port.onMessage.addListener((data) => {
        console.log('Received from content:', data.message)
        if (data.message === 'WHO') {
            port.postMessage({ message: 'I am popup.js' })
        }
    })
})

// content.js
chrome.runtime.onConnect.addListener((port) => {
    console.log(port)
    if (port.name === 'test') {
        port.onMessage.addListener((data) => {
            console.log('Received from popup:', data.message)
            if (data.message === 'WHO') {
                port.postMessage({ message: 'I am content.js' })
            }
        })
    }
})


四、参考资源