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

提交評論