Введение
Это не будет отличным шаблоном для публичного расширения браузера. Однако есть много случаев, когда это будет достаточно хорошим способом. Например, какой-нибудь внутренний отладчик, или что-то похожее на redux dev tools.
Требования
В manifest.json
:
{
"action": {
"default_popup": "index.html"
},
"permissions": ["tabs", "activeTab", "scripting"],
"manifest_version": 3
}
Подготовить
Мы должны иметь некоторое всплывающее окно для отображения полученных данных и разрешения на внедрение скрипта в активную вкладку.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="index.css" rel="stylesheet">
<title>Internal Dev Tools</title>
</head>
<body>
<div id="root"></div>
<script src="index.js"></script>
</body>
</html>
// popups index.js
chrome.tabs.query(
{ currentWindow: true, active: true },
([tab]) => {
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
args: [42],
func: function (arg) {
// executed in the page
return arg
},
},
([{ result }]) => {
// executed in the extension
console.log('received data: ', result); // 42
}
);
}
);
В этом скрипте расширение выполняет func
на вкладке активной страницы с одним аргументом «42». Возврат от него перехватывается в обратном вызове второго аргумента.
Передача данных со страницы
Теперь нам нужно передать данные со страницы:
// page
const stateReport = generateDataToSendIntoExtension();
const message = { type: 'DEV_TOOLS_REPORT_MESSAGE_TYPE', payload: stateReport };
window.postMessage(message);
Самый простой способ поддерживать его в актуальном состоянии — обернуть его с помощью setInterval:
// page
setInterval(() => {
// postMessage
}, 1_000)
Получение данных в расширении
Чтобы получить данные из window.postMessage(
, мы должны использовать window.addEventListener('message', callback);
. К сожалению, обратный вызов будет вызван асинхронно, поэтому простой возврат из предыдущей части не сработает, нам придется обернуть его в Promise
. Также страница вызывает window.postMessage(
каждые 1 секунду, поэтому в расширении мы должны использовать setInterval
.
// popups index.js
const intervalTimeout = setInterval(() => {
chrome.tabs.query(
{ currentWindow: true, active: true },
([tab]: [{ id: number }]) => {
// @ts-expect-error
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
args: ['DEV_TOOLS_REPORT_MESSAGE_TYPE'],
func: function (messageType) {
return new Promise((resolve) => {
const callback = ({ data }) => {
window.removeEventListener("message", callback);
if (data?.type === messageType) {
resolve(data.payload);
}
};
window.addEventListener("message", callback);
});
},
},
([{ result }]) => {
setState(result);
}
);
}
);
}, 1_000);
Поскольку мы используем setInterval
, мы должны помнить об удалении слушателя после вызова его обратного вызова. Также, чтобы избежать конфликтов с сообщениями другого окна, мы должны добавить оператор if, чтобы отфильтровать другие сообщения. И последнее — отображение данных со страницы в popup.html расширения. В моем случае popup содержит простое приложение react, поэтому единственное, что нужно сделать, это вызвать setState
с данными со страницы.
Резюме
Конечно, это не производственное решение. Слишком много разрешений, слабая реактивность и потенциальные утечки данных. Однако для внутренних целей (например, в качестве инструментов разработчика) это работает.