Chrome Extensions: Bridging the Gap Between Layers

Share this article

Building a Chrome extension is meant to be easy and in many ways, it is. The documentation is really well done and has loads and loads of examples. Also, it’s quite easy to inspect any of the ones you’ve installed already to see the magic going on behind. Another big plus is that it’s all Javascript, CSS and HTML with the added bonus of the Chrome API for the extra nibble of magic. Recently, I had to develop a toolbar for my company that would have to read the currently viewed page, show some SEO information, do some AJAX calls and so on. Not really hard, but I did stumble on a problem that was not really well documented (if at all). Before going further, if you’re not totally familiar with Chrome extension development, I suggest you go read this overview. You’ll understand more about the complexity between the multiple layers of the architecture.

The problem

I decided to load my extension’s UI elements (like the toolbar and miscellaneous popups) through injected iframes in every web page. With that in mind, the communication between multiple iframes, the current DOM, the Chrome Background Javascript file and other layers that Chrome offers was not the easiest thing to do. In fact, the problem resided with the use of iframes. I had to send lots of data through JSON objects from the Background layer to any of the iframes and vice versa. Manipulating an iframe is not possible from the Content Script injected in the page, because of the cross-domain restriction. For example, the page URL currently viewed is http://www.example.com and the injected iframe URLs are chrome-extensions://uniqueidmadeoutoflotsandlotsofletters Communication between both is impossible because cross-domain communication is a big NO-NO.

Why use iframes, then?

Well, iframes are the only way (currently) to isolate a chunk of Javascript, CSS and HTML without being influenced by the current web page style and behavior. Also, I was stubborn enough to think there was probably a way to communicate between all the layers in a graceful manner. Even though I could not find an answer on Google or StackOverflow.

What’s the solution?

When using the Chrome API method chrome.tabs.sendMessage to send a message from the Background layer, the message is sent to ALL frames, not just the one that has the ContentScript injected. I don’t know why I didn’t think of that first! Since it’s the ContentScript that injects the iframes, they ALSO have access to the Chrome API. So, the iframes can talk to their parent ContentScript with the default DOM method window.parent.postMessage, talk to the Background layer with chrome.extension.sendRequest and they can also listen to the Background layer messages with the chrome.extension.onMessage.addListener method.

How to make it happen?

The idea is simple: I create a set of Receptionists that will handle all the transfers of messages from one layer to another. Currently, this is how I’ve setup each layer’s roles :

Background (see background.js)

Can receive messages from ContentScript and either redirect them to the proper iframe or process the message. Can send messages to all frames (ContentScript and iframes).

ContentScript (see inject.js)

Can receive messages from both the Background layer and the iframes. When coming from an iframe (through the default window.postMessage method) it redirects the message to the Background if specified. If not specified, it processes the message. Can send messages only to the Background.

Iframe (see iframe.js)

Can receive messages from the only the Background layer, then checks if it was meant for him and then processes the message. Can send messages to the ContentScript with window.parent.postMessage. So in other words: –          Background talks to ContentScript and iframes, but only listens to ContentScript. –          ContentScript listens to Background and iframes
, but only talks to Background. –          Iframes talks to ContentScript and listen to Background. Side note: I understand that Background could also listen to iframe messages, but in my example I’ve skipped this concept since it was not necessary.

Differentiating the iframes

Every iframe has a unique ID (called view in my example later below) so it’s easy to redirect the messages to a particular iframe. A simple way to do so is by adding an  attribute in the URL when loading the iframe, like this:
chrome.extension.getURL('html/iframe/comment.html?view=comment’);
Messages setup The messages passed are simple objects containing two properties: –          message –          data Every layer (Background, ContentScript and IFrame) has a tell method that sends the message with both properties.
tell(‘tell-something’, {attribute1:’a’, attribute2:’b’});
When an iframe sends a message, the current iframe view ID is also sent as a source property in data.
tell(‘tell-parent-something’, {source:’comment’});
When a message needs to be sent to a particular iframe, a view property is added with the right view ID in data.
tell(‘tell-to-an-iframe’, {

    view:’comment’,

    title:’hello world!});
If a message needs to be sent to all iframes, I used the “*” wildcard for that.
tell(‘tell-to-all-iframes’, {view:*, title:’foo bar’});
If no view is specified, it’s the ContentScript/Background that should process the message.

Now, the example (finally)!

I’ve created a simple extension for liking pages that I call iHeart (you can find the source on my github). example It’s a simple button with a heart on the left side of the screen. When clicked, the user can add a comment and save it. The pages saved will be then listed in the extension popup button: heart button

The gritty details

Every layer has its own telling and listening methods:

Background

Telling
_this.tell = function (message, data){

    var data = data || {};

    chrome.tabs.getSelected(null, function (tab){

        if (!tab) return;

        chrome.tabs.sendMessage(tab.id, {

            message   : message,

            data : data

        });

    });

};
Listening
function onPostMessage (request, sender, sendResponse){

    if (!request.message) return;

    if (request.data.view){

        _this.tell(request.message, request.data);

        return;

    }

    processMessage(request);

};

ContentScript

Telling
function tell (message, data){

    var data = data || {};

    // send a message to "background.js"

    chrome.extension.sendRequest({

        message : message,

        data : data

    });

};
Listening
// messages coming from iframes and the current webpage

function dom_onMessage (event){

    if (!event.data.message) return;

    // tell another iframe a message
    if (event.data.view){
        tell(event.data);

    }else{

        processMessage(event.data);

    }

};

// messages coming from "background.js"

function background_onMessage (request, sender, sendResponse){

    if (request.data.view) return;

    processMessage(request);

};

Iframe

Telling
_this.tell = function (message, data){

var data = data || {};

data.source = _view;

window.parent.postMessage({

        message   : message,

        data : data

    }, '*');

};
Listening
function background_onMessage (request, sender, sendResponse){

    // make sure the message was for this view (you can use the "*" wildcard to target all views)

    if (

        !request.message ||

        !request.data.view ||

        (request.data.view != _view && request.data.view != '*')

    ) return;

    // call the listener callback

    if (_listener) _listener(request);

};
The communication process is quite simple. When you visit a web page and like what you see (it can be anything really, it’s what you like, I won’t judge), you then click on the iHeart button. Then, the button tells to open up the comment iframe.

js/iframe/heart.js

function heart_onClick (event){

    $('.heart').addClass('active');

    _iframe.tell('heart-clicked');

};
It then processes the message in the ContentScript and opens the comment popup.

js/inspect.js

function processMessage (request){

if (!request.message) return;

    switch (request.message){

        case 'iframe-loaded':

            message_onIframeLoaded(request.data);

            break;

        case 'heart-clicked':

            message_onHeartClicked(request.data);

            break;

        case 'save-iheart':

            message_onSaved(request.data);

            break;

    }

};

...

function message_onHeartClicked (data){

    var comment = getView('comment');

    comment.iframe.show();

    tell('open-comment', {

        view:'comment',

        url:window.location.href,

        title:document.title

    });

};
The comment popup shows up and displays the current web page title below the comment box. js/iframe/comment.js
function onMessage (request){

    switch (request.message){

        case 'open-comment':

            message_onOpenComment(request.data);

            break;

        case 'website-is-hearted':

            message_onIsHearted(request.data);

            break;

    }

};

...

function message_onOpenComment (data){

    $('.page-title').html(data.title);

};
When the save button is pressed, the comment iframe sends the info back to the ContentScript. js/iframe/comment.js
function save_onClick (event){

    var comment = $('#comment').val() || '';

    _iframe.tell('save-iheart', {

         comment   : comment

    });

};
The ContentScript hides the comment iframe and tell the Background to save the whole thing. js/inject.js
function message_onSaved (data){

    var comment = getView('comment');

    comment.iframe.hide();

    tell('save-iheart', {

        url:window.location.href,

        title:document.title,

        comment:data.comment

    });

};
And finally, Background finalizes all the details by saving the website in an array. js/background.js
function onPostMessage (request, sender, sendResponse){

    if (!request.message) return;

    if (request.data.view){

        _this.tell(request.message, request.data);

        return;

    }

    switch (request.message){

        case 'save-iheart':

        message_onSaved(request.data);

        break;

    case 'all-iframes-loaded':

        message_allIframesLoaded(request.data);

        break;

    }

};function message_onSaved (data){

    _websites.push({

        url           : data.url,

        title         : data.title,

        comment       : data.comment

    });

};

And … the receptionists did their job

That’s pretty much it. That’s my solution to a communication problem between multiple types of layers and it wasn’t too hard … Now, if I could resolve as easily the communication problems in my personal relationship, that would be great, thanks :P The example could be taken way way further by dealing with validations of data, saving the liked web pages in a database, dealing with resizing of iframes content dynamically, adding some animation to the extension to make it more fun to use. All of that is great and already do-able, but it’s out of scope of this article.

Frequently Asked Questions (FAQs) about Chrome Extensions

How do I allow Chrome extensions to access iframes?

Chrome extensions can access iframes by using the “all_frames” option in the manifest.json file. This option allows the extension to run scripts not only in the top frame, but also in every iframe. The “all_frames” option is set to false by default, so you need to manually change it to true. However, be aware that allowing access to all iframes can pose security risks, so it’s important to use this option judiciously.

How can I inject CSS into a webpage through a Chrome extension?

Injecting CSS into a webpage through a Chrome extension involves creating a content script. In the manifest.json file, you need to specify the CSS file that you want to inject. The “css” property in the “content_scripts” section allows you to list the CSS files to be injected. When the conditions specified in the “matches” property are met, the CSS file will be injected into the webpage.

What is the purpose of the “iframe allow” Chrome extension?

The “iframe allow” Chrome extension is designed to give users control over the iframe settings on their browser. By default, Chrome blocks third-party iframes for security reasons. However, some websites require iframes to function properly. The “iframe allow” extension lets users enable iframes on a per-site basis, ensuring that they can access the full functionality of websites while maintaining their security settings.

How does the “Super CSS Inject” Chrome extension work?

The “Super CSS Inject” Chrome extension allows users to inject custom CSS into any webpage. After installing the extension, users can click on the extension icon and enter their custom CSS. The extension then injects the CSS into the current webpage, allowing users to customize the appearance of the webpage to their liking.

What is the “iframe new tab” Chrome extension used for?

The “iframe new tab” Chrome extension is designed to open iframes in a new tab. This can be useful when you want to view the content of an iframe in a larger view. After installing the extension, you can right-click on any iframe and select the “Open iframe in new tab” option. The content of the iframe will then be opened in a new tab.

Maxime LefrançoisMaxime Lefrançois
View Author

Maxime is a web programmer by day and a Karaoke fanatic by night. Works for a web agency as a Front End Programmer. His main interests are JavaScript, CSS and Wordpress, Flash ActionScript and bacon.

google chrome
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week
Loading form