React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解_第1頁
React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解_第2頁
React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解_第3頁
React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解_第4頁
React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解_第5頁
已閱讀5頁,還剩9頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進(jìn)行舉報或認(rèn)領(lǐng)

文檔簡介

第React模塊聯(lián)邦多模塊項目實戰(zhàn)詳解目錄前提:1.修改webpack增加ModuleFederationPlugin2.本地開發(fā)測試3.根據(jù)路由變化自動加載對應(yīng)的服務(wù)入口4.線上部署5.問題記錄

前提:

老項目是一個多模塊的前端項目,有一個框架層級的前端服務(wù)A,用來渲染界面的大概樣子,其余各個功能模塊前端定義自己的路由信息與組件。本地開發(fā)時,通過依賴框架服務(wù)A來啟動項目,在線上部署時會有一個總前端的應(yīng)用,在整合的時候,通過在獲取路由信息時批量加載各個功能模塊的路由信息,來達(dá)到服務(wù)整合的效果。

//config.js

//這個配置文件定義在收集路由時需要從哪些依賴?yán)锸占?/p>

modules:[

'front-service-B',

'front-service-C',

'front-service-D',

痛點

本地聯(lián)調(diào)多個前端服務(wù)時比較麻煩,需要下載對應(yīng)服務(wù)npm資源,并在config.js中配置上需要整合的服務(wù)名稱,并且在debugger時,看到的source樹中是經(jīng)過webpack編譯后的代碼。如果本地聯(lián)調(diào)多個服務(wù)時,需要修改依賴服務(wù)的代碼,要么直接在node_modules中修改,要么將拉取對應(yīng)服務(wù)代碼,在源碼上修改好了之后通過編譯將打出來的包替換node_modules中的源文件,或者使用yalc來link本地啟動的服務(wù),不管是哪種方法都比直接修改動態(tài)刷新都要麻煩的多。部署線上開發(fā)環(huán)境時,需要將修改好的本地服務(wù)提交到代碼庫,跑完一次CI編譯后,還需要再跑一次總前端應(yīng)用的CICD才能部署到線上,這樣發(fā)布測試的時間成本大大增加。

需求

實現(xiàn)真正意義上的微前端,各服務(wù)的資源可相互引用,并且在對應(yīng)模塊編譯更新后,線上可直接看到效果,不需要重新CICD一次總前端,在本地開發(fā)時,引入不同前端服務(wù),可通過線上版本或者本地版本之間的自由切換。自然而然,我們想到ModuleFederation模塊聯(lián)邦。

思路

首先需要明確一下思路,既然各個服務(wù)是通過路由來驅(qū)動的,那我們需要做的,簡單來說就是將各個服務(wù)的路由文件通過模塊聯(lián)邦導(dǎo)出,在框架服務(wù)A的路由收集里,通過監(jiān)測路由pathname的變化,來動態(tài)引入對應(yīng)服務(wù)的路由信息來達(dá)到微前端的效果。

實戰(zhàn)

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:這是模塊聯(lián)邦編譯后生成的入口文件名,增加ModuleFederationPlugin后會在打包出來的dist文件中多生成一個$filename文件。name:一個模塊的唯一值,在這個例子中,用不同模塊package.json中設(shè)置的routeName值來作為唯一值。

functiongetPackageRouteName(){

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

constpackageData=fs.readFileSync(packagePath);

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

returnparsePackageData.routeName;

library:打包方式,此處與name值一致就行.exposes:這是重要的參數(shù)之一,設(shè)置了哪些模塊能夠?qū)С?。參?shù)為一個對象,可設(shè)置多個,在這里我們最重要的就是導(dǎo)出各個服務(wù)的路由文件,路徑在$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:模塊單例的配置項,由于各個模塊單獨編譯可運行,為保證依賴項單例(共享模塊),通過設(shè)置這個參數(shù)來配置。

//這里的配置項按不同項目需求來編寫主要目的是避免依賴生成多例導(dǎo)致數(shù)據(jù)不統(tǒng)一的問題

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:這是引入導(dǎo)出模塊的配置項,比如我們配置了一個name為A的exposes模塊,則可以在這里配置

//ModuleFederationPlugin

remotes:{

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

//usage

importCompAfrom'A';

但是在我實際測試中,使用remotes導(dǎo)入模塊,會報各種各樣奇奇怪怪的問題,不知道是我的版本問題還是哪里配置沒對,所以這里在導(dǎo)入模塊的地方,我選擇了官方文檔中的動態(tài)遠(yuǎn)程容器方法.

2.本地開發(fā)測試

本地要完成的需求是,單獨啟動服務(wù)A后,通過注入服務(wù)B的入口文件,達(dá)到路由整合里有兩個服務(wù)的路由信息。

在這里我們假設(shè)服務(wù)A的路由pathname是pathA,服務(wù)B的pathanme是pathB

這個時候我們本地啟動兩個服務(wù),服務(wù)A在8080端口,服務(wù)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,請檢查服務(wù)版本');

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服務(wù)的pathname,目的是在加載完B服務(wù)的路由文件后設(shè)置到Route信息上,通過異步script的方法,向head中增加一條src為remoteEntry地址的script標(biāo)簽。

如果加載文件成功,會在window變量下生成一個window.$name的變量,這個name值目前就是服務(wù)B的ModuleFederationPlugin配置的name值。通過window.$name.get(./index)就可以拿到我們導(dǎo)出的路由信息了。

如果一切順利這時在切換不同服務(wù)路由時,應(yīng)該能成功加載路由信息了。

3.根據(jù)路由變化自動加載對應(yīng)的服務(wù)入口

上面我們是寫死了一個pathname和remote地址,接下來要做的是在路由變化時,自動去加載對應(yīng)的服務(wù)入口。這里我們第一步需要將所有的前端服務(wù)共享到環(huán)境變量中。在.env(環(huán)境變量的方法可以有很多種,目的是配置在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,請檢查服務(wù)版本');

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變化時,通過環(huán)境變量找到對應(yīng)的remoteEn

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論