Управляемая рама

Демиан Рензулли
Demián Renzulli
Саймон Хангл
Simon Hangl

Элемент <iframe> обычно используется для встраивания внешних ресурсов в контекст браузера. IFrame обеспечивают соблюдение политик безопасности веба, изолируя внедрённый контент из разных источников от главной страницы и наоборот. Хотя такой подход повышает безопасность, обеспечивая надёжную границу между источниками, он ограничивает некоторые варианты использования. Например, пользователям может потребоваться динамически загружать и управлять контентом из разных источников, например, когда учитель запускает событие навигации для отображения веб-страницы на экране класса. Однако многие веб-сайты явно блокируют встраивание в iframe с помощью заголовков безопасности, таких как X-Frame-Options и Content Security Policy (CSP). Кроме того, ограничения iframe не позволяют встраиваемым страницам напрямую управлять навигацией или поведением встраиваемого контента.

API Controlled Frame решает это ограничение, позволяя загружать любой веб-контент, даже если он применяет ограничительные политики встраивания. Этот API доступен исключительно в изолированных веб-приложениях (IWA) , которые включают дополнительные меры безопасности для защиты пользователей и разработчиков от потенциальных рисков.

Реализация контролируемых рамок

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

Добавить политику разрешений

Чтобы использовать контролируемые фреймы, включите соответствующее разрешение, добавив поле permissions_policy со значением "controlled-frame" в манифест IWA. Кроме того, включите ключ cross-origin-isolated . Этот ключ не является специфичным для контролируемых фреймов, но обязателен для всех IWA и определяет, может ли документ получать доступ к API, требующим изоляции между источниками.

{
   ...
  "permissions_policy": {
     ...
     "controlled-frame": ["self"],
     "cross-origin-isolated": ["self"]
     ...
  }
   ...
}

Ключcontrolled controlled-frame в манифесте изолированного веб-приложения (IWA) определяет список разрешений политики разрешений, указывающий, какие источники могут использовать контролируемые фреймы. Хотя манифест поддерживает полный синтаксис политики разрешений, допуская такие значения, как * , определенные источники или ключевые слова, такие как self и src , важно отметить, что специфичные для IWA API не могут быть делегированы другим источникам. Даже если список разрешений содержит подстановочные знаки или внешние источники, эти разрешения не вступят в силу для таких функций IWA, какcontrolled controlled-frame . В отличие от стандартных веб-приложений, IWA по умолчанию устанавливают все контролируемые политикой функции в значение none, что требует явного объявления. Для специфичных для IWA функций это означает, что функционально эффективны только такие значения, как self (собственное происхождение IWA) или src (источник встроенного фрейма).

Добавить элемент «Управляемая рамка»

Вставьте элемент <controlledframe> в HTML-код, чтобы встроить сторонний контент в ваш IWA.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

Дополнительный атрибут partition настраивает разбиение хранилища на разделы для встроенного контента, позволяя изолировать такие данные, как файлы cookie и локальное хранилище, для сохранения данных между сеансами.

Пример: раздел хранения в памяти

Создайте управляемый фрейм, используя раздел хранилища в памяти с именем "session1" . Данные, хранящиеся в этом разделе (например, файлы cookie и localStorage), будут удалены после уничтожения фрейма или завершения сеанса приложения.

<controlledframe id="controlledframe_1" src="https://example.com">
</controlledframe>

Пример: раздел постоянного хранения

Создайте управляемый фрейм, используя раздел постоянного хранилища с именем "user_data" . Префикс "persist:" гарантирует, что данные, хранящиеся в этом разделе, будут сохранены на диске и будут доступны в сеансах работы приложения.

<controlledframe id="frame_2" src="..." partition="persist:user_data">
</controlledframe>

Получить ссылку на элемент

Получите ссылку на элемент <controlledframe> , чтобы можно было взаимодействовать с ним как с любым стандартным элементом HTML:

const controlledframe = document.getElementById('controlledframe_1');

Частые сценарии и варианты использования

Как правило, следует выбирать технологию, наиболее соответствующую вашим потребностям, избегая при этом излишней сложности. В последние годы прогрессивные веб-приложения (PWA) сократили разрыв с нативными приложениями , обеспечивая мощный веб-интерфейс. Если веб-приложению требуется встраивать сторонний контент, рекомендуется сначала изучить стандартный подход с использованием <iframe> . Если требования превышают возможности iframe, управляемые фреймы на IWA могут стать оптимальной альтернативой. Типичные примеры использования описаны в следующих разделах.

Встраивание стороннего веб-контента

Многим приложениям необходима возможность загрузки и отображения стороннего контента в пользовательском интерфейсе. Однако при участии нескольких владельцев веб-приложений (что часто встречается в случае со встроенными приложениями) становится сложно установить согласованные сквозные политики. Например, настройки безопасности могут помешать традиционному <iframe> встраивать определённые типы контента, даже если у компании есть на это законные основания. В отличие от элементов <iframe> , управляемые фреймы разработаны для обхода этих ограничений, позволяя приложениям загружать и отображать контент, даже если стандартное встраивание явно запрещено.

Варианты использования

  • Презентации в классе : преподаватель использует сенсорный экран в классе для переключения между образовательными ресурсами, которые обычно блокируют встраивание iframe.
  • Цифровые вывески в розничных магазинах и торговых центрах : киоск в торговом центре циклично переключает веб-сайты разных магазинов. Управляемые фреймы обеспечивают корректную загрузку этих страниц, даже если они ограничивают встраивание.

Примеры кода

Следующие API-интерфейсы управляемых кадров полезны для управления встроенным содержимым.

Навигация : управляемые фреймы предоставляют несколько методов программного управления и контроля навигацией и историей навигации встроенного контента.

Атрибут src получает или задает URL-адрес содержимого, отображаемого во фрейме, функционируя так же, как атрибут HTML.

controlledframe.src = "https://example.com";

Метод back() возвращает вас на один шаг назад в истории фрейма. Возвращаемое обещание преобразуется в логическое значение, указывающее, был ли переход успешным.

document.getElementById('backBtn').addEventListener('click', () => {
controlledframe.back().then((success) => {
console.log(`Back navigation ${success ? 'succeeded' : 'failed'}`); }).catch((error) => {
   console.error('Error during back navigation:', error);
   });
});

Метод forward() перемещает фрейм на один шаг вперёд по истории. Возвращаемое обещание преобразуется в логическое значение, указывающее, был ли переход успешным.

document.getElementById('forwardBtn').addEventListener('click', () => {
controlledframe.forward().then((success) => {
   console.log(`Forward navigation ${success ? 'succeeded' : 'failed'}`);
}).catch((error) => {
    console.error('Error during forward navigation:', error);
  });
});

Метод reload() перезагружает текущую страницу в фрейме.

document.getElementById('reloadBtn').addEventListener('click', () => {
   controlledframe.reload();
});

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

  • loadstart : срабатывает при начале навигации в кадре.
  • loadcommit : срабатывает после обработки запроса на навигацию и начала загрузки основного содержимого документа.
  • contentload : срабатывает после завершения загрузки основного документа и его основных ресурсов (аналогично DOMContentLoaded).
  • loadstop : срабатывает, когда все ресурсы страницы (включая подфреймы и изображения) завершены.
  • loadabort : Срабатывает, если навигация прерывается (например, из-за действия пользователя или запуска другой навигации).
  • loadredirect : срабатывает, когда во время навигации происходит перенаправление на стороне сервера.
controlledframe.addEventListener('loadstart', (event) => {
   console.log('Navigation started:', event.url);
   // Example: Show loading indicator
 });
controlledframe.addEventListener('loadcommit', (event) => {
   console.log('Navigation committed:', event.url);
 });
controlledframe.addEventListener('contentload', (event) => {
   console.log('Content loaded for:', controlledframe.src);
   // Example: Hide loading indicator, maybe run initial script
 });
controlledframe.addEventListener('loadstop', (event) => {
   console.log('All resources loaded for:', controlledframe.src);
 });
controlledframe.addEventListener('loadabort', (event) => {
   console.warn(`Navigation aborted: ${event.url}, Reason: ${event.detail.reason}`);
 });
controlledframe.addEventListener('loadredirect', (event) => {
   console.log(`Redirect detected: ${event.oldUrl} -> ${event.newUrl}`);
});

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

  • dialog : активируется при попытке встроенного содержимого открыть диалоговое окно (оповещение, подтверждение, запрос). Вы получаете информацию и можете ответить.
  • consolemessage : срабатывает, когда сообщение регистрируется на консоли внутри кадра.
  • permissionrequest : активируется, когда встроенный контент запрашивает разрешение (например, геолокацию и уведомления). Вы получаете подробную информацию и можете разрешить или отклонить запрос.
  • newwindow : срабатывает, когда встроенный контент пытается открыть новое окно или вкладку (например, с помощью window.open или ссылки с target="_blank" ). Вы получаете подробную информацию и можете обработать или заблокировать это действие.
controlledframe.addEventListener('dialog', (event) => {
   console.log(Dialog opened: Type=${event.messageType}, Message=${event.messageText});
   // You will need to respond, e.g., event.dialog.ok() or .cancel()
 });

controlledframe.addEventListener('consolemessage', (event) => {
   console.log(Frame Console [${event.level}]: ${event.message});
 });

controlledframe.addEventListener('permissionrequest', (event) => {
   console.log(Permission requested: Type=${event.permission});
   // You must respond, e.g., event.request.allow() or .deny()
   console.warn('Permission request needs handling - Denying by default');
   if (event.request && event.request.deny) {
     event.request.deny();
   }
});

controlledframe.addEventListener('newwindow', (event) => {
   console.log(New window requested: URL=${event.targetUrl}, Name=${event.name});
   // Decide how to handle this, e.g., open in a new controlled frame and call event.window.attach(), ignore, or block
   console.warn('New window request needs handling - Blocking by default');
 });

Существуют также события изменения состояния, которые уведомляют вас об изменениях, связанных с собственным состоянием рендеринга контролируемого кадра, например об изменениях его размеров или уровня масштабирования.

  • sizechanged : срабатывает при изменении размеров содержимого фрейма.
  • zoomchange : срабатывает при изменении уровня масштабирования содержимого кадра.
controlledframe.addEventListener('sizechanged', (event) => {
  console.log(Frame size changed: Width=${event.width}, Height=${event.height});
});

controlledframe.addEventListener('zoomchange', (event) => {
  console.log(Frame zoom changed: Factor=${event.newZoomFactor});
});

Методы хранения : управляемые фреймы предлагают API для управления данными, хранящимися в разделе фрейма.

Используйте clearData() для удаления всех сохранённых данных, что особенно полезно для сброса фрейма после пользовательского сеанса или обеспечения чистого состояния. Метод возвращает Promise, который разрешается после завершения операции. Также можно указать дополнительные параметры конфигурации:

  • types : массив строк, определяющий типы данных, которые необходимо очистить (например, ['cookies', 'localStorage', 'indexedDB'] ). Если этот параметр пропущен, обычно очищаются все применимые типы данных.
  • options : управление процессом очистки, например указание временного диапазона с использованием свойства since (метка времени в миллисекундах с начала эпохи), чтобы очищать только данные, созданные после этого времени.

Пример: очистить все хранилище, связанное с контролируемым фреймом.

function clearAllPartitionData() {
   console.log('Clearing all data for partition:', controlledframe.partition);
   controlledframe.clearData()
     .then(() => {
       console.log('Partition data cleared successfully.');
     })
     .catch((error) => {
       console.error('Error clearing partition data:', error);
     });
}

Пример: удалить только файлы cookie и локальное хранилище, созданные за последний час.

function clearRecentCookiesAndStorage() {
   const oneHourAgo = Date.now() - (60 * 60 * 1000);
   const dataTypesArray = ['cookies', 'localStorage'];
   const dataTypesToClearObject = {};
   for (const type of dataTypesArray) {
      dataTypesToClearObject[type] = true;
   }
   const clearOptions = { since: oneHourAgo };
   console.log(`Clearing ${dataTypesArray.join(', ')} since ${new    Date(oneHourAgo).toISOString()}`); controlledframe.clearData(clearOptions, dataTypesToClearObject) .then(() => {
   console.log('Specified partition data cleared successfully.');
}).catch((error) => {
   console.error('Error clearing specified partition data:', error);
});
}

Расширить или изменить сторонние приложения

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

Варианты использования

  • Внедрение фирменного стиля на сторонних сайтах : внедрение пользовательских CSS и JavaScript во встроенные веб-сайты для применения единой визуальной темы.
  • Ограничить навигацию и поведение ссылок : перехватить или отключить определенное поведение тега <a> с помощью внедрения скрипта.
  • Автоматизируйте восстановление после сбоев или бездействия : отслеживайте встроенное содержимое на предмет состояний сбоя (например, пустой экран, ошибки скрипта) и программно перезагружайте или сбрасывайте сеанс после истечения времени ожидания.

Примеры кода

Внедрение скрипта : используйте executeScript() для внедрения JavaScript в управляемый фрейм, что позволяет настраивать поведение, добавлять наложения или извлекать данные со встроенных сторонних страниц. Вы можете предоставить встроенный код в виде строки или указать один или несколько файлов скрипта (используя относительные пути в пакете IWA). Метод возвращает обещание, которое преобразуется в результат выполнения скрипта — обычно это значение последнего оператора.

document.getElementById('scriptBtn').addEventListener('click', () => {
   controlledframe.executeScript({
      code: `document.body.style.backgroundColor = 'lightblue';
             document.querySelectorAll('a').forEach(link =>    link.style.pointerEvents = 'none');
             document.title; // Return a value
            `,
      // You can also inject files:
      // files: ['./injected_script.js'],
}) .then((result) => {
   // The result of the last statement in the script is usually returned.
   console.log('Script execution successful. Result (e.g., page title):', result); }).catch((error) => {
   console.error('Script execution failed:', error);
   });
});

Внедрение стиля : используйте insertCSS() для применения пользовательских стилей к страницам, загруженным в управляемый фрейм.

document.getElementById('cssBtn').addEventListener('click', () => {
  controlledframe.insertCSS({
    code: `body { font-family: monospace; }`
    // You can also inject files:
    // files: ['./injected_styles.css']
  })
  .then(() => {
    console.log('CSS injection successful.');
  })
  .catch((error) => {
    console.error('CSS injection failed:', error);
  });
});

Перехват сетевых запросов : используйте API WebRequest для наблюдения и потенциального изменения сетевых запросов со встроенной страницы, например, блокируя запросы, изменяя заголовки или ведя журнал использования.

// Get the request object
const webRequest = controlledframe.request;

// Create an interceptor for a specific URL pattern
const interceptor = webRequest.createWebRequestInterceptor({
  urlPatterns: ["*://evil.com/*"],
  blocking: true,
  includeHeaders: "all"
});

// Add a listener to block the request
interceptor.addEventListener("beforerequest", (event) => {
  console.log('Blocking request to:', event.url);
  event.preventDefault();
});

// Add a listener to modify request headers
interceptor.addEventListener("beforesendheaders", (event) => {
  console.log('Modifying headers for:', event.url);
  const newHeaders = new Headers(event.headers);
  newHeaders.append('X-Custom-Header', 'MyValue');
  event.setRequestHeaders(newHeaders);
});

Добавление пользовательских контекстных меню : используйте API contextMenus для добавления, удаления и обработки пользовательских контекстных меню во встроенном фрейме. В этом примере показано, как добавить пользовательское меню «Копировать выделенное» в управляемый фрейм. При выделении текста и щелчке правой кнопкой мыши появляется меню. При щелчке по нему выделенный текст копируется в буфер обмена, обеспечивая простое и удобное взаимодействие со встроенным содержимым.

const menuItemProperties = {
  id: "copy-selection",
  title: "Copy selection",
  contexts: ["selection"],
  documentURLPatterns: [new URLPattern({ hostname: '*.example.com'})]
};

// Create the context menu item using a promise
try {
  await controlledframe.contextMenus.create(menuItemProperties);
  console.log(`Context menu item "${menuItemProperties.id}" created successfully.`);
} catch (error) {
  console.error(`Failed to create context menu item:`, error);
}

// Add a standard event listener for the 'click' event
controlledframe.contextMenus.addEventListener('click', (event) => {
    if (event.menuItemId === "copy-selection" && event.selectionText) {
        navigator.clipboard.writeText(event.selectionText)
          .then(() => console.log("Text copied to clipboard."))
          .catch(err => console.error("Failed to copy text:", err));
    }
});

Демо

Ознакомьтесь с демонстрацией Controlled Frame для получения обзора методов Controlled Frames.

Демонстрация Controlled Frame

В качестве альтернативы IWA Kitchen Sink представляет собой приложение с несколькими вкладками, каждая из которых демонстрирует отдельный API IWA, например управляемые фреймы, прямые сокеты и многое другое.

Кухонная мойка IWA

Заключение

Управляемые фреймы предлагают мощный и безопасный способ встраивания, расширения и взаимодействия со сторонним веб-контентом в изолированных веб-приложениях (IWA). Обходя ограничения iframe, они предоставляют новые возможности, такие как выполнение скриптов внутри встроенного контента, перехват сетевых запросов и реализация настраиваемых контекстных меню, — и всё это при сохранении строгих границ изоляции. Однако, поскольку эти API обеспечивают глубокий контроль над встроенным контентом, они также накладывают дополнительные ограничения безопасности и доступны только в IWA, которые разработаны для обеспечения более строгих гарантий как для пользователей, так и для разработчиков. В большинстве случаев разработчикам следует сначала рассмотреть возможность использования стандартных элементов <iframe> , которые проще и достаточны во многих сценариях. Управляемые фреймы следует рассматривать, когда решения на основе iframe блокируются ограничениями встраивания или не обладают необходимыми возможностями управления и взаимодействия. Независимо от того, создаете ли вы киоск-интерфейс, интегрируете сторонние инструменты или проектируете модульные системы плагинов, управляемые фреймы обеспечивают детальное управление в структурированной, разрешенной и безопасной среде, что делает их важнейшим инструментом в следующем поколении современных веб-приложений.

Дополнительные ресурсы