百度生成手机网站,网站备案更改,制作公司网站有哪些,技能网站建设项目需求英文社区对 Webpack Module Federation 的响应非常热烈#xff0c;甚至被誉为“A game-changer in JavaScript architecture”#xff0c;相对而言国内对此热度并不高#xff0c;这一方面是因为 MF 强依赖于 Webpack5#xff0c;升级成本有点高#xff1b;另一方面是国内已…英文社区对 Webpack Module Federation 的响应非常热烈甚至被誉为“A game-changer in JavaScript architecture”相对而言国内对此热度并不高这一方面是因为 MF 强依赖于 Webpack5升级成本有点高另一方面是国内已经有一些成熟微前端框架例如 qiankun。不过我个人觉得 MF 有不少实用性强非常值得学习、使用的特性包括 应用可按需导出若干模块这些模块最终会被单独打成模块包功能上有点像 NPM 模块 应用可在运行时基于 HTTP(S) 协议动态加载其它应用暴露的模块且用法与动态加载普通 NPM 模块一样简单 与其它微前端方案不同MF 的应用之间关系平等没有主应用/子应用之分每个应用都能导出/导入任意模块 等等。
而对于微前端的解释大家可以查这篇文章。
简单示例
Module Federation 的基本逻辑是一端导出模块另一端导入、使用模块实现上两端都依赖于 Webpack 5 内置的 ModuleFederationPlugin 插件 对于模块生成方需要使用 ModuleFederationPlugin 插件的 expose 参数声明需要导出的模块列表 对于模块使用方需要使用 ModuleFederationPlugin 插件的 remotes 参数声明需要从哪些地方导入远程模块。
接下来我们按这个流程一步步搭建一个简单的 Webpack Module Federation 示例基本框架
//MF-basic
├─ app-1
│ ├─ dist
│ │ ├─ ...
│ ├─ package.json
│ ├─ src
│ │ ├─ main.js
│ │ ├─ foo.js
│ │ └─ utils.js
│ └─ webpack.config.js
├─ app-2
│ ├─ dist
│ │ ├─ ...
│ ├─ package.json
│ ├─ src
│ │ ├─ bootstrap.js
│ │ └─ main.js
│ ├─ webpack.config.js
├─ lerna.json
└─ package.json 提示为简化依赖管理示例引入 lerna 实现 Monorepo 策略不过这与文章主题无关这里不做过多介绍。 其中app-1、app-2 是两个独立应用分别有一套独立的 Webpack 构建配置类似于微前端场景下的“微应用”概念。在本示例中app-1 负责导出模块 —— 类似于子应用app-2 负责使用这些模块 —— 类似于主应用。 我们先看看模块导出方 —— 也就是 app-1 的构建配置
const path require(path);
const { ModuleFederationPlugin } require(webpack).container;module.exports {mode: development,devtool: false,entry: path.resolve(__dirname, ./src/main.js),output: {path: path.resolve(__dirname, ./dist),// 必须指定产物的完整路径否则使用方无法正确加载产物资源publicPath: http://localhost:8081/dist/,},plugins: [new ModuleFederationPlugin({// MF 应用名称name: app1,// MF 模块入口可以理解为该应用的资源清单filename: remoteEntry.js,// 定义应用导出哪些模块exposes: {./utils: ./src/utils,./foo: ./src/foo,},}),],// MF 应用资源提供方必须以 http(s) 形式提供服务// 所以这里需要使用 devServer 提供 http(s) server 能力devServer: {port: 8081,hot: true,},
};
作用模块导出方app-1 的配置逻辑可以总结为 需要使用 ModuleFederationPlugin 的 exposes 项声明哪些模块需要被导出使用 filename 项定义入口文件名称 需要使用 devServer 启动开发服务器能力。
使用 ModuleFederationPlugin 插件后Webpack 会将 exposes 声明的模块分别编译为独立产物并将产物清单、MF 运行时等代码打包进 filename 定义的应用入口文件(Remote Entry File)中。例如 app-1 经过 Webpack 编译后将生成如下产物
//MF-basic
├─ app-1
│ ├─ dist
│ │ ├─ main.js
│ │ ├─ remoteEntry.js
│ │ ├─ src_foo_js.js
│ │ └─ src_utils_js.js
│ ├─ src
│ │ ├─ ... main.js 为整个应用的编译结果此处可忽略 src_utils_js.js 与 src_foo_js.js 分别为 exposes 声明的模块的编译产物 remoteEntry.js 是 ModuleFederationPlugin 插件生成的应用入口文件包含模块清单、MF 运行时代码。 拓展1
devServer的配置项
devServer 是 Webpack 开发工具中的一个选项主要用于提供一个快速的本地开发服务器帮助开发人员在开发过程中更方便地构建和测试应用。使用 devServer 可以提高开发效率提供热更新功能并简化静态资源的提供。以下是 devServer 的一些关键特性和配置选项
关键特性 热模块替换 (Hot Module Replacement, HMR): 支持在运行时替换、添加或删除模块而无需完全刷新页面。这使得开发者可以看到改动的即时效果提高开发效率。 自动刷新页面: 当文件发生变化时devServer 可以自动刷新浏览器确保开发者总是能看到最新的代码效果。 自定义路由: 可以配置 devServer 的路由例如重定向请求到特定页面适用于单页应用SPA。 Proxy: 支持 API 代理允许将开发环境中的请求代理到其他服务器这在需要与后端 API 交互时非常有用。 错误页面: 可以定制错误页面方便开发者在出现问题时进行调试。
基本配置
在 Webpack 配置文件中通常在 module.exports 对象中添加 devServer 配置。以下是一个基本的配置示例
const path require(path);module.exports {// 其他配置...devServer: {contentBase: path.join(__dirname, dist), // 静态文件的目录compress: true, // 启用 gzip 压缩port: 9000, // 监听端口hot: true, // 启用热模块替换open: true, // 自动打开浏览器proxy: {/api: http://localhost:5000 // 代理 API 请求},historyApiFallback: true, // 处理所有 404 请求重定向到 index.html},
};
contentBase: 指定静态文件的根目录默认是 public 目录。compress: 布尔值启用 gzip 压缩。port: 指定开发服务器的端口。hot: 启用热模块替换。open: 在服务器启动后自动打开浏览器。proxy: 配置代理将请求代理到其他服务器适用于跨域请求。historyApiFallback: 启用后将所有 404 响应重定向到 index.html适合单页应用。 接下来继续看看模块导入方 —— 也就是 app-2 的配置方法
const path require(path);
const HtmlWebpackPlugin require(html-webpack-plugin);
const { ModuleFederationPlugin } require(webpack).container;module.exports {mode: development,devtool: false,entry: path.resolve(__dirname, ./src/main.js),output: {path: path.resolve(__dirname, ./dist),},plugins: [// 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境new ModuleFederationPlugin({// 使用 remotes 属性声明远程模块列表remotes: {// 地址需要指向导出方生成的应用入口文件RemoteApp: app1http://localhost:8081/dist/remoteEntry.js,},}),new HtmlWebpackPlugin(),],devServer: {port: 8082,hot: true,open: true,},
};
作用远程模块使用方app-2 需要使用 ModuleFederationPlugin 声明远程模块的 HTTP(S) 地址与模块名称(示例中的 RemoteApp)之后在 app-2 中就可以使用模块名称异步导入 app-1 暴露出来的模块例如
// app-2/src/main.js
(async () {const { sayHello } await import(RemoteApp/utils);sayHello();
})();
其中 remoteEntry.js 即 app-1 构建的应用入口文件 src_utils_js.js 则是 import(RemoteApp/utils) 语句导入的远程模块。
总结一下MF 中的模块导出/导入方都依赖于 ModuleFederationPlugin 插件其中导出方需要使用插件的 exposes 项声明导出哪些模块使用 filename 指定生成的入口文件导入方需要使用 remotes 声明远程模块地址之后在代码中使用异步导入语法 import(module) 引入模块。
这种模块远程加载、运行的能力搭配适当的 DevOps 手段已经足以满足微前端的独立部署、独立维护、开发隔离的要求在此基础上 MF 还提供了一套简单的依赖共享功能用于解决多应用间基础库管理问题。
拓展2
ModuleFederationPlugin 插件
ModuleFederationPlugin 是 Webpack 5 引入的一个强大功能旨在实现微前端架构Micro Frontends。它允许多个独立构建的应用程序或模块在运行时动态共享和加载彼此的代码从而实现更灵活的代码复用和跨团队协作。
核心概念 微前端将一个大型应用拆分成多个小型、独立的应用每个应用可以独立开发、部署和维护。 共享模块通过 ModuleFederationPlugin不同的应用可以共享某些公共依赖从而减少重复代码和整体包的大小。 动态加载应用可以在运行时加载其他应用的模块而无需在构建时将它们打包在一起。
使用方法
1. 配置主应用
首先我们需要在主应用Host中配置 ModuleFederationPlugin。以下是一个简单的配置示例
// webpack.config.js (主应用)
const ModuleFederationPlugin require(webpack/lib/container/ModuleFederationPlugin);
const path require(path);module.exports {mode: development,entry: ./src/index.js,output: {filename: main.js,path: path.resolve(__dirname, dist),publicPath: auto, // 适应于动态加载},plugins: [new ModuleFederationPlugin({name: app1, // 主应用的名称filename: remoteEntry.js, // 远程入口文件exposes: {// 指定要暴露的模块./Component: ./src/Component, },shared: {// 共享模块react: { singleton: true, eager: true },react-dom: { singleton: true, eager: true },},}),],
};
在这个示例中配置了一个名为 app1 的主应用暴露了一个名为 Component 的模块并且共享了 react 和 react-dom。
2. 配置远程应用
接下来我们需要配置一个远程应用Remote它将从主应用中加载模块。
// webpack.config.js (远程应用)
const ModuleFederationPlugin require(webpack/lib/container/ModuleFederationPlugin);
const path require(path);module.exports {mode: development,entry: ./src/index.js,output: {filename: remoteEntry.js,path: path.resolve(__dirname, dist),publicPath: auto,},plugins: [new ModuleFederationPlugin({name: app2, // 远程应用的名称filename: remoteEntry.js,remotes: {app1: app1http://localhost:3001/remoteEntry.js, // 指定主应用的远程入口},shared: {react: { singleton: true, eager: true },react-dom: { singleton: true, eager: true },},}),],
};
在这个示例中远程应用 app2 可以从主应用 app1 中加载暴露的模块。确保两个应用都在不同的端口运行例如app1 在 3001 端口app2 在 3002 端口然后你可以在 app2 中加载和使用 app1 中的模块。
3. 加载远程模块
在远程应用中我们可以使用动态导入来加载主应用暴露的模块。
// src/index.js (远程应用)
import(app1/Component).then((Component) {// 使用加载的组件const App Component.default;// 渲染组件
});
依赖共享
上例应用相互独立各自管理、打包基础依赖包但实际项目中应用之间通常存在一部分公共依赖 —— 例如 Vue、React、Lodash 等如果简单沿用上例这种分开打包的方式势必会出现依赖被重复打包造成产物冗余的问题为此 ModuleFederationPlugin 提供了 shared 配置用于声明该应用可被共享的依赖模块。
例如改造上例模块导出方 app-1 添加 shared 配置
module.exports {// ...plugins: [new ModuleFederationPlugin({name: app1,filename: remoteEntry.js,exposes: {./utils: ./src/utils,./foo: ./src/foo,}, // 可被共享的依赖模块shared: [lodash]}),],// ...
};
接下来还需要修改模块导入方 app-2添加相同的 shared 配置
module.exports {// ...plugins: [// 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境new ModuleFederationPlugin({// 使用 remotes 属性声明远程模块列表remotes: {// 地址需要指向导出方生成的应用入口文件RemoteApp: app1http://localhost:8081/dist/remoteEntry.js,},shared: [lodash]}),new HtmlWebpackPlugin(),],// ...
};
之后运行页面可以看到最终只加载了一次 lodash 产物而改动前则需要分别从导入/导出方各加载一次 lodash
添加 shared 后改动前
注意这里要求两个应用使用 版本号完全相同 的依赖才能被复用假设上例应用 app-1 用了 lodash4.17.0 而 app-2 用的是 lodash4.17.1Webpack 还是会同时加载两份 lodash 代码我们可以通过 shared.[lib].requiredVersion 配置项显式声明应用需要的依赖库版本来解决这个问题
module.exports {// ...plugins: [new ModuleFederationPlugin({// ...// 共享依赖及版本要求声明shared: {lodash: {requiredVersion: ^4.17.0,},},}),],// ...
};
上例 requiredVersion: ^4.17.0 表示该应用支持共享版本大于等于 4.17.0 小于等于 4.18.0 的 lodash其它应用所使用的 lodash 版本号只要在这一范围内即可复用。requiredVersion 支持 Semantic Versioning 2.0 标准这意味着我们可以复用 package.json 中声明版本依赖的方法。 requiredVersion 的作用在于限制依赖版本的上下限实用性极高。除此之外我们还可以通过 shared.[lib].shareScope 属性更精细地控制依赖的共享范围例如
module.exports {// ...plugins: [new ModuleFederationPlugin({// ...// 共享依赖及版本要求声明shared: {lodash: {// 任意字符串shareScope: foo},},}),],// ...
};
在这种配置下其它应用所共享的 lodash 库必须同样声明为 foo 空间才能复用。shareScope 在多团队协作时能够切分出多个资源共享空间降低依赖冲突的概率。
以下为我总结后的app1和app2webpack.config.js配置项
app1
const path require(path);
const { ModuleFederationPlugin } require(webpack).container;
const HtmlWebpackPlugin require(html-webpack-plugin);module.exports {mode: development,devtool: false,entry: {main: path.resolve(__dirname, ./src/main.js),},output: {path: path.resolve(__dirname, ./dist),publicPath: http://localhost:8081/dist/,filename: [name].js, // 初始文件名模板chunkFilename: src_[name]_js.js, // 动态分块文件名模板},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: {loader: babel-loader,options: {presets: [babel/preset-env, babel/preset-react],plugins: [babel/plugin-transform-runtime,babel/plugin-syntax-dynamic-import]}}}],},plugins: [new ModuleFederationPlugin({name: app1,filename: remoteEntry.js,exposes: {./utils: ./src/utils,./foo: ./src/foo,},shared: {lodash: {singleton: true, // 确保只有一个实例requiredVersion: ^4.17.21, // 指定所需的版本范围import: lodash, // 指定导入的模块路径strictVersion: false, // 是否严格匹配版本},},}),new HtmlWebpackPlugin(),],optimization: {splitChunks: {chunks: all,cacheGroups: {foo: {test: /[\\/]src[\\/]foo\.js$/,name: foo,chunks: all,},utils: {test: /[\\/]src[\\/]utils\.js$/,name: utils,chunks: all,},},},},devServer: {port: 8081,hot: true,},
};
app2
const path require(path);
const { ModuleFederationPlugin } require(webpack).container;
const HtmlWebpackPlugin require(html-webpack-plugin);module.exports {mode: development,devtool: false,entry: {main: path.resolve(__dirname, ./src/main.js),},output: {path: path.resolve(__dirname, ./dist),publicPath: http://localhost:8082/dist/, // 确保与 app2 的启动端口一致filename: [name].js,chunkFilename: src_[name]_js.js,},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: {loader: babel-loader,options: {presets: [babel/preset-env, babel/preset-react],plugins: [babel/plugin-transform-runtime,babel/plugin-syntax-dynamic-import]}}}],},plugins: [new ModuleFederationPlugin({name: app2,filename: remoteEntry.js,remotes: {app1: app1http://localhost:8081/dist/remoteEntry.js, // 指向 app1 的 remoteEntry.js},shared: {lodash: {singleton: true,requiredVersion: ^4.17.21,import: lodash,strictVersion: false,},}, // 如果需要共享库可以在这里配置}),new HtmlWebpackPlugin(),],optimization: {splitChunks: {chunks: all,cacheGroups: {// 可以根据需要添加缓存组},},},devServer: {port: 8082,hot: true,open: true,},
}; 拓展3
shared 配置
常见配置选项 singleton: 类型: boolean说明: 如果设置为 trueWebpack 会确保只会有一个实例被加载适用于像 React 这样的库它们通常应该只存在一个实例。 eager: 类型: boolean说明: 如果设置为 trueWebpack 会在应用加载时立即加载这个共享模块而不是在第一次使用时异步加载。这对于某些共享库如 React可能是有益的。 requiredVersion: 类型: string说明: 用于指定所需的版本范围。Webpack 会根据这个版本来判断是否可以共享该模块。如果不兼容则会引发警告或错误。 strictVersion: 类型: boolean说明: 如果设置为 true则要求共享模块的版本必须完全匹配 requiredVersion 指定的版本。这样可以避免因版本不匹配导致的问题。 import: 类型: string说明: 可以用于指定在其他应用中使用时共享模块的导入路径。例如你可以使用此配置来导入不同的模块路径。 shareScope: 类型: string说明: 用于指定共享模块的作用域通常在微前端架构中使用。
完整的微前端应用
Module Federation 是一种非常新的技术社区资料还比较少接下来我们来编写一个完整的微前端应用帮助你更好理解 MF 的功能与用法。微前端架构通常包含一个作为容器的主应用及若干负责渲染具体页面的子应用分别对标到下面示例的 packages/host 与 packages/order 应用
//MF-micro-fe
├─ packages
│ ├─ host
│ │ ├─ public
│ │ │ └─ index.html
│ │ ├─ src
│ │ │ ├─ App.js
│ │ │ ├─ HomePage.js
│ │ │ ├─ Navigation.js
│ │ │ ├─ bootstrap.js
│ │ │ ├─ index.js
│ │ │ └─ routes.js
│ │ ├─ package.json
│ │ └─ webpack.config.js
│ └─ order
│ ├─ src
│ │ ├─ OrderDetail.js
│ │ ├─ OrderList.js
│ │ ├─ main.js
│ │ └─ routes.js
│ ├─ package.json
│ └─ webpack.config.js
├─ lerna.json
└─ package.json 示例代码MF-micro-fe可以辅助阅读 先看看 order 对应的 MF 配置
module.exports {// ...plugins: [new ModuleFederationPlugin({name: order,filename: remoteEntry.js,// 导入路由配置exposes: {./routes: ./src/routes,},}),],
};
注意order 应用实际导出的是路由配置文件 routes.js。而 host 则通过 MF 插件导入并消费 order 应用的组件对应配置
module.exports {// ...plugins: [// 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境new ModuleFederationPlugin({// 使用 remotes 属性声明远程模块列表remotes: {// 地址需要指向导出方生成的应用入口文件RemoteOrder: orderhttp://localhost:8081/dist/remoteEntry.js,},})],// ...
};
注意order 应用实际导出的是路由配置文件 routes.js。而 host 则通过 MF 插件导入并消费 order 应用的组件对应配置
module.exports {// ...plugins: [// 模块使用方也依然使用 ModuleFederationPlugin 插件搭建 MF 环境new ModuleFederationPlugin({// 使用 remotes 属性声明远程模块列表remotes: {// 地址需要指向导出方生成的应用入口文件RemoteOrder: orderhttp://localhost:8081/dist/remoteEntry.js,},})],// ...
};
之后在 host 应用中引入 order 的路由配置并应用到页面中
import localRoutes from ./routes;
// 引入远程 order 模块
import orderRoutes from RemoteOrder/routes;const routes [...localRoutes, ...orderRoutes];const App () (React.StrictModeHashRouterh1Micro Frontend Example/h1Navigation /Routes{routes.map((route) (Routekey{route.path}path{route.path}element{React.Suspense fallback{.../}route.component //React.Suspense}exact{route.exact}/))}/Routes/HashRouter/React.StrictMode
);export default App;
通过这种方式一是可以将业务代码分解为更细粒度的应用形态二是应用可以各自管理路由逻辑降低应用间耦合性。最终能降低系统组件间耦合度更有利于多团队协作。除此之外MF 技术还有非常大想象空间国外有大神专门整理了一系列实用 MF 示例Module Federation Examples感兴趣的读者务必仔细阅读这些示例代码。
拓展4
什么是沙箱技术
“沙箱”是一种用于安全和隔离的技术通常用于运行不可信或潜在危险的代码而不影响主系统或其他应用程序的环境。在软件开发、网络安全和虚拟化等领域沙箱的概念被广泛应用。以下是沙箱的一些关键特征和应用场景
关键特征 隔离性: 沙箱环境与主系统或其他应用程序是隔离的。运行在沙箱中的代码无法直接访问主系统的资源确保主系统不会受到不良代码的影响。 安全性: 沙箱可以限制代码的权限和行为防止其执行恶意操作如访问敏感数据、修改系统文件等。 可控性: 在沙箱中运行的代码可以被监控和控制可以轻松地对其进行调试和分析。 资源限制: 沙箱可以限制代码的资源使用例如 CPU、内存和网络访问防止代码过度消耗系统资源。
应用场景 Web 浏览器: 现代浏览器使用沙箱技术来隔离网页和插件防止恶意网页对用户系统的攻击。例如浏览器的“安全沙箱”可以防止 JavaScript 访问本地文件系统。 虚拟机和容器: 通过虚拟机如 VMware、VirtualBox和容器如 Docker可以创建沙箱环境来运行应用程序。每个虚拟机或容器都是一个独立的执行环境提供了隔离和安全性。 移动应用: 移动操作系统如 Android 和 iOS使用沙箱来限制应用程序的访问权限确保一个应用程序无法影响其他应用程序或系统。 API 测试: 在开发和测试 API 时可以使用沙箱环境来模拟生产环境以安全地测试代码而不影响实际数据。 恶意软件分析: 安全研究人员使用沙箱环境分析和研究恶意软件的行为帮助识别威胁而不危及主系统的安全。
在微前端中的应用
在微前端架构中沙箱可以用于隔离不同微前端应用之间的代码和状态确保它们不会相互影响。虽然 Module Federation 本身没有内置的沙箱功能但可以借助其他技术如 iframe 或 Web Workers来实现沙箱效果增强微前端应用的安全性。
Module Federation 实现的微前端架构并未提供沙箱能力可能会造成一些问题如全局命名冲突、CSS 风格冲突、恶意代码注入、CSS 风格冲突等。而qiankun是拥有js沙箱技术的感兴趣的可以前往官网学习。