React模块联邦多模块项目实战详解_第1页
React模块联邦多模块项目实战详解_第2页
React模块联邦多模块项目实战详解_第3页
React模块联邦多模块项目实战详解_第4页
React模块联邦多模块项目实战详解_第5页
已阅读5页,还剩9页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第React模块联邦多模块项目实战详解目录前提:1.修改webpack增加ModuleFederationPlugin2.本地开发测试3.根据路由变化自动加载对应的服务入口4.线上部署5.问题记录

前提:

老项目是一个多模块的前端项目,有一个框架层级的前端服务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一次总前端,在本地开发时,引入不同前端服务,可通过线上版本或者本地版本之间的自由切换。自然而然,我们想到ModuleFederation模块联邦。

思路

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

实战

1.修改webpack增加ModuleFederationPlugin

importwebpack,{container}from'webpack';

const{ModuleFederationPlugin,}=container;

newModuleFederationPlugin({

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值来作为唯一值。

functiongetPackageRouteName(){

constpackagePath=path.join(cwd,'package.json');

constpackageData=fs.readFileSync(packagePath);

constparsePackageData=JSON.parse(packageData.toString());

returnparsePackageData.routeName;

library:打包方式,此处与name值一致就行.exposes:这是重要的参数之一,设置了哪些模块能够导出。参数为一个对象,可设置多个,在这里我们最重要的就是导出各个服务的路由文件,路径在$packageRepo/react/index.js中,

functiongetExpose(){

constpackagePath=path.join(cwd,'package.json');

constpackageData=fs.readFileSync(packagePath);

constparsePackageData=JSON.parse(packageData.toString());

letobj={};

obj['./index']='./react/index.js';

return{...obj};

shared:模块单例的配置项,由于各个模块单独编译可运行,为保证依赖项单例(共享模块),通过设置这个参数来配置。

//这里的配置项按不同项目需求来编写主要目的是避免依赖生成多例导致数据不统一的问题

functiongetShared(){

constobj={

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,

returnobj;

remotes:这是引入导出模块的配置项,比如我们配置了一个name为A的exposes模块,则可以在这里配置

//ModuleFederationPlugin

remotes:{

A:'A@http://localhost:3001/remoteEntry.js',

//usage

importCompAfrom'A';

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

2.本地开发测试

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

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

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

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

importReact,{Suspense,useEffect,useState}from'react';

import{Route,useLocation}from'react-router-dom';

importCacheRoute,{CacheSwitch}from'react-router-cache-route';

importNoMacthfrom'@/components/c7n-errors/404';

importSkeletonfrom'@/components/skeleton';

constroutes:[string,React.ComponentType][]=__ROUTES__||[];

constAutoRouter=()={

const[allRoutes,setAllRoutes]=useState(routes);

const{

pathname

}=useLocation();

functionloadComponent(scope,module,onError){

returnasync()={

//Initializesthesharescope.Thisfillsitwithknownprovidedmodulesfromthisbuildandallremotes

await__webpack_init_sharing__('default');

constcontainer=window[scope];//orgetthecontainersomewhereelse

//Initializethecontainer,itmayprovidesharedmodules

if(!container){

thrownewError('加载了错误的importManifest.js,请检查服务版本');

try{

awaitcontainer.init(__webpack_share_scopes__.default);

constfactory=awaitwindow[scope].get(module);

constModule=factory();

returnModule;

}catch(e){

if(onError){

returnonError(e);

throwe;

constloadScrip=(url,callback)={

letscript=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);

constasyncGetRemoteEntry=async(path,remoteEntry)=newPromise((resolve)={

loadScrip(remoteEntry,()={

if(window[path]){

constlazyComponent=loadComponent(path,'./index');

resolve([`/${path}`,React.lazy(lazyComponent)])

}else{

resolve();

constcallbackWhenPathName=async(path)={

letarr=allRoutes;

constremoteEntry='http://localhost:9090/remoteEntry';

constresult=awaitasyncGetRemoteEntry(path,remoteEntry);

if(result){

arr.push(result)

setAllRoutes([].concat(arr));

useEffect(()={

callbackWhenPathName('pathB')

},[])

return(

Suspensefallback={Skeleton/}

CacheSwitch

{allRoutes.map(([path,component])=Routepath={path}component={component}/)}

CacheRoutepath="*"component={NoMacth}/

/CacheSwitch

/Suspense

exportdefaultAutoRouter;

这里来解释一下,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

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

importReact,{Suspense,useEffect,useState}from'react';

import{Route,useLocation}from'react-router-dom';

importCacheRoute,{CacheSwitch}from'react-router-cache-route';

importNoMacthfrom'@/components/c7n-errors/404';

importSkeletonfrom'@/components/skeleton';

//@ts-expect-error

constroutes:[string,React.ComponentType][]=__ROUTES__||[];

constAutoRouter=()={

const[allRoutes,setAllRoutes]=useState(routes);

const{

pathname

}=useLocation();

functionloadComponent(scope,module,onError){

returnasync()={

//Initializesthesharescope.Thisfillsitwithknownprovidedmodulesfromthisbuildandallremotes

await__webpack_init_sharing__('default');

constcontainer=window[scope];//orgetthecontainersomewhereelse

//Initializethecontainer,itmayprovidesharedmodules

if(!container){

thrownewError('加载了错误的importManifest.js,请检查服务版本');

try{

awaitcontainer.init(__webpack_share_scopes__.default);

constfactory=awaitwindow[scope].get(module);

constModule=factory();

returnModule;

}catch(e){

if(onError){

returnonError(e);

throwe;

constloadScrip=(url,callback)={

letscript=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);

constasyncGetRemoteEntry=async(path,remoteEntry)=newPromise((resolve)={

loadScrip(remoteEntry,()={

if(window[path]){

constlazyComponent=loadComponent(path,'./index');

resolve([`/${path}`,React.lazy(lazyComponent)])

}else{

resolve();

constcallbackWhenPathName=async(path)={

letarr=allRoutes;

constenv:any=window._env_;

constenvList=Object.keys(env);

if(window[path]allRoutes.find(i=i[0].includes(path))){

return;

}else{

constremoteEntry=env[`remote_${path}`];

if(remoteEntry){

if(window[path]){

constlazyComponent=loadComponent(path,'./index');

arr.push([`/${path}`,React.lazy(lazyComponent)]);

setAllRoutes([].concat(arr));

}else{

constresult=awaitasyncGetRemoteEntry(path,remoteEntry);

if(result){

arr.push(result)

setAllRoutes([].concat(arr));

useEffect(()={

constpath=pathname.split('/')[1];

callbackWhenPathName(path)

},[pathname])

return(

Suspensefallback={Skeleton/}

CacheSwitch

{allRoutes.map(([path,component])=Routepath={path}component={component}/)}

CacheRoutepath="*"component={NoMacth}/

/CacheSwitch

/Suspense

exportdefaultAutoRouter;

唯一的变化就是在pathname变化时,通过环境变量找到对应的remoteEn

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论