Статьи

Расширения Chrome: преодоление разрыва между слоями

Создание расширения Chrome должно быть простым и во многих отношениях. Документация действительно хорошо сделана и содержит множество примеров. Кроме того, довольно легко осмотреть любой из тех, что вы уже установили, чтобы увидеть, что происходит за волшебством. Еще одним большим плюсом является то, что это все Javascript, CSS и HTML с дополнительным бонусом API Chrome для дополнительного куска магии.

Недавно мне пришлось разработать панель инструментов для моей компании, которая должна была бы читать просматриваемую в данный момент страницу, показывать некоторую SEO-информацию, делать AJAX-вызовы и так далее. Не очень сложно, но я наткнулся на проблему, которая не очень хорошо документирована (если вообще).

Прежде чем идти дальше, если вы не совсем знакомы с разработкой расширений для Chrome, советую прочитать этот обзор . Вы поймете больше о сложности между несколькими уровнями архитектуры.

Проблема

Я решил загрузить элементы пользовательского интерфейса моего расширения (например, панель инструментов и другие всплывающие окна) через встроенные фреймы на каждой веб-странице. Имея это в виду, обмен данными между несколькими iframe , текущим DOM, файлом Chrome Background Javascript и другими слоями, которые предлагает Chrome, был не самым простым делом.

На самом деле проблема заключалась в использовании фреймов . Мне пришлось отправить много данных через объекты JSON из фонового слоя в любой из фреймов и наоборот. Из -за междоменного ограничения управление iframe невозможно из сценария содержимого, введенного на странице.

Например, URL страницы, просматриваемый в данный момент:

http://www.example.com

и вставленные URL-адреса iframe

хром-расширения: // uniqueidmadeoutoflotsandlotsofletters

Связь между обоими невозможна, потому что междоменная связь — это большая НЕТ-НЕТ.

Зачем тогда использовать фреймы?

Что ж, iframes — это единственный способ (в настоящее время) изолировать кусок Javascript, CSS и HTML без влияния текущего стиля и поведения веб-страницы.

Кроме того, я был достаточно упрям, чтобы думать, что, возможно, есть способ изящно общаться между всеми слоями. Хотя я не смог найти ответ в Google или StackOverflow.

Какое решение?

При использовании метода Chrome API chrome.tabs.sendMessage для отправки сообщения из фонового слоя сообщение отправляется во ВСЕ кадры, а не только в тот, в который вставлен ContentScript.

Я не знаю, почему я не подумал об этом первым!

Так как это ContentScript, который внедряет iframes , они также имеют доступ к Chrome API.

Таким образом, фреймы могут общаться со своим родительским ContentScript с помощью метода DOM по умолчанию window.parent.postMessage , общаться с фоновым слоем с помощью chrome.extension.sendRequest, а также они могут прослушивать сообщения фонового уровня с помощью chrome.extension.onMessage. метод addListener .

Как это сделать?

Идея проста: я создаю набор администраторов, которые будут обрабатывать все передачи сообщений с одного уровня на другой.

В настоящее время вот как я настроил роли каждого слоя:

Фон (см. Background.js)

Может получать сообщения из ContentScript и либо перенаправлять их в соответствующий фрейм, либо обрабатывать сообщение.

Может отправлять сообщения во все кадры (ContentScript и iframes ).

ContentScript (см. Inject.js)

Может получать сообщения как из фонового слоя, так и из фреймов .

При поступлении из iframe (через метод window.postMessage по умолчанию) он перенаправляет сообщение в фоновый режим, если он указан. Если не указано, обрабатывает сообщение.

Можно отправлять сообщения только в фоновом режиме.

Iframe (см. Iframe.js)

Может получать сообщения только от фонового слоя, затем проверяет, предназначено ли оно для него, а затем обрабатывает сообщение.

Может отправлять сообщения в ContentScript с помощью window.parent.postMessage .

Итак, другими словами:

— Фон общается с ContentScript и iframes , но слушает только ContentScript.

— ContentScript слушает Background и iframes , но говорит только с Background.

Iframes общается с ContentScript и слушает фон.

Примечание: я понимаю, что Background также может прослушивать сообщения iframe , но в моем примере я пропустил эту концепцию, так как в ней не было необходимости.

Дифференциация фреймов

Каждый iframe имеет уникальный идентификатор (называемый view в моем примере ниже), поэтому легко перенаправить сообщения в определенный iframe . Простой способ сделать это — добавить атрибут в URL при загрузке iframe , например так:

chrome.extension.getURL('html/iframe/comment.html?view=comment’);

Настройка сообщений

Передаваемые сообщения — это простые объекты, содержащие два свойства:

— сообщение

— данные

Каждый слой (Background, ContentScript и IFrame) имеет метод Tell, который отправляет сообщение с обоими свойствами.

 tell(‘tell-something’, {attribute1:’a’, attribute2:’b’});

Когда iframe отправляет сообщение, текущий идентификатор представления iframe также отправляется как свойство источника в данных .

 tell(‘tell-parent-something’, {source:’comment’});

Когда сообщение необходимо отправить в определенный iframe , свойство представления добавляется с правильным идентификатором представления в данных .

 tell(‘tell-to-an-iframe’, {

    view:’comment’,

    title:’hello world!});

Если сообщение должно быть отправлено на все iframes , я использовал для этого подстановочный знак «*».

 tell(‘tell-to-all-iframes’, {view:*, title:’foo bar’});

Если представление не указано, это ContentScript / Background должен обрабатывать сообщение.

Теперь пример (наконец-то)!

Я создал простое расширение для симпатичных страниц, которое я называю iHeart (вы можете найти исходный код на моем github ).

пример

Это простая кнопка с сердцем на левой стороне экрана. При нажатии пользователь может добавить комментарий и сохранить его. Сохраненные страницы будут перечислены во всплывающей кнопке расширения:

кнопка сердца

Песчаные детали

Каждый слой имеет свои собственные методы говорения и прослушивания :

Фон

запечатленное

 _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

запечатленное

 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

запечатленное

 _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);

};

Процесс общения довольно прост. Когда вы посещаете веб-страницу и вам нравится то, что вы видите (это может быть что угодно, это то, что вам нравится, я не буду судить), вы затем нажимаете кнопку iHeart . Затем кнопка говорит, чтобы открыть комментарий iframe.

JS / Iframe / heart.js

 function heart_onClick (event){

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

    _iframe.tell('heart-clicked');

};

Затем он обрабатывает сообщение в ContentScript и открывает всплывающее окно комментария.

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

    });

};

Появится всплывающее окно с комментарием, которое отображает заголовок текущей веб-страницы под полем для комментариев

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

};

Когда кнопка сохранения нажата, iframe комментария отправляет информацию обратно в ContentScript.

JS / Iframe / comment.js

 function save_onClick (event){

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

    _iframe.tell('save-iheart', {

         comment   : comment

    });

};

ContentScript скрывает комментарий iframe и указывает фону сохранить все это.

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

    });

};

И, наконец, Background завершает все детали, сохраняя сайт в массиве.

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

    });

};

И … администраторы сделали свою работу

Вот и все. Это мое решение проблемы связи между несколькими типами слоев, и это было не слишком сложно …

Теперь, если бы я мог так же легко решить проблемы общения в моих личных отношениях, это было бы здорово, спасибо: P

Этот пример можно было бы сделать намного дальше, рассматривая проверки данных, сохраняя любимые веб-страницы в базе данных, динамически изменяя размер содержимого iframes , добавляя анимацию в расширение, чтобы сделать его более увлекательным в использовании. Все это прекрасно и уже выполнимо, но выходит за рамки этой статьи.