Frame controlado

Demián Renzulli
Demián Renzulli
Simon Hangl
Simon Hangl

O elemento <iframe> é usado normalmente para incorporar recursos externos em um contexto de navegação. Os iframes aplicam as políticas de segurança da Web isolando o conteúdo incorporado de origem cruzada da página host e vice-versa. Embora essa abordagem melhore a segurança ao garantir um limite seguro entre origens, ela limita alguns casos de uso. Por exemplo, os usuários podem precisar carregar e gerenciar conteúdo dinamicamente de diferentes fontes, como um professor acionando um evento de navegação para mostrar uma página da Web em uma tela da sala de aula. No entanto, muitos sites bloqueiam explicitamente a incorporação em iframes usando cabeçalhos de segurança, como X-Frame-Options e Política de Segurança de Conteúdo (CSP, na sigla em inglês). Além disso, as restrições de iframe impedem que as páginas de incorporação gerenciem diretamente a navegação ou o comportamento do conteúdo incorporado.

A API Controlled Frame resolve essa limitação permitindo o carregamento de qualquer conteúdo da Web, mesmo que ele aplique políticas de incorporação restritivas. Essa API está disponível exclusivamente em aplicativos da Web isolados (IWAs, na sigla em inglês), que incorporam medidas de segurança adicionais para proteger usuários e desenvolvedores de possíveis riscos.

Implementar frames controlados

Antes de usar um Controlled Frame, você precisa configurar uma IWA funcional. Depois, é possível integrar os frames controlados às suas páginas.

Adicionar política de permissões

Para usar frames controlados, ative a permissão correspondente adicionando um campo permissions_policy com o valor "controlled-frame" ao manifesto da IWA. Além disso, inclua a chave cross-origin-isolated. Essa chave não é específica para frames controlados, mas é necessária para todas as IWAs e determina se o documento pode acessar APIs que exigem isolamento de origem cruzada.

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

A chave controlled-frame em um manifesto de App Isolado da Web (IWA) define uma lista de permissões da política de permissões, especificando quais origens podem usar frames controlados. Embora o manifesto seja compatível com a sintaxe completa da Permissions Policy, permitindo valores como *, origens específicas ou palavras-chave como self e src, é crucial observar que as APIs específicas do IWA não podem ser delegadas a outras origens. Mesmo que a lista de permissões inclua um caractere curinga ou origens externas, essas permissões não vão entrar em vigor para recursos da IWA, como controlled-frame. Ao contrário dos apps da Web padrão, os IWAs definem todos os recursos controlados por política como "nenhum", exigindo declarações explícitas. Para recursos específicos da IWA, isso significa que apenas valores como self (a origem da própria IWA) ou src (a origem de um frame incorporado) são funcionalmente eficazes.

Adicionar um elemento de frame controlado

Insira um elemento <controlledframe> no HTML para incorporar conteúdo de terceiros na sua IWA.

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

O atributo opcional partition configura o particionamento de armazenamento para conteúdo incorporado, permitindo isolar dados como cookies e armazenamento local para manter dados em várias sessões.

Exemplo: partição de armazenamento na memória

Crie um frame controlado usando uma partição de armazenamento na memória chamada "session1". Os dados armazenados nessa partição (por exemplo, cookies e localStorage) serão limpos quando o frame for destruído ou a sessão do aplicativo terminar.

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

Exemplo: partição de armazenamento permanente

Crie um frame controlado usando uma partição de armazenamento permanente chamada "user_data". O prefixo "persist:" garante que os dados armazenados nessa partição sejam salvos em disco e estejam disponíveis em todas as sessões do aplicativo.

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

Receber referência de elemento

Receba uma referência ao elemento <controlledframe> para interagir com ele como qualquer elemento HTML padrão:

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

Cenários e casos de uso frequentes

Como regra geral, escolha a melhor tecnologia para atender às suas necessidades e evite complexidade desnecessária. Nos últimos anos, os Progressive Web Apps (PWAs) diminuíram a diferença em relação aos apps nativos, permitindo experiências poderosas na Web. Se um aplicativo da Web precisar incorporar conteúdo de terceiros, é recomendável primeiro explorar a abordagem regular de <iframe>. Se os requisitos excederem as capacidades dos iframes, os frames controlados em IWAs podem ser a melhor alternativa. Os casos de uso comuns são descritos nas seções a seguir.

Incorporar conteúdo da Web de terceiros

Muitos aplicativos precisam carregar e mostrar conteúdo de terceiros na interface do usuário. No entanto, quando vários proprietários de apps da Web estão envolvidos, o que é comum em aplicativos incorporados, fica difícil estabelecer políticas consistentes de ponta a ponta. Por exemplo, as configurações de segurança podem impedir que um <iframe> tradicional incorpore determinados tipos de conteúdo, mesmo quando as empresas têm uma necessidade legítima de fazer isso. Ao contrário dos elementos <iframe>, os frames controlados são projetados para ignorar essas restrições, permitindo que os aplicativos carreguem e mostrem conteúdo mesmo que ele proíba explicitamente a incorporação padrão.

Casos de uso

  • Apresentações em sala de aula: um professor usa uma tela sensível ao toque em sala de aula para alternar entre recursos educacionais que normalmente bloqueiam a incorporação de iframes.
  • Sinalização digital em lojas ou shoppings: um quiosque de shopping exibe sites de diferentes lojas. Os frames controlados garantem que essas páginas sejam carregadas corretamente, mesmo que restrinjam a incorporação.

Amostras de código

As APIs Controlled Frame a seguir são úteis para gerenciar conteúdo incorporado.

Navegação: os frames controlados oferecem vários métodos para gerenciar e controlar programaticamente a navegação e o histórico de navegação do conteúdo incorporado.

O atributo src recebe ou define o URL do conteúdo exibido no frame, funcionando da mesma forma que o atributo HTML.

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

O método back() volta uma etapa no histórico do frame. A promessa retornada é resolvida como um booleano que indica se a navegação foi concluída.

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

O método forward() avança uma etapa no histórico do frame. A promessa retornada é resolvida como um booleano que indica se a navegação foi bem-sucedida.

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

O método reload() recarrega a página atual no frame.

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

Além disso, os frames controlados fornecem eventos que permitem rastrear todo o ciclo de vida das solicitações de navegação, desde a iniciação e os redirecionamentos até o carregamento, conclusão ou interrupção do conteúdo.

  • loadstart: disparado quando uma navegação começa no frame.
  • loadcommit: disparado quando a solicitação de navegação é processada e o conteúdo do documento principal começa a ser carregado.
  • contentload: disparado quando o documento principal e os recursos essenciais terminam de carregar (semelhante a DOMContentLoaded).
  • loadstop: disparado quando todos os recursos da página (incluindo subframes, imagens) terminam de carregar.
  • loadabort: disparado se uma navegação for cancelada (por exemplo, por ação do usuário ou início de outra navegação).
  • loadredirect: disparado quando um redirecionamento do lado do servidor ocorre durante a navegação.
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}`);
});

Você também pode monitorar e interceptar interações ou solicitações específicas iniciadas pelo conteúdo carregado no frame controlado, como tentativas de abrir caixas de diálogo, solicitar permissões ou abrir novas janelas.

  • dialog: disparado quando o conteúdo incorporado tenta abrir uma caixa de diálogo (alerta, confirmação, solicitação). Você recebe detalhes e pode responder.
  • consolemessage: disparado quando uma mensagem é registrada no console dentro do frame.
  • permissionrequest: disparado quando o conteúdo incorporado solicita uma permissão (por exemplo, geolocalização e notificações). Você recebe detalhes e pode permitir ou negar a solicitação.
  • newwindow: disparado quando o conteúdo incorporado tenta abrir uma nova janela ou guia (por exemplo, com window.open ou um link com target="_blank"). Você recebe detalhes e pode processar ou bloquear a ação.
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');
 });

Há também eventos de mudança de estado que notificam você sobre mudanças relacionadas ao estado de renderização do próprio frame controlado, como modificações nas dimensões ou no nível de zoom.

  • sizechanged: disparado quando as dimensões do conteúdo do frame mudam.
  • zoomchange: disparado quando o nível de zoom do conteúdo do frame muda.
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});
});

Métodos de armazenamento: os Controlled Frames oferecem APIs para gerenciar dados armazenados em uma partição de frame.

Use clearData() para remover todos os dados armazenados, o que é especialmente útil para redefinir o frame após uma sessão do usuário ou garantir um estado limpo. O método retorna uma promessa que é resolvida quando a operação é concluída. Também é possível fornecer opções de configuração opcionais:

  • types: uma matriz de strings que especifica quais tipos de dados limpar (por exemplo, ['cookies', 'localStorage', 'indexedDB']). Se omitido, todos os tipos de dados aplicáveis geralmente são limpos.
  • options: controla o processo de limpeza, como especificar um período usando uma propriedade "since" (carimbo de data/hora em milissegundos desde o período) para limpar apenas os dados criados após esse horário.

Exemplo: limpar todo o armazenamento associado ao Controlled Frame

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

Exemplo: limpar apenas cookies e localStorage criados na última hora

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

Estender ou alterar aplicativos de terceiros

Além da incorporação simples, os frames controlados oferecem mecanismos para que o IWA de incorporação exerça controle sobre o conteúdo da Web de terceiros incorporado. É possível executar scripts no conteúdo incorporado, interceptar solicitações de rede e substituir menus de contexto padrão, tudo em um ambiente seguro e isolado.

Casos de uso

  • Impor a marca em sites de terceiros: injete CSS e JavaScript personalizados em sites incorporados para aplicar um tema visual unificado.
  • Restringir a navegação e o comportamento do link: interceptar ou desativar determinados comportamentos de tags <a> com a injeção de script.
  • Automatizar a recuperação após falhas ou inatividade: monitore o conteúdo incorporado para estados de falha (por exemplo, tela em branco, erros de script) e recarregue ou redefina a sessão de forma programática após um tempo limite.

Amostras de código

Injeção de script: use executeScript() para injetar JavaScript no frame controlado, permitindo personalizar o comportamento, adicionar sobreposições ou extrair dados de páginas incorporadas de terceiros. Você pode fornecer código inline como uma string ou referenciar um ou mais arquivos de script (usando caminhos relativos no pacote IWA). O método retorna uma promessa que é resolvida como o resultado da execução do script, geralmente o valor da última instrução.

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

Injeção de estilo: use insertCSS() para aplicar estilos personalizados a páginas carregadas em um frame controlado.

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

Interceptação de solicitações de rede: use a API WebRequest para observar e modificar solicitações de rede da página incorporada, como bloquear solicitações, alterar cabeçalhos ou registrar o uso.

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

Adicionar menus de contexto personalizados: use a API contextMenus para adicionar, remover e processar menus personalizados de clique com o botão direito do mouse no frame incorporado. Este exemplo mostra como adicionar um menu personalizado "Copiar seleção" em um frame controlado. Quando um texto é selecionado e o usuário clica com o botão direito do mouse, o menu aparece. Ao clicar nele, o texto selecionado é copiado para a área de transferência, permitindo interações simples e fáceis de usar no conteúdo incorporado.

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

Demonstração

Confira a demonstração do Controlled Frame para uma visão geral dos métodos de Controlled Frames.

A demonstração do Controlled Frame

Como alternativa, o IWA Kitchen Sink apresenta um app com várias guias, cada uma demonstrando uma API IWA diferente, como Controlled Frames, Direct Sockets e muito mais.

IWA Kitchen Sink

Conclusão

Os frames controlados oferecem uma maneira eficiente e segura de incorporar, estender e interagir com conteúdo da Web de terceiros em Apps Isolados da Web (AIWs). Ao superar as limitações dos iframes, eles permitem novos recursos, como executar scripts dentro de conteúdo incorporado, interceptar solicitações de rede e implementar menus de contexto personalizados, tudo isso mantendo limites de isolamento estritos. No entanto, como essas APIs oferecem controle total sobre o conteúdo incorporado, elas também vêm com restrições de segurança adicionais e só estão disponíveis em IWAs, que são projetados para impor garantias mais fortes para usuários e desenvolvedores. Para a maioria dos casos de uso, os desenvolvedores devem primeiro considerar o uso de elementos <iframe> padrão, que são mais simples e suficientes em muitos cenários. Os frames controlados devem ser avaliados quando as soluções baseadas em iframe são bloqueadas por restrições de incorporação ou não têm os recursos de controle e interação necessários. Seja para criar experiências em quiosques, integrar ferramentas de terceiros ou projetar sistemas de plug-ins modulares, os frames controlados permitem um controle refinado em um ambiente estruturado, com permissão e seguro. Isso os torna uma ferramenta essencial na próxima geração de aplicativos da Web avançados.

Mais recursos