译自 https://medium.com/@sercaneraslan/micro-frontend-architecture-with-webpack-module-federation-part-1-9827d436bd1e
书名译者Sercan Eraslan
大背景
他们有两个用React撰写的管理工作液晶,在这里他们追踪和管理工作Trendyol GO(Hızlı Market和Trendyol Yemek)中大部份期的订货。单个库房较好地满足用户了他们的期许,即使一开始他们是惟一的工程项目组,雇员数目不多。
单个的库房并不常常坏的。假如它能满足用户你的期许,假如它不能给你带来消极影响,那么你能采用各种方式。重要的是;找出以最有效率的方式达到你的最终目标的方式。
约1.5年后,当他们的老将数目显得足够多多时,他们以应用领域驱动力结构设计(DDD)的经营理念将他们的工程项目组分割为数个小工程项目组。在这一点上,他们必须结构设计微后端的结构,使每一工程项目组能分立合作开发应用应用领域。
我之前在几个不同的工程项目过微后端的实战经验。我也曾他们结构设计过两个微后端构架,但研究大部份其他的代替计划并优先选择最能满足用户他们市场需求的计划是更科学合理的。他们积极探索了大部份的代替计划,取舍了每一计划的异同(我不能在这首诗中谈及大部份的代替计划,即使那是另两个热门话题。),在评估结果的最后,他们发现Webpack 组件联邦政府能较好地满足用户他们的市场需求。
为什么是Webpack组件联邦政府?
当他们研究了大部份的代替计划后,所致下列原因,优先选择Webpack组件联邦政府枭女。
没保护生产成本(假如你他们创建两个构架,会有保护生产成本)没工程项目组某一的自学生产成本(假如你他们创建两个构架,会有自学生产成本)向组件联邦政府过渡期的生产成本极小不须要对每一工程项目进行重新构架大部份的市场需求都在构筑时得到满足用户在运转时不须要附加的工作撷取倚赖的生产效率高库/架构分立你不须要处置大部份的填充和内存难题你不须要处置路由器难题Shell和Micro Apps不是紧谐振的,而是松谐振的怎么采用组件联邦政府有下列四种形式:
搜索引擎通过这种方式,你能创建尽可能多的微型后端(应用应用领域程序),并通过Shell App管理工作完全分立的域。例如,想象一下,在Shell App两个菜单,当链接被点击时,它将在右边带出相关的应用应用领域程序。
image.png
2.微件
通过这种方式,你能从任何应用应用领域程序中添加任何微件/组件(即一小段代码)到任何应用应用领域程序。你能在产品应用应用领域中的用户应用应用领域中公开UserDetail组件。
image.png
混合型你能同时采用第一和第二种方式。
开始课堂教学
首先创建两个app 命名为shell,并且以相同的方式创建应用应用领域product & user
npx create-react-app shellcd shellyarn add webpack webpack-cli webpack-server html-webpack-plugin css-loader style-loader babel-loader webpack-dev-server第一步:初始化工程项目import React from react;import reactDOM from react-dom;import App from ./App;ReactDOM.render(document.getElementById(root)第二步:配置webpackconst HtmlWebpackPlugin = require(html-webpack-plugin);const ModuleFederationPlugin = require(webpack/lib/container/ModuleFederationPlugin);const deps = require(./package.json).dependencies;module.exports = {mode: development,devServer: {port: 3001,},module: {rules: [test: /.js?$/,exclude: /node_modules/,loader: babel-loader,options: {presets: [@babel/preset-env,@babel/preset-react,],},},test: /.css$/i,use: [“style-loader”, “css-loader”],},],},plugins: [new ModuleFederationPlugin(name: SHELL,filename: remoteEntry.js,shared: […deps,react: { requiredVersion: deps.react, singleton: true },react-dom: {requiredVersion: deps[react-dom],singleton: true,},},],),new HtmlWebpackPlugin({template:./public/index.html,}),],
其中关于组件联邦政府的配置项,解释如下:
name: 他们用它来确定应用应用领域程序的名称。他们将通过这个名称与其他应用应用领域程序进行交流。filename: 他们用它作为两个入口文件。在这个例子中,其他应用应用领域程序将能够通过输入 “SHELL@http://localhost:3001/remoteEntry.js “访问SHELL应用应用领域程序。shared(共享)。他们用它来指定这个应用应用领域程序将与其他应用应用领域程序共享哪些倚赖。这里须要注意的是 “singleton: true”。假如你不写 “singleton: true”,每一应用应用领域程序将在两个单独的React实例上运转把同样的文件复制到User和 Product工程项目,但不要忘记增加端口和改变名称字段。
第三步:结构设计// app.jsimport React from react;import ./App.css;const App = () => (
Hi from Shell App
);export default App;// app.css.shell-app {margin: 5px;text-align: center;background: #FFF3E0;border: 1px dashed #FFB74D;border-radius: 5px;color: #FFB74D;
上面两个文件都放到src下,Product以及User工程项目也做同样的更改,只是将src命名为shell。 三个工程项目依次运转下列的命令
yarn webpack server
image.png
现在,他们大部份的应用应用领域程序都为Micro Frontends构架做好了准备,并且能相互分立运转。
第四步:整合
是时候提到组件联邦政府的两个伟大的功能了 🙂
expose:它允许你从任何应用应用领域程序到另两个应用应用领域程序共享两个组件、两个页面或整个应用应用领域程序。你所暴露的一切都被创建为两个单独的构筑,从而创造了两个自然的tree shaking。每一构筑都以文件的MD5哈希值命名,所以你不必担心内存的难题。
remote:它决定了你将从哪些应用应用领域程序接收两个组件、两个页面或应用应用领域程序本身。 每一应用应用领域程序都可以同时暴露和定义两个远程,并多次进行。
现在让他们把Product应用应用领域暴露给Shell应用应用领域,让他们做第两个微后端连接。
让他们打开产品 repo 中的 webpack.config.js 文件,并改变其中传给ModuleFederationPlugin中的对象,如下。exposes对象中的值决定了它在repo中共享哪个组件,而对象中的key决定了其他应用应用领域程序能访问这个组件的名称。
new ModuleFederationPlugin(name: PRODUCT,filename: remoteEntry.js,exposes: {./App: ./src/App,},shared: […deps,react: { requiredVersion: deps.react, singleton: true },react-dom: {requiredVersion: deps[react-dom],singleton: true,},},],),
让他们打开shell repo中的webpack.config.js文件,将其中传给ModuleFederationPlugin方式的对象做如下修改。remotes对象中的值决定了如何访问Product(@符号前的名称必须与Product库中Webpack config中的名称相同),对象中的key允许他们只用名称来访问Product。
new ModuleFederationPlugin(name: SHELL,filename: remoteEntry.js,remotes: {PRODUCT: PRODUCT@http://localhost:3002/remoteEntry.js},shared: […deps,react: { requiredVersion: deps.react, singleton: true },react-dom: {requiredVersion: deps[react-dom],singleton: true,},},],),
他们已经将2个应用应用领域程序连接在一起。现在让他们看看如何在Shell应用程序中采用Product。让他们在shell资源库中打开App.js并做如下修改。
import React from react;import ./App.css;const ProductApp = React.lazy(() => import(PRODUCT/App)const App = () => (
Hi from Shell App
export default App;
他们已经定义了名为 “App “的组件,该组件通过React的lazy方式从Product中暴露出来,到他们名为ProductApp的变量。他们须要对他们将从不同的微后端获得的组件采用lazy函数,他们须要采用Suspense在模板部分采用它,这样他们能确保大部份的东西都加载到页面上。
假如你现在想,你能在User应用应用领域程序中添加两个组件,并尝试在产品应用应用领域程序中采用这个组件:) 你能采用expose和remotes共享你想要的组件。 创建组件联邦政府的Zack Jackson有两个Github repo,叫做module-federation-examples。在这个Repo中,有许多的示例,如React、Vue、Angular、服务器端渲染、共享路由器,假如你想的话,你能查看它们。
须要解决的难题路由器
其中两个重要的难题是,微后端管理工作他们的路由器,以保持它们与Shell的松散谐振关系。
在他们创建的工程项目中,他们倾向于在Shell的路由器安装微后端组件。当到达/mf-a路径时,Shell会懒加载Micro-Frontend-A应用应用领域程序,当用户到达/mf-b路径时,它以同样的方式加载Micro-Frontend-B。
// shell/src/Shell.jsimport …const MicroFrontendA = lazy(() => import(MicroFrontendA/MicroFrontendARoutes));const MicroFrontendB = lazy(() => import(MicroFrontendB/MicroFrontendBRoutes));const Shell = () => {return (Yükleniyor…}>export default Shell;
之后,控制权转移到微后端。微后端-A处置他们的子组件,并对其进行路由器设置。与上面的例子有关,当导航到/mf-a的路径时,PageA被加载,当路径是/mf-a/page-b时,PageB被加载。
// micro-frontend-a/src/pages/MicroFrontendARoutes.jsimport React, { lazy } from react;import { Switch, Route, useRouteMatch } from react-router-dom;import withPermissions from Shell/hoc/withPermissions;const PageA = lazy(() => import(pages/pageA/PageA));const PageB = lazy(() => import(pages/pageB/PageB));const MicroFrontendARoutes = () => {const { path } = useRouteMatch();return (withPermissions([VIEW_PAGE_A])(PageA)}withPermissions([VIEW_PAGE_B])(PageB)}export default MicroFrontendARoutes;共享状态以及hooks
实际上,在组件联邦政府中共享这些是非常容易的;但它目前有两个有趣的解决计划。
假如你看看我为Shell的webpack.config.js所举的例子,在共享方面有两个微妙的接触。两个消耗公共状态的hooks也在库下共享。由于应用应用领域程序常常在Shell下渲染,大部份的context都以正确的顺序加载,当他们像例子中那样共享 hooks时,他们能在微后端中采用通用context而不能有任何错误。
// shell/webpack.config.jsconst { dependencies: deps } = require(./package.json);const moduleFederationOptions = {…exposes: {… ./hooks/useToastr: ./src/hooks/useToastr,},shared: [},./src/hooks/useToastr, // Here!],热重载
举例说明他们遇到的难题。假如他们通过Shell访问应用应用领域程序,他们在Micro-Frontend-A中做的两个改变不能触发热重载。因此,他们在合作开发的时候会慢一点,他们必须在每次改变后刷新。
为了解决这个难题,组件联邦政府工程项目组合作开发了@module-federation/fmr包。当它作为插件被包含在Webpack配置中时,你的组件联邦政府结构的任何变化都会自动运转Live Reload。
部署
在使应用应用领域程序上线的过程中,他们遇到了两个主要难题。
在运转时动态地设置publicPath。 当用组件联邦政府创建两个复杂的应用应用领域程序时,会出现这类难题。Shell将从哪里获得微后端的共享文件?属于Shell的文件将来自微后端的哪些路径?许多文件路径须要被设置。他们通过正确指定publicPath选项来控制这些。
在Trendyol GO中,他们将应用应用领域程序作为Docker镜像创建一次,然后他们使它们在不同的环境中通过环境变量接受不同的设置。假如publicPath是在构筑时设置的,他们就必须解决大的配置文件难题,这不是两个优化的解决计划。
他们稍微修改了Zack Jackson在这首诗中提到的方式,使其非常简单地在运转时分配动态publicPath。
在他们采用的方式中,有两个叫做setPublicPath.js的文件。其内容的格式如下。
// shell/src/setPublicPath.js__webpack_public_path__ = `${new URL(document.currentScript.src).origin}/`;
他们通过在构筑时操作Webpack设置中的entry
// shell/webpack.config.jsentry: {Shell: ./src/setPublicPath,main: ./src/index,},
在运转时,动态设置组件联邦政府设置中分配的远程URL。他们用External Remotes Plugin来实现
// shell/webpack.config.jsconst moduleFederationOptions = {…remotes: {MicroFrontendA: MicroFrontendA@[window.MF_A_URL]/remoteEntry.js,MicroFrontendB: MicroFrontendB@[window.MF_B_URL]/remoteEntry.js,}, …
如何在运转时设置window.MF_A_URL and window.MF_B_URL
// shell/src/index.jsimport config from config; // dynamic vars. from an .env file e.g.window.MF_A_URL = config.MF_A_URL;window.MF_B_URL = config.MF_B_URL;import(./bootstrap);
在这个过程的最后,他们实现了两个稳定的应用应用领域程序。虽然在他们面前还有许多不同的改进,但从现在开始,每一工程项目组能合作开发他们的组件,而不必倚赖其他工程项目组。