首页IT科技react mobx(React模块联邦多模块项目实战详解)

react mobx(React模块联邦多模块项目实战详解)

时间2025-09-05 11:43:39分类IT科技浏览6574
导读:前提: 老项目是一个多模块的前端项目,有一个框架层级的前端服务A,用来渲染界面的大概样子,其余各个功能模块前端定义自己的路由信息与组件。本地开发时,通过依赖框架服务A来启动项目,在线上部署时会有一个总前端的应用,在整合的时候,通过在获取路由信息时批量加载各个功能模块的路由信息,来达到服务整合的效果。...

前提:

老项目是一个多模块的前端项目                ,有一个框架层级的前端服务A                        ,用来渲染界面的大概样子         ,其余各个功能模块前端定义自己的路由信息与组件                。本地开发时            ,通过依赖框架服务A来启动项目                        ,在线上部署时会有一个总前端的应用             ,在整合的时候        ,通过在获取路由信息时批量加载各个功能模块的路由信息                        ,来达到服务整合的效果                        。

// config.js // 这个配置文件 定义在收集路由时需要从哪些依赖里收集 modules: [ front-service-B, front-service-C, front-service-D, ... ],

痛点

本地联调多个前端服务时比较麻烦                 ,需要下载对应服务npm资源    ,并在config.js中配置上需要整合的服务名称                        ,并且在debugger时                     ,看到的source树中是经过webpack编译后的代码         。 如果本地联调多个服务时,需要修改依赖服务的代码                    ,要么直接在node_modules中修改                         ,要么将拉取对应服务代码     ,在源码上修改好了之后通过编译将打出来的包替换node_modules中的源文件                ,或者使用yalc来link本地启动的服务                        ,不管是哪种方法都比直接修改动态刷新都要麻烦的多            。 部署线上开发环境时         ,需要将修改好的本地服务提交到代码库            ,跑完一次CI编译后                        ,还需要再跑一次总前端应用的CICD才能部署到线上             ,这样发布测试的时间成本大大增加                        。

需求

实现真正意义上的微前端        ,各服务的资源可相互引用                        ,并且在对应模块编译更新后                 ,线上可直接看到效果    ,不需要重新CICD一次总前端                        ,在本地开发时                     ,引入不同前端服务,可通过线上版本或者本地版本之间的自由切换             。自然而然                    ,我们想到Module Federation——模块联邦        。

思路

首先需要明确一下思路                         ,既然各个服务是通过路由来驱动的     ,那我们需要做的                ,简单来说就是将各个服务的路由文件通过模块联邦导出                        ,在框架服务A的路由收集里         ,通过监测路由pathname的变化            ,来动态引入对应服务的路由信息来达到微前端的效果                        。

实战

1. 修改webpack增加ModuleFederationPlugin

import webpack, { container } from webpack; const { ModuleFederationPlugin,} = container; new ModuleFederationPlugin({ filename: remoteEntry.js, name: getPackageRouteName(), library: { type: var, name: getPackageRouteName(), }, exposes: getExpose(), shared: getShared(), // remotes: getRemotes(envStr, modules), }),
filename: 这是模块联邦编译后生成的入口文件名                        ,增加ModuleFederationPlugin后会在打包出来的dist文件中多生成一个$filename文件                 。 name:一个模块的唯一值             ,在这个例子中        ,用不同模块package.json中设置的routeName值来作为唯一值    。
function getPackageRouteName() { const packagePath = path.join(cwd, package.json); const packageData = fs.readFileSync(packagePath); const parsePackageData = JSON.parse(packageData.toString()); return parsePackageData.routeName; }
library: 打包方式                        ,此处与name值一致就行. exposes: 这是重要的参数之一                 ,设置了哪些模块能够导出                        。参数为一个对象    ,可设置多个                        ,在这里我们最重要的就是导出各个服务的路由文件                     ,路径在$packageRepo/react/index.js中,
function getExpose() { const packagePath = path.join(cwd, package.json); const packageData = fs.readFileSync(packagePath); const parsePackageData = JSON.parse(packageData.toString()); let obj = {}; obj[./index] = ./react/index.js; return { ...obj }; }
shared: 模块单例的配置项,由于各个模块单独编译可运行                    ,为保证依赖项单例(共享模块)                         ,通过设置这个参数来配置                     。
// 这里的配置项按不同项目需求来编写 主要目的是避免依赖生成多例导致数据不统一的问题 function getShared() { const obj = { ckeditor: { singleton: true, eager: true, }, react: { singleton: true, requiredVersion: 16.14.0, }, react-dom: { singleton: true, requiredVersion: 16.14.0, }, react-router-dom: { singleton: true, requiredVersion: ^5.1.2, }, react-router: { singleton: true, requiredVersion: ^5.1.2, }, axios: { singleton: true, requiredVersion: ^0.16.2, }, react-query: { singleton: true, requiredVersion: ^3.34.6, }, }; Object.keys(dep).forEach((item) => { obj[item] = { singleton: true, requiredVersion: dep[item], }; if (eagerList.includes(item)) { obj[item] = { ...obj[item], eager: true, }; } }); return obj; }
remotes: 这是引入导出模块的配置项     ,比如我们配置了一个name为A的exposes模块                ,则可以在这里配置
// ModuleFederationPlugin remotes: { A: A@http://localhost:3001/remoteEntry.js, }, // usage import CompA from A;

但是在我实际测试中                        ,使用remotes导入模块         ,会报各种各样奇奇怪怪的问题            ,不知道是我的版本问题还是哪里配置没对                        ,所以这里在导入模块的地方             ,我选择了官方文档中的动态远程容器方法.

2.本地开发测试

本地要完成的需求是        ,单独启动服务A后                        ,通过注入服务B的入口文件                 ,达到路由整合里有两个服务的路由信息。

在这里我们假设服务A的路由pathname是pathA    ,服务B的pathanme是pathB

这个时候我们本地启动两个服务                        ,服务A在8080端口                     ,服务B在9090端口,启动后                    ,如果你的ModuleFederationPlugin配置正确,可以通过localhost:9090/remoteEntry.js来查看是否生成了入口文件                    。

这个时候我们来到路由收集文件

import React, { Suspense, useEffect, useState } from react; import { Route, useLocation } from react-router-dom; import CacheRoute, { CacheSwitch } from react-router-cache-route; import NoMacth from @/components/c7n-error-pages/404; import Skeleton from @/components/skeleton; const routes:[string, React.ComponentType][] = __ROUTES__ || []; const AutoRouter = () => { const [allRoutes, setAllRoutes] = useState(routes); const { pathname } = useLocation(); function loadComponent(scope, module, onError) { return async () => { // Initializes the share scope. This fills it with known provided modules from this build and all remotes await __webpack_init_sharing__(default); const container = window[scope]; // or get the container somewhere else // Initialize the container, it may provide shared modules if (!container) { throw new Error(加载了错误的importManifest.js                         ,请检查服务版本); } try { await container.init(__webpack_share_scopes__.default); const factory = await window[scope].get(module); const Module = factory(); return Module; } catch (e) { if (onError) { return onError(e); } throw e; } }; } const loadScrip = (url, callback) => { let script = document.createElement(script); if (script.readyState) { // IE script.onreadystatechange = function () { if (script.readyState === loaded || script.readyState === complete) { script.onreadystatechange = null; callback(); } } } else { // 其他浏览器 script.onload = function () { callback(); } } script.src = url; script.crossOrigin = anonymous; document.head.appendChild(script); } const asyncGetRemoteEntry = async (path, remoteEntry) => new Promise((resolve) => { loadScrip(remoteEntry, () => { if (window[path]) { const lazyComponent = loadComponent(path, ./index); resolve([`/${path}`, React.lazy(lazyComponent)]) } else { resolve(); } }); }) const callbackWhenPathName = async (path) => { let arr = allRoutes; const remoteEntry = http://localhost:9090/remoteEntry; const result = await asyncGetRemoteEntry(path, remoteEntry); if (result) { arr.push(result) setAllRoutes([].concat(arr)); } } useEffect(() => { callbackWhenPathName(pathB) }, []) return ( <Suspense fallback={<Skeleton />}> <CacheSwitch> {allRoutes.map(([path, component]) => <Route path={path} component={component} />)} <CacheRoute path="*" component={NoMacth} /> </CacheSwitch> </Suspense> ); } export default AutoRouter;

这里来解释一下     ,callbackWhenPathName方法引入了B服务的pathname                ,目的是在加载完B服务的路由文件后设置到Route信息上,通过异步script的方法                        ,向head中增加一条src为remoteEntry地址的script标签                         。

如果加载文件成功         ,会在window变量下生成一个window.$name的变量,这个name值目前就是服务B的ModuleFederationPlugin配置的name值     。通过window.$name.get(./index)就可以拿到我们导出的路由信息了                。

如果一切顺利这时在切换不同服务路由时            ,应该能成功加载路由信息了                        。

3.根据路由变化自动加载对应的服务入口

上面我们是写死了一个pathname和remote地址,接下来要做的是在路由变化时                        ,自动去加载对应的服务入口         。 这里我们第一步需要将所有的前端服务共享到环境变量中            。在.env(环境变量的方法可以有很多种             ,目的是配置在window变量中        ,可直接访问)中配置如下:

remote_A=http://localhost:9090/remoteEntry.js remote_B=http://localhost:9091/remoteEntry.js remote_C=http://localhost:9092/remoteEntry.js remote_D=http://localhost:9093/remoteEntry.js remote_E=http://localhost:9094/remoteEntry.js ...

修改一下上面的路由收集方法:

import React, { Suspense, useEffect, useState } from react; import { Route, useLocation } from react-router-dom; import CacheRoute, { CacheSwitch } from react-router-cache-route; import NoMacth from @/components/c7n-error-pages/404; import Skeleton from @/components/skeleton; // @ts-expect-error const routes:[string, React.ComponentType][] = __ROUTES__ || []; const AutoRouter = () => { const [allRoutes, setAllRoutes] = useState(routes); const { pathname } = useLocation(); function loadComponent(scope, module, onError) { return async () => { // Initializes the share scope. This fills it with known provided modules from this build and all remotes await __webpack_init_sharing__(default); const container = window[scope]; // or get the container somewhere else // Initialize the container, it may provide shared modules if (!container) { throw new Error(加载了错误的importManifest.js                        ,请检查服务版本); } try { await container.init(__webpack_share_scopes__.default); const factory = await window[scope].get(module); const Module = factory(); return Module; } catch (e) { if (onError) { return onError(e); } throw e; } }; } const loadScrip = (url, callback) => { let script = document.createElement(script); if (script.readyState) { // IE script.onreadystatechange = function () { if (script.readyState === loaded || script.readyState === complete) { script.onreadystatechange = null; callback(); } } } else { // 其他浏览器 script.onload = function () { callback(); } } script.src = url; script.crossOrigin = anonymous; document.head.appendChild(script); } const asyncGetRemoteEntry = async (path, remoteEntry) => new Promise((resolve) => { loadScrip(remoteEntry, () => { if (window[path]) { const lazyComponent = loadComponent(path, ./index); resolve([`/${path}`, React.lazy(lazyComponent)]) } else { resolve(); } }); }) const callbackWhenPathName = async (path) => { let arr = allRoutes; const env: any = window._env_; const envList = Object.keys(env); if (window[path] && allRoutes.find(i => i[0].includes(path))) { return; } else { const remoteEntry = env[`remote_${path}`]; if (remoteEntry) { if (window[path]) { const lazyComponent = loadComponent(path, ./index); arr.push([`/${path}`, React.lazy(lazyComponent)]); setAllRoutes([].concat(arr)); } else { const result = await asyncGetRemoteEntry(path, remoteEntry); if (result) { arr.push(result) setAllRoutes([].concat(arr)); } } } } } useEffect(() => { const path = pathname.split(/)[1]; callbackWhenPathName(path) }, [pathname]) return ( <Suspense fallback={<Skeleton />}> <CacheSwitch> {allRoutes.map(([path, component]) => <Route path={path} component={component} />)} <CacheRoute path="*" component={NoMacth} /> </CacheSwitch> </Suspense> ); } export default AutoRouter;

唯一的变化就是在pathname变化时                 ,通过环境变量找到对应的remoteEntry的地址来加载                        。

4.线上部署

在各个分服务的CI中    ,我们需要增加上传打包后文件的操作                        ,这里我们选择的是MINIO服务器                     ,将各个服务通过webpack打包后的dist文件(当然dist文件中也包含了remoteEntry.js文件)上传在MINIO上,可直接通过url访问到文件内容即可             。

在以前的版本中                    ,总前端需要依赖各个服务进行一个装包                         ,编译部署的过程

// 总前端的package.json "dependencies": { "架构-A": "x.x.x", "服务B": "x.x.x", "服务C": "x.x.x", "服务D": "x.x.x, ... },

但是现在我们的总前端只需要一个框架类的服务A     ,其余服务都只通过环境变量的方法来引入就行了        。

// 总前端的.env文件 remote_B=$MINIO_URL/remoteB/$版本号/remoteEntry.js remote_C=$MINIO_URL/remoteC/$版本号/remoteEntry.js remote_D=$MINIO_URL/remoteD/$版本号/remoteEntry.js remote_E=$MINIO_URL/remoteE/$版本号/remoteEntry.js

5.问题记录

在配置ModuleFederationPlugin的remotes时                ,最好用JSON.stringify包裹一下                        ,不然会导致编译之后生成的remote地址为变量名而不是字符串                        。 如果出现fn is not function错误         ,多半是expose导出写的有问题            ,如果实在解决不了                        ,建议使用官方推荐的loadComponent方法                 。 webpack的optimization方法貌似与ModuleFederationPlugin不兼容             ,建议去掉    。 如果出现shared模块共享问题        ,可通过增加一个bootStrap方法                        。
import("./bootstrap.js")
import App from ./App.jsx import ReactDOM from react-dom; import React from react; ReactDOM.render(<App />, document.getElementById("app"));

以上就是React 模块联邦多模块项目实战详解的详细内容                        ,更多关于React 模块联邦多模块的资料请关注本站其它相关文章!

声明:本站所有文章                 ,如无特殊说明或标注    ,均为本站原创发布                     。任何个人或组织                        ,在未征得本站同意时                     ,禁止复制                、盗用                         、采集        、发布本站内容到任何网站            、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理                    。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
eml文件怎么用手机打开(EML文件如何打开) seo关键词优化软件(如何选择适合的SEO关键词优化软件App?)