您现在的位置是:网站首页> 编程资料编程资料
React 模块联邦多模块项目实战详解_React_
2023-05-24
731人已围观
简介 React 模块联邦多模块项目实战详解_React_
前提:
老项目是一个多模块的前端项目,有一个框架层级的前端服务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 ( }> {allRoutes.map(([path, component]) => )} ); } 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 (win
相关内容
- ant-design-vue中设置Table每页显示的条目数量方式_vue.js_
- Ant Design Vue中的table与pagination的联合使用方式_vue.js_
- ant design vue的table取消自带分页问题_vue.js_
- JavaScript中net::ERR_CONNECTION_REFUSED解决方法大全_javascript技巧_
- Vue electron前端开启局域网接口实现流程详细介绍_vue.js_
- Vue全局监测错误并生成错误日志实现方法介绍_vue.js_
- Vant实现上传多个图片或视频,更改视频预览图_vue.js_
- React hook超详细教程_React_
- 使用vant-uploader上传照片无法删除的解决_vue.js_
- Varlet组件实现一个丝滑的点击水波效果详解_javascript技巧_
点击排行
本栏推荐
