阅读视图

发现新文章,点击刷新页面。

前端-通信机制

业务开发中,通信可能会涉及到同源与跨域的场景,Web开发中,同源策略(协议+域名+端口一致)是保障用户信息安全的核心机制。但业务中常需实现页面间通信,本文提供了三种主流方案:同源高效的BroadcastChannel、基于StorageEvent的跨标签页同步、跨域安全的postMessage

一、BroadcastChannel:同源页面间的广播站

核心特性

  • 严格遵循同源策略,不同源页面自动隔离
  • 发布-订阅模式,一对多通信
  • 频道名称在同源内唯一,跨页面同名频道自动关联

代码示例

// 页面A(商品详情页)
const productChannel = new BroadcastChannel('product_updates');
productChannel.postMessage({
  type: 'PRICE_UPDATE',
  data: { sku: 'SKU-123', price: 199 }
});

// 页面B(购物车页面)
const cartChannel = new BroadcastChannel('product_updates');
cartChannel.addEventListener('message', (e) => {
  if (e.data.type === 'PRICE_UPDATE') {
    updateCartItem(e.data.data.sku, e.data.data.price);
  }
});

关键要点

  1. 必须使用new BroadcastChannel(channelName)创建同名频道
  2. 消息建议包含type字段作为标识符,便于接收方路由处理
  3. 浏览器自动管理连接,无需手动维护窗口引用

二、StorageEvent:跨标签页的状态同步

触发条件

  • 必须由不同标签页/窗口触发
  • 必须通过localStorage.setItem()/removeItem()/clear()修改
  • 同一标签页内的修改不会触发事件

代码示例

// 页面A(主题设置页)
document.getElementById('theme-btn').addEventListener('click', () => {
  const darkMode = !JSON.parse(localStorage.getItem('darkMode'));
  localStorage.setItem('darkMode', JSON.stringify(darkMode)); // 触发事件
});

// 页面B(所有页面)
window.addEventListener('storage', (e) => {
  if (e.key === 'darkMode') {
    applyTheme(JSON.parse(e.newValue));
  }
});

调试技巧

  • 使用window.open()打开测试窗口确保同源
  • 在控制台检查e.url确认触发来源
  • 避免使用sessionStorage(仅当前标签页有效)

三、跨域通信:postMessage实践

示例代码

// 父页面(https://main.com)
const iframe = document.createElement('iframe');
iframe.src = 'https://trusted-subdomain.com/widget';
document.body.appendChild(iframe);

// iframe.contentWindow,子页面window发送事件
iframe.contentWindow.postMessage({
  type: 'INIT_WIDGET',
  apiKey: 'ABC123'
}, 'https://trusted-subdomain.com');

// 父页面监听子页面e.source.postMessage
window.addEventListener('message', (e) => {
 
});

// iframe页面(https://trusted-subdomain.com),子页面监听
window.addEventListener('message', (e) => {
  if (e.origin !== 'https://main.com') return;
  
  if (e.data.type === 'INIT_WIDGET') {
    initWidget(e.data.apiKey);
    // e.source父页面的windowd对象
    e.source.postMessage({ status: 'READY' }, e.origin);
  }
});

以上代码是基于iframe嵌套的跨域页面实现的,也可以基于windowNew = window.open(url),即多个window跨域窗口通信,本质上获取window对象是关键

安全要点

  1. 永远不要使用targetOrigin: '*'(生产环境)
  2. 消息数据应包含类型字段便于路由
  3. 使用e.source而非直接操作window.opener
  4. 敏感数据需加密传输

runtime chunk 到底是什么?

runtime chunk(运行时代码块)是 Webpack 生成的一小段核心代码,它不包含你的业务逻辑,而是负责:

  • 管理模块之间的依赖关系(比如哪个模块对应哪个文件);
  • 加载和执行打包后的模块(比如异步加载 chunk);
  • 维护模块的缓存和版本映射。

实操案例:从零看 runtime chunk 的生成

1. 准备极简项目结构

plaintext

├── src
│   ├── index.js       # 主入口
│   └── utils.js       # 工具模块
├── package.json
└── webpack.config.js

2. 编写业务代码

javascript

运行

// src/utils.js
export const add = (a, b) => a + b;

javascript

运行

// src/index.js
// 同步引入 + 异步引入,触发Webpack的模块管理逻辑
import { add } from './utils.js';
console.log('同步调用:', add(1, 2));

// 异步引入(关键:会让runtime逻辑更明显)
setTimeout(() => {
  import('./async-module.js').then(({ sayHello }) => {
    sayHello();
  });
}, 1000);

javascript

运行

// src/async-module.js
export const sayHello = () => console.log('异步模块加载成功!');

3. Webpack 配置(默认开启 runtime chunk)

javascript

运行

// webpack.config.js
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].bundle.js', // 主bundle
    chunkFilename: '[name].chunk.js', // 异步chunk
    path: path.resolve(__dirname, 'dist'),
  },
  optimization: {
    // 默认值为'single':将runtime提取为单独的chunk
    runtimeChunk: 'single', 
    splitChunks: {
      chunks: 'all', // 分割同步/异步chunk
    },
  },
};

4. 执行打包 & 查看输出

运行 npx webpack 后,dist 目录会生成 3 个文件:

plaintext

dist/
├── runtime.bundle.js   # runtime chunk(核心!)
├── main.bundle.js      # 主业务chunk(index + utils)
└── async-module.chunk.js # 异步chunk

5. 核心:runtime.bundle.js 里有什么?

打开 runtime.bundle.js,核心内容(简化后)如下:

javascript

运行

// runtime的核心逻辑:模块映射 + 加载器
(() => {
  // 1. 模块ID和文件路径的映射表(关键)
  const moduleMap = {
    "./src/async-module.js": () => import("./async-module.chunk.js"),
  };

  // 2. 模块加载器:处理异步import的核心逻辑
  window.__webpack_require__.e = (chunkId) => {
    // 加载对应的chunk文件(比如async-module.chunk.js)
    // 处理模块缓存、依赖解析
  };

  // 3. 模块缓存管理:避免重复加载
  const installedModules = {};
  window.__webpack_require__.c = installedModules;
})();

对比:禁用 runtime chunk 的效果

修改 Webpack 配置,将 runtimeChunk: false,重新打包后:

  • dist 目录只有 2 个文件:main.bundle.jsasync-module.chunk.js
  • main.bundle.js 里会包含原本 runtime.bundle.js 的所有逻辑(模块映射、加载器等);
  • 此时 main.bundle.js = 业务代码 + runtime 代码(即多个 chunk 合并到一个 bundle,对应上一轮你问的场景)。

为什么要单独提取 runtime chunk?

这是最关键的实战价值,用一个场景说明:假设你只修改了 utils.js 里的 add 函数(比如改成 a + b + 1),重新打包后:

  • main.bundle.js 的内容变了 → hash 值会变;
  • async-module.chunk.js 没改 → hash 值不变;
  • runtime.bundle.js 里的模块映射表没改 → hash 值不变;

用户浏览器缓存中:

  • 只会重新加载 main.bundle.jsruntime.bundle.jsasync-module.chunk.js 会复用缓存;

如果不提取 runtime chunk,main.bundle.js 包含 runtime 逻辑,哪怕只改一行业务代码,整个 main.bundle.js 的 hash 都变,用户需要重新加载全部内容 → 缓存失效,性能变差。


总结

  1. runtime chunk 本质:Webpack 的 “模块调度器”,包含模块映射、加载逻辑、缓存管理,无业务代码;
  2. 核心作用:管理打包后模块的加载和依赖,单独提取可提升缓存复用率;
  3. 表现形式:默认会生成单独的 runtime.bundle.js,禁用则合并到主 bundle 中(多 chunk→单 bundle)。

webpack的工作原理

Webpack 是一个**模块打包器**,核心工作是把项目中散落的、各种格式的模块(JS/TS/CSS/ 图片等),按照依赖关系打包成浏览器能识别的静态资源。
❌