受控框架

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

<iframe> 元素通常用于在浏览上下文中嵌入外部资源。iframe 通过将跨源嵌入内容与宿主网页隔离来强制执行 Web 的安全政策,反之亦然。虽然这种方法通过确保来源之间的安全边界来提高安全性,但会限制某些使用情形。例如,用户可能需要动态加载和管理来自不同来源的内容,例如教师触发导航事件以在教室屏幕上显示网页。不过,许多网站会使用 X-Frame-Options 和内容安全政策 (CSP) 等安全标头明确禁止在 iframe 上嵌入内容。此外,iframe 限制可防止嵌入网页直接管理嵌入内容的导航或行为。

Controlled Frame API 通过允许加载任何 Web 内容(即使该内容强制执行限制性嵌入政策)来解决此限制。此 API 仅在隔离 Web 应用 (IWA) 中提供,其中包含额外的安全措施,可保护用户和开发者免受潜在风险的侵害。

实现受控框架

在使用受控框架之前,您需要设置一个可正常运行的 IWA。然后,您可以将受控框架集成到网页中。

添加权限政策

如需使用受控帧,请在 IWA 清单中添加值为 "controlled-frame"permissions_policy 字段,以启用相应权限。此外,还包括 cross-origin-isolated 键。此键并非特定于受控框架,但所有 IWA 都需要此键,并且此键可确定文档是否可以访问需要跨源隔离的 API。

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

独立式 Web 应用 (IWA) 清单中的 controlled-frame 键用于定义权限政策许可名单,指定哪些来源可以使用受控框架。虽然清单支持完整的权限政策语法(允许使用 * 等值、特定来源或 selfsrc 等关键字),但务必要注意,IWA 特定的 API 无法委托给其他来源。即使许可名单包含通配符或外部来源,这些权限也不会对 controlled-frame 等 IWA 功能生效。与标准 Web 应用不同,IWA 默认将所有受政策控制的功能设置为“无”,因此需要明确声明。对于 IWA 特有的功能,这意味着只有 self(IWA 自身的来源)或 src(嵌入式框架的来源)等值在功能上有效。

添加受控框架元素

在 HTML 中插入 <controlledframe> 元素,以在 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');

常见场景和使用情形

一般来说,请选择最能满足您需求的技术,同时避免不必要的复杂性。近年来,渐进式 Web 应用 (PWA) 缩小了与原生应用的差距,从而实现了强大的 Web 体验。如果 Web 应用需要嵌入第三方内容,建议先探索常规的 <iframe> 方法。如果需求超出 iframe 的功能范围,那么 IWA 上的受控框架可能是最佳替代方案。以下部分介绍了常见应用场景。

嵌入第三方网页内容

许多应用都需要能够在自己的界面中加载和显示第三方内容。不过,如果涉及多个 Web 应用所有者(嵌入式应用的常见情况),则很难建立一致的端到端政策。例如,安全设置可能会阻止传统 <iframe> 嵌入某些类型的内容,即使企业有正当理由这样做也是如此。与 <iframe> 元素不同,受控框架旨在绕过这些限制,即使应用明确禁止标准嵌入,也能加载和显示内容。

使用场景

  • 课堂演示:教师使用教室触屏在通常会阻止 iframe 嵌入的教育资源之间切换。
  • 零售店或购物中心内的数字标牌:购物中心内的自助服务终端会轮流显示不同商店的网站。受控框架可确保即使这些网页限制嵌入,也能正确加载。

代码示例

以下 Controlled Frame API 有助于管理嵌入式内容。

导航:受控框架提供多种方法,以编程方式管理和控制嵌入式内容的导航和导航历史记录。

src 属性用于获取或设置框架中显示的内容的网址,其功能与 HTML 属性相同。

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

back() 方法会在框架的历史记录中向后导航一步。返回的 promise 会解析为一个布尔值,指示导航是否成功。

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() 方法会在框架的历史记录中向前导航一步。返回的 promise 会解析为一个布尔值,指示导航是否成功。

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,该 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 和 localStorage

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 可以控制嵌入的第三方 Web 内容。您可以在嵌入式内容中执行脚本、拦截网络请求和替换默认的上下文菜单,所有这些操作都在安全的隔离环境中进行。

使用场景

  • 在第三方网站上强制执行品牌推广:将自定义 CSS 和 JavaScript 注入嵌入式网站,以应用统一的视觉主题。
  • 限制导航和链接行为:通过脚本注入来拦截或停用某些 <a> 代码行为。
  • 在崩溃或不活动后自动恢复:监控嵌入式内容是否存在故障状态(例如,屏幕空白、脚本错误),并在超时后以编程方式重新加载或重置会话。

代码示例

脚本注入:使用 executeScript() 将 JavaScript 注入受控框架,让您可以自定义行为、添加叠加层或从嵌入的第三方网页中提取数据。您可以提供内嵌代码(以字符串形式)或引用一个或多个脚本文件(使用 IWA 软件包中的相对路径)。该方法会返回一个 promise,该 promise 会解析为脚本执行的结果(通常是最后一个语句的值)。

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

网络请求拦截:使用 WebRequest API 观察并可能修改嵌入式网页的网络请求,例如阻止请求、更改标头或记录使用情况。

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

添加自定义上下文菜单:使用 contextMenus API 在嵌入式框架内添加、移除和处理自定义右键点击菜单。此示例展示了如何在受控框架内添加自定义的“复制所选内容”菜单。当用户选择文本并右键点击时,系统会显示菜单。点击该按钮会将所选文本复制到剪贴板,从而在嵌入式内容中实现简单易用的互动。

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

演示

如需大致了解受控帧的方法,请查看受控帧演示

受控框架演示

或者,IWA Kitchen Sink 是一款具有多个标签页的应用,每个标签页都展示了不同的 IWA API,例如受控框架、直接套接字等。

IWA Kitchen Sink

总结

受控框架提供了一种强大而安全的方式,可在独立式 Web 应用 (IWA) 中嵌入、扩展和与第三方 Web 内容互动。通过克服 iframe 的限制,它们实现了新功能,例如在嵌入式内容中执行脚本、拦截网络请求和实现自定义上下文菜单,同时保持严格的隔离边界。不过,由于这些 API 可对嵌入式内容进行深度控制,因此还附带额外的安全限制,并且仅在 IWA 中可用,而 IWA 旨在为用户和开发者提供更强的保证。对于大多数使用情形,开发者应首先考虑使用标准 <iframe> 元素,这些元素更简单,并且在许多场景中都足够使用。当基于 iframe 的解决方案因嵌入限制而被阻止或缺乏必要的控制和互动功能时,应评估受控框架。无论您是构建信息亭体验、集成第三方工具,还是设计模块化插件系统,受控框架都能在结构化、许可式和安全的环境中实现精细控制,因此是下一代高级 Web 应用中的关键工具。

更多资源