Нельзя просто так взять и обратиться к фоновой странице

  • Tutorial
Всё дело — в политике безопасности, аналогичной кроссдоменной. Обращение к страницам других табов или к фоновой странице расширения сознательно ограничено, потому что они считаются страницами других доменов, имеют запреты на прямой доступ к скриптовому окружению, аналогично чужим окнам и фреймам. Механизм сообщений «спасает» как при кроссдоменном доступе между фреймами, как и в доступе к страницам расширений (фоновая, настройки, попап, ...).

В расширении браузера Google Chrome (и Chromium) наиболее важна по функциям — фоновая страница. Она имеет специальный URL вида chrome-extension://ciegcibjokpkcklhgbpnmnikpkmkhbjk/, где длинное имя домена — случайное имя, создаваемое в недрах браузера, которым именуется также каталог расширения где-то в служебной папке ОС. Из контентного скрипта (аналогичного юзерскриптам, исполняемым на странице браузера) можно получить доступ к файлам и картинкам расширения. Но нельзя выполнить много функций, путь к которым лежит через фоновую страницу: устроить хранилище, относящееся к группе реальных доменных имён; хранить настройки расширения, общие для всего расширения. Нужно лишь добраться в Мордор к фоновой странице. Однако, нельзя просто так, по URL, это сделать.

Данного описания нет на страницах документации к Хрому. Вернее, оно есть, но для «немного других» методов и объектов, что фактически означает — нет, до тех пор, пока не исправили. Поведение описанных объектов для обмена сообщениями подтверждается несколькими примерами в ответах на StackOverflow. Искать ответы каждый раз по разрозненным примерам — утомительно, поэтому, чтобы покончить c этой неразберихой, пусть уж они будут собранными в одном месте здесь. Перебраны все 4 комбинации прямых и обратных вызовов, чтобы перед глазами всегда были работающие шаблоны их.

Для проверки и демонстрации нужно скопировать или создать 3 файла (manifest.json, script.js, background.js), создав тестовое расширение в режиме разработчика в Хроме, и смотреть на сообщения в консоли из 2 страниц — окна браузера с внедрённым скриптом script.js и фоновой (смотреть консоль фоновой background.js — нажав на ссылку «Проверить режимы просмотра: _generated_background_page.html» в chrome://extensions/). То же будет работать в каком-нибудь готовом расширении, если 2 фрагмента кодов из статьи расставить по страницам и выполнить.

На Хабре подобная задача решалась в прикладных целях в habrahabr.ru/post/159145, ноябрь 2012 — те же самые источники знаний, которые молчаливо подправили и использовали. В интернете на русском похожая задача описывалась в статье от июня 2012. Здесь — описан механизм, который используется для решения таких задач, с учётом того, что он выполняется в скриптах расширения браузера Google Chrome.

Если к страницам в окнах и фреймах того же домена получаем доступ напрямую (как к окну другого скриптового окружения), то фоновая страница, равно как и другие табы и страницы браузера, выступают как страницы чужих доменов. Мы можем слать им сообщения, реализуя паттерн слабой связанности процедур, и можем передавать в них лишь объекты, способные к сериализации — никаких DOM, окружений и других сложных объектов.

К фоновой странице есть, из-за чего стремиться. В ней выполняются такие функции, которые в скрипте или юзерскрипте немыслимы. Там — не только память, общая для всех страниц. Там — реализация многих интерфейсов, описанных на developer.chrome.com/extensions/api_index.html, а в будущем ожидается и того больше. В скудном интерфейсе сообщений есть функции-коллбеки, которые, будучи заданными, выполняются в вызывающей функции, на стороне своего окружения, но с параметром-результатом, который передала другая сторона.

Всё это — немного модифицированный интерфейс среды Javascript postMessage, созданный не очень давно в браузерах для решения проблем регламентации кроссдоменного доступа. Обычно он применяется для общения скриптов между окнами и фреймами и остаётся единственным удобным средством общения, если домены страниц — разные. Ранее для решения этой проблемы использовался «костыль» под кодовым именем «window.name», по ключевым словам используемых объектов. Он работает и сейчас, но сложный и, очевидно, медленный, по сравнению с методом postMessage, пришедшим ему на замену. Вспомним, как работает обычный механизм postMessage, потому что на нём основаны механизмы обмена данными в расширении Хрома.

Интерфейс postMessage (кроссбраузерно, IE8+)


Страница-инициатор сообщения создаёт пользовательское событие «message» — делает отправку сообщения, причём делает это не из своего домена, а из того, к которому желает обратиться. (в IE8 есть ограничение — обращение только к родительскому окну фрейма).
otherWindow.postMessage(message, targetOrigin);
	//message - строка или другой _сериализуемый_ объект
	//targetOrigin - название целевого домена окна otherWindow, для "проверки знания"

Если имя домена угадано, сообщение отправится. Для его приёма в окне otherWindow должна быть выполнена пара условий:
1) существовать обработчик события;
2) обработчик «ждёт» сообщение именно с домена-отправителя и проверяет это через второй аргумент.
window.addEventListener('message', function(event){ //приём сообщения
  if(event.origin !== 'URL_домена-отправителя') //проверка отправителя, например, 'http://example.org'
    return;
  ...//использование event.data, равного message из .postMessage
  ...//доступно event.source - окно-окружение передающего события, но
     //  содержимое его недоступно в кроссдоменных обменах
}, false);


Отправляем сообщение из окна в фоновую страницу (Chrome)


В расширениях основу (postMessage) немного переработали, добавив коллбеки и автоматизировав проверки доменов. Стало удобнее, чем если собирать передачу сообщения из кирпичиков. В самом конце приведём код на стандартном postMessage, используя доступные средства, чтобы показать, что магии в следующих описанных sendMessage или sendRequest нет — это просто оболочка. Но postMessage не смог выполниться из-за недостаточных прав расширения на создание фрейма с особым протоколом «chrome-extension://*», про который формат манифест-файла тоже ничего не знает, и дело утонуло в бюрократических проволочках. Вот и ответ на вопрос, почему им пришлось придумать свои функции обмена.

Приведённые примеры можно увидеть, проверить и исследовать их поведение со страницы установки расширения Chrome: spmbt.kodingen.com/bgMessageXmp/index.htm. Если расширение не установлено, клики по 5 ссылкам сообщают об отсутствии расширения. После установки (в расширении — всего 3 необходимых файла) клики начинают выполнять примеры, описанные ниже.

Как установить: 1) скачать архив, 2) распаковать (или сразу инсталлировать), 3) на chrome://extensions/ перейти в «режим разработчика», 4) выполнить «Загрузить распакованное расширение...», выбрав каталог из архива, 5) обновить страницу, с которой загружалось расширение. 5 ссылок готовы к выполнению примеров. После выполнения достаточно удалить скрипт средствами chrome://extensions/. Этот скрипт примеров может быть использован как каркас для тестирования или написания расширений. Для данной статьи он помог отладить скрипты из текста и устранить из них опечатки.

Пример 1.
Если надо отправить из таба в фоновую страницу и на этом закончить, используется простой формат сообщений:
	chrome.extension.sendMessage('некий объект в фон');

Принимается сообщение в фоновой или любой другой странице:
chrome.extension.onMessage.addListener(function(request){
	if(request=='некий объект в фон') //проверяется, от того ли окна и скрипта отправлено
		console.log('1. Принято: ', request);
});


Пример 2.
Эта пара функций приспособлена для обмена обратным сообщением. Добавляем параметров, из скрипта:
chrome.extension.sendMessage('запрос backMsg', function(backMessage){
	console.log('2. Обратно принято из фона:', backMessage);
});

из фоновой страницы:
chrome.extension.onMessage.addListener(function(request, sender, f_callback){
	if(request=='запрос backMsg'){ //проверяется, от того ли окна и скрипта отправлено
		console.log('2. прошло через фон: ', request);
		f_callback('backMsg'); //обратное сообщение
	}
});

f_callback — выполняется в скрипте (не в фоновой странице), но с параметром, заданным в фоновой.
Объект sender имеет вид:
{	active: true
	highlighted: true
	id: 34 //id страницы  (вкладки) Хрома
	incognito: false
	index: 0
	pinned: false
	selected: true
	status: "complete"
	title: "HabrAjax by spmbt" //document.title
	url: "http://spmbt.kodingen.com/habrahabr/habrAjax/index.htm" //location.href
	windowId: 1 //id окна Хрома
}

Таким образом, из него кое-что известно о том, кто отправил сообщение. Сейчас не будем использовать значений из sender, но возможно отслеживание, из какого таба или окна пришло сообщение, рассылать изменения в другие контентные скрипты, если они есть в других табах.
(конец примера 2)

Если надо передать что-то посложнее — нет проблем встроить хеш или массив вместо строки. Передача хеша вместо набора аргументов — вообще, более продвинутая форма обмена — занимает 1 аргумент, читается легче, так как самодокументирован ключами, значения хеша переставляются и удаляются элементарно — не нужно помнить порядок и беспокоиться об отсутствующих значениях.

С техникой замены аргумента на хеш 3 аргументов хватит на всё, и каждый из них выполняет свою роль. Идя дальше, их можно было бы заменить 1 аргументом, с ключами {request:..., sender:..., callback: f(){...}}; почему не сделали? Видимо, посчитали, что 3 аргумента — это ещё такой джентльменский предел, который не зазорно предлагать к запоминанию (и к дальнейшей расшифровке). К тому же, на самодокументацию тратятся символы. Когда аргументов 2-3, выбор склоняется в сторону позиционных аргументов.

Например, нужно отправлять не только имя команды, но и хеш с данными. Пишем из скрипта:
var data ='From page ';
chrome.extension.sendMessage({cmd:'exec1', h:{data1: data, dataX: d+1}, function(backMessage)
	console.log(backMessage);
}});

из фоновой страницы:
chrome.extension.onMessage.addListener(function(request, sender, callback){
  if(request.cmd =='exec1'){
    callback('backMsg'); //обратное сообщение
    console.log('Tis message in background page printed after'
      +' receive of data1 = ', request.h.data1, '; URL= ',sender.url);
});

Укоротим часто повторяющиеся вызовы из контентных страниц:
var inBg = function(cmd, h, f_back){
	h.cmd = cmd;
	chrome.extension.sendMessage(h, f_back);
});
...
inBg('exec1', {data1: data, dataX: d+1}, function(){
	...
});


(Третьего коллбека почему-то не предусмотрено… Управление перебрасывается только 2 раза.)

Отправляем сообщение из фоновой страницы в окно (Chrome)


Пример 3.
Если стоит обратная задача, отправить сообщение из фоновой страницы в, допустим, активный таб:
в фоновой пишем:
chrome.tabs.getSelected(null, function(tab){ //выбирается ид открытого таба, выполняется коллбек с ним
    chrome.tabs.sendRequest(tab.id,{msg:"msg01"}); //запрос  на сообщение
}); 

в контенте:
chrome.extension.onRequest.addListener(function(req){ //обработчик запроса из background
	console.log('3. Принято из фона:', req.msg); //выведется переданное сообщение
});

Принципиально ли то, что в первом случае использовали пару sendMessage-onMessage, а во втором — sendRequest-onRequest? Нет, работает любой метод, лишь бы он был из одной пары.

Пример 4.
Если нужно отправить сообщение с обратным коллбеком, в фоновой пишем:
var inBack = function(tabId, cmd, h, f_back){
	h.cmd = cmd;
	chrome.tabs.sendMessage(tabId, h, f_back);
};
chrome.tabs.getSelected(null, function(tab){
	inBack(tab.id,'exec0', {dat:'h'}, function(backMessage){
		console.log('4. Обратный приём из контента: ', backMessage);
	});
});

На контент-странице:
chrome.extension.onMessage.addListener(function(request, sender, callback){
	if(request.cmd =='exec0'){ //выполнить
		console.log('4. из фона:', request.dat);
		callback('12w3');
	}
});


Выполняется, как видно — всё, что было описано в документации, но для немного другого объекта (chrome.runtime), на developer.chrome.com/extensions/messaging.html, а chrome.runtime в данной задаче не работает — не имеет метода sendMessage.

Передача на стандартном postMessage (не выполнена)


Несмотря на отрицательный результат, эта попытка передачи подскажет догадки о том, почему разработчикам пришлось заняться собственными функциями передачи данных в расширениях, несмотря на наличие postMessage.

Один из немногих выполнимых в контентном скрипте особых методов расширений — chrome.extension.getURL('путь'). Он возвращает путь к ресурсам расширения. Открыв его, получим ресурсы расширения (но не к каталогам) — картинки и тексты из него. Иной домен и здесь сыграет свою роль: чтобы получить тексты в окружение скрипта страницы, нужно делать кроссдоменный Ajax. Или поступить проще — получить тексты через стенку домена механизмом сообщений.

Пример 5.
Передадим сообщение, используя определившийcя протокол («chrome-extension:») с доменом (что-то вида "//ciegcibjokpkcklhgbpnmnikpkmkhbjk").
var bgUrl = chrome.extension.getURL('');
console.log(bgUrl); //удовлетворение любопытства

Но не всё просто. Доступа к окну фоновой страницы у контентного скрипта нет. Метод есть:
chrome.extension.getBackgroundPage();

Но доступа нет:
Uncaught Error: «getBackgroundPage» can only be used in extension processes. See the content scripts documentation for more details.
Коммандо не унывает. Для простого обмена ведь надо создать фрейм. Ничего не мешает создать фрейм.
var ifr = document.createElement('iframe');
ifr.id ='ifr1';
ifr.src = bgUrl;
document.body.appendChild(ifr);

Возникла ошибка:
Denying load of chrome-extension://ciegcibjokpkcklhgbpnmnikpkmkhbjk/. Resources must be listed in the web_accessible_resources manifest key in order to be loaded by pages outside the extension.
Добавляем разрешение в манифест (а при chrome.extension.sendMessage этого было не нужно).
«permissions»:[«chrome-extension://*», ...]
Возникла ошибка на chrome://extensions/:
При установке расширения возникли предупреждения:
Permission 'chrome-extension://' is unknown or URL pattern is malformed.

Если бы доступ не был запрещён, осталось бы выполнить
ifr.postMessage('Yep', bgUrl.replace(/\/$/,'') );

и в фоновой —
window.addEventListener('message', function(ev){
	console.log('origin: ', ev.origin);
},!1);


На этом пример можно закончить — он показал, что доступ через штатную функцию закрыли по другим причинам — из-за необходимости просмотра фоновых страниц в фреймах, чего по безопасности, видимо, было недопустимо (в «manifest_version»: 2). Поэтому в расширениях Chrome, но также и по соображениям оптимизации синтаксиса и двунаправленного обмена, были придуманы специальные методы обмена сообщениями.

Пример показал, что особых фокусов в передаче сообщений в фоновую страницу нет. Но нужны особые разрешения, которых для расширений не имеем. Закрытая часть методов объекта chrome.extension это спокойно делает, но другим не даёт. (Нужно попытаться ещё проделать это через sandbox.html — разрешит ли она создать в себе фрейм с фоновой страницей.)

На установку тестового расширения и тестовую страницу.
  • +25
  • 36,2k
  • 8
Поделиться публикацией

Комментарии 8

    +4
    В своё время родил такую конструкцию:
    function ChromeBus(channel, handlers) {
    	this.channel = channel;
    	this.handlers = handlers;
    };
    
    ChromeBus.prototype = {
    	init: function() {
    		var self = this;
    		if( this.channel == null ) { 
    			/* background script */
    			this.ports = {};
    			chrome.extension.onConnect.addListener(function(port){ self.onConnect(port); });
    			this.postMessage = this.postMessage_chan;
    		} else { 
    			/* content script */
    			this.port = chrome.extension.connect({'name': this.channel});
    			this.port.onMessage.addListener(function(msg, port){ 
    				self.onMessage_ext(msg, port); 
    			});
    			this.postMessage = this.postMessage_ext;
    		}
    	},
    
    	onConnect: function(port) {
    		var self = this;
    		var channel = port.name;
    		if( this.ports[channel] === undefined ) 
    			this.ports[channel] = [port];
    		else
    			this.ports[channel].push(port);
    		port.onDisconnect.addListener(function(port){ self.onDisconnect(port); });
    		port.onMessage.addListener(function(msg, port){ self.onMessage_chan(msg, port); });
    		if( this.handlers['onConnect'] !== undefined )
    			this.handlers['onConnect'].call(this, channel, port);
    	},
    
    	onDisconnect: function(port) {
    		var self = this;
    		var channel = port.name;
    		this.ports[channel] = this.ports[channel].filter(function(p){return p != port;});
    		if( this.handlers['onDisconnect'] !== undefined )
    			this.handlers['onDisconnect'].call(this, channel, port);
    	},
    
    	postMessage_ext: function(id, msg) {
    		msg = msg || {};
    		msg.id = id;
    		this.port.postMessage(msg);
    	},
    
    	onMessage_chan: function(msg, port) {
    		method = this.handlers[msg.id];
    		if( method ) method.call(this, port.name, msg);
    	},
    
    	postMessage_chan: function(channel, id, msg) {
    		msg = msg || {};
    		msg.id = id;
    		this.ports[channel].forEach(function(port){	port.postMessage(msg);});
    	},
    
    	onMessage_ext: function(msg, port) {
    		method = this.handlers[msg.id];
    		if( method ) method.call(this, msg);
    	}
    }
    


    Используется одинаково и на фоновой, и на встроенных:
    
    function Background() {
    	this.init();
    }
    
    Background.prototype = new ChromeBus(null, {
    	'ack': function(channel, args) {
    		this.postMessage(channel, 'hello');
    	},
    
    	'user-logged': function() {
    		this.postMessage('embedded', 'reload');
    		this.postMessage('frame', 'reload');
    	},
    })
    

      +1
      А, ну и для общения со скриптами в родных доменах (у меня это были iframы):
      function WinBus(handlers) {
        this.handlers = handlers;
      };
      
      WinBus.prototype = {
        init: function() {
          var self = this;
          window.addEventListener("message", function(event){ self.onWinMessage(event) });
        },
      
        postWinMessage: function(id, msg) {
          var origin = window.location.origin;
          msg = msg || {};
          msg.id = id;
          window.postMessage( msg, origin );
        },
      
        onWinMessage: function(event) {
          method = this.handlers[event.data.id];
          if( method ) method.call(this, event.data);
        },
      
      }
      
      
      +5
      chrome-extension://ciegcibjokpkcklhgbpnmnikpkmkhbjk/, где длинное имя домена — случайное имя, создаваемое в недрах браузера,


      Вообще-то, это — половина SHA-256 хэша публичного ключа в a-p кодировке.
        0
        Если не трудно киньте ссылку как такое генерировать.
          +1
          Пример кода на Питоне; объяснение, почему именно такой формат.
        0
        Суровые Челябинские Хромокодеры)))
          +3
          (кроссбраузерно, IE8+)

          Смотрится как «18+»
            0
            То же вполне доступно описано в документации. Не нахожу ничего сложного в общении контент-скрипта и background-страницы.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое