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

下載本文檔

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

文檔簡(jiǎn)介

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

前提:

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

//config.js

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

modules:[

'front-service-B',

'front-service-C',

'front-service-D',

痛點(diǎn)

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

需求

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

思路

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

實(shí)戰(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后會(huì)在打包出來的dist文件中多生成一個(gè)$filename文件。name:一個(gè)模塊的唯一值,在這個(gè)例子中,用不同模塊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ù)為一個(gè)對(duì)象,可設(shè)置多個(gè),在這里我們最重要的就是導(dǎo)出各個(gè)服務(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:模塊單例的配置項(xiàng),由于各個(gè)模塊單獨(dú)編譯可運(yùn)行,為保證依賴項(xiàng)單例(共享模塊),通過設(shè)置這個(gè)參數(shù)來配置。

//這里的配置項(xiàng)按不同項(xiàng)目需求來編寫主要目的是避免依賴生成多例導(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)出模塊的配置項(xiàng),比如我們配置了一個(gè)name為A的exposes模塊,則可以在這里配置

//ModuleFederationPlugin

remotes:{

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

//usage

importCompAfrom'A';

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

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

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

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

這個(gè)時(shí)候我們本地啟動(dòng)兩個(gè)服務(wù),服務(wù)A在8080端口,服務(wù)B在9090端口,啟動(dòng)后,如果你的ModuleFederationPlugin配置正確,可以通過localhost:9090/remoteEntry.js來查看是否生成了入口文件。

這個(gè)時(shí)候我們來到路由收集文件

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('加載了錯(cuò)誤的importManifest.js,請(qǐng)檢查服務(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)簽。

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

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

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

上面我們是寫死了一個(gè)pathname和remote地址,接下來要做的是在路由變化時(shí),自動(dòng)去加載對(duì)應(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('加載了錯(cuò)誤的importManifest.js,請(qǐng)檢查服務(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變化時(shí),通過環(huán)境變量找到對(duì)應(yīng)的remoteEn

溫馨提示

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

最新文檔

評(píng)論

0/150

提交評(píng)論