




版權(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ì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 2025至2030中國百貨行業(yè)發(fā)展研究與產(chǎn)業(yè)戰(zhàn)略規(guī)劃分析評(píng)估報(bào)告
- 2025至2030中國生物貂行業(yè)產(chǎn)業(yè)運(yùn)行態(tài)勢(shì)及投資規(guī)劃深度研究報(bào)告
- 2025至2030中國玉米淀粉行業(yè)發(fā)展研究與產(chǎn)業(yè)戰(zhàn)略規(guī)劃分析評(píng)估報(bào)告
- 影樓團(tuán)隊(duì)培訓(xùn)課件
- 技術(shù)賦能教育實(shí)現(xiàn)個(gè)性化教學(xué)的突破
- 財(cái)務(wù)報(bào)銷流程培訓(xùn)
- 教育數(shù)據(jù)挖掘的潛力學(xué)生在多元評(píng)價(jià)體系中的應(yīng)用
- 年度培訓(xùn)計(jì)劃編寫課件
- 技術(shù)創(chuàng)新助力教育混和教學(xué)模式新發(fā)展
- 智慧城市服務(wù)中智能公共服務(wù)設(shè)施的可持續(xù)發(fā)展融資策略
- 血糖監(jiān)測(cè)課件小講課
- 教育培訓(xùn)機(jī)構(gòu)突發(fā)事件應(yīng)急預(yù)案
- DB23T 3841-2024 非煤礦山機(jī)械化、數(shù)字化、智能化、管理現(xiàn)代化建設(shè)規(guī)范
- 汽車車身密封條設(shè)計(jì)指南
- 光伏工程勞務(wù)承包合同協(xié)議書
- DBJT13-24-2017 福建省建筑幕墻工程質(zhì)量驗(yàn)收規(guī)程
- 學(xué)校會(huì)議審批管理制度
- 課內(nèi)文言文翻譯句句落實(shí)-2024-2025學(xué)年統(tǒng)編版語文九年級(jí)上冊(cè)
- 【中美家庭教育差異比較探究(英文)(論文)】
- 2024年秋新人教版八年級(jí)上冊(cè)物理全冊(cè)教學(xué)課件(新版教材)
- 國防動(dòng)員工作計(jì)劃
評(píng)論
0/150
提交評(píng)論