亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

React 模塊聯(lián)邦多模塊項目實戰(zhàn)詳解

 更新時間:2022年10月20日 15:20:45   作者:UI仔  
這篇文章主要介紹了React 模塊聯(lián)邦多模塊項目實戰(zhàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

前提:

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

// config.js
// 這個配置文件 定義在收集路由時需要從哪些依賴里收集
modules: [
    'front-service-B',
    'front-service-C',
    'front-service-D',
    ...
  ],

痛點

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

需求

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

思路

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

實戰(zhàn)

1. 修改webpack增加ModuleFederationPlugin

import webpack, { container } from 'webpack';
const {  ModuleFederationPlugin,} = container;
new ModuleFederationPlugin({  
    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中設置的routeName值來作為唯一值。
function getPackageRouteName() {
  const packagePath = path.join(cwd, 'package.json');
  const packageData = fs.readFileSync(packagePath);
  const parsePackageData = JSON.parse(packageData.toString());
  return parsePackageData.routeName;
}
  • library: 打包方式,此處與name值一致就行.
  • exposes: 這是重要的參數(shù)之一,設置了哪些模塊能夠導出。參數(shù)為一個對象,可設置多個,在這里我們最重要的就是導出各個服務的路由文件,路徑在$packageRepo/react/index.js中,
function getExpose() {
  const packagePath = path.join(cwd, 'package.json');
  const packageData = fs.readFileSync(packagePath);
  const parsePackageData = JSON.parse(packageData.toString());
  let obj = {};
  obj['./index'] = './react/index.js';
  return { ...obj };
}
  • shared: 模塊單例的配置項,由于各個模塊單獨編譯可運行,為保證依賴項單例(共享模塊),通過設置這個參數(shù)來配置。
// 這里的配置項按不同項目需求來編寫 主要目的是避免依賴生成多例導致數(shù)據(jù)不統(tǒng)一的問題
function getShared() {
  const obj = {
    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,
      };
    }
  });
  return obj;
}
  • remotes: 這是引入導出模塊的配置項,比如我們配置了一個name為A的exposes模塊,則可以在這里配置
// ModuleFederationPlugin
remotes: {
    A: 'A@http://localhost:3001/remoteEntry.js',
},
// usage
import CompA from 'A';

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

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

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

在這里我們假設服務A的路由pathnamepathA,服務B的pathanmepathB

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

這個時候我們來到路由收集文件

import React, { Suspense, useEffect, useState } from 'react';
import { Route, useLocation  } from 'react-router-dom';
import CacheRoute, { CacheSwitch } from 'react-router-cache-route';
import NoMacth from '@/components/c7n-error-pages/404';
import Skeleton from '@/components/skeleton';
const routes:[string, React.ComponentType][] = __ROUTES__ || [];
const AutoRouter = () => {
  const [allRoutes, setAllRoutes] = useState(routes);
  const {
    pathname
  } = useLocation();
  function loadComponent(scope, module, onError) {
    return async () => {
      // Initializes the share scope. This fills it with known provided modules from this build and all remotes
      await __webpack_init_sharing__('default');
      const container = window[scope]; // or get the container somewhere else
      // Initialize the container, it may provide shared modules
      if (!container) {
        throw new Error('加載了錯誤的importManifest.js,請檢查服務版本');
      }
      try {
        await container.init(__webpack_share_scopes__.default);
        const factory = await window[scope].get(module);
        const Module = factory();
        return Module;
      } catch (e) {
        if (onError) {
          return onError(e);
        }
        throw e;
      }
    };
  }
  const loadScrip = (url, callback) => {
    let script = 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);
}
  const asyncGetRemoteEntry = async (path, remoteEntry) => new Promise((resolve) => {
    loadScrip(remoteEntry, () => {
      if (window[path]) {
        const lazyComponent = loadComponent(path, './index');
        resolve([`/${path}`, React.lazy(lazyComponent)])
      } else {
        resolve();
      }
    });
  })
  const callbackWhenPathName = async (path) => {
      let arr = allRoutes;
      const remoteEntry = 'http://localhost:9090/remoteEntry';
      const result = await asyncGetRemoteEntry(path, remoteEntry);
      if (result) {
        arr.push(result)
        setAllRoutes([].concat(arr));
      }
  }
  useEffect(() => {
    callbackWhenPathName('pathB')
  }, [])
  return (
    <Suspense fallback={<Skeleton />}>
      <CacheSwitch>
        {allRoutes.map(([path, component]) => <Route path={path} component={component} />)}
        <CacheRoute path="*" component={NoMacth} />
      </CacheSwitch>
    </Suspense>
  );
}
export default AutoRouter;

這里來解釋一下,callbackWhenPathName方法引入了B服務的pathname,目的是在加載完B服務的路由文件后設置到Route信息上,通過異步script的方法,向head中增加一條srcremoteEntry地址的script標簽。

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

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

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

上面我們是寫死了一個pathnameremote地址,接下來要做的是在路由變化時,自動去加載對應的服務入口。 這里我們第一步需要將所有的前端服務共享到環(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
...

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

import React, { Suspense, useEffect, useState } from 'react';
import { Route, useLocation  } from 'react-router-dom';
import CacheRoute, { CacheSwitch } from 'react-router-cache-route';
import NoMacth from '@/components/c7n-error-pages/404';
import Skeleton from '@/components/skeleton';
// @ts-expect-error
const routes:[string, React.ComponentType][] = __ROUTES__ || [];
const AutoRouter = () => {
  const [allRoutes, setAllRoutes] = useState(routes);
  const {
    pathname
  } = useLocation();
  function loadComponent(scope, module, onError) {
    return async () => {
      // Initializes the share scope. This fills it with known provided modules from this build and all remotes
      await __webpack_init_sharing__('default');
      const container = window[scope]; // or get the container somewhere else
      // Initialize the container, it may provide shared modules
      if (!container) {
        throw new Error('加載了錯誤的importManifest.js,請檢查服務版本');
      }
      try {
        await container.init(__webpack_share_scopes__.default);
        const factory = await window[scope].get(module);
        const Module = factory();
        return Module;
      } catch (e) {
        if (onError) {
          return onError(e);
        }
        throw e;
      }
    };
  }
  const loadScrip = (url, callback) => {
    let script = 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);
}
  const asyncGetRemoteEntry = async (path, remoteEntry) => new Promise((resolve) => {
    loadScrip(remoteEntry, () => {
      if (window[path]) {
        const lazyComponent = loadComponent(path, './index');
        resolve([`/${path}`, React.lazy(lazyComponent)])
      } else {
        resolve();
      }
    });
  })
  const callbackWhenPathName = async (path) => {
    let arr = allRoutes;
    const env: any = window._env_;
    const envList = Object.keys(env);
    if (window[path] && allRoutes.find(i => i[0].includes(path))) {
      return;
    } else {
      const remoteEntry = env[`remote_${path}`];
      if (remoteEntry) {
        if (window[path]) {
          const lazyComponent = loadComponent(path, './index');
          arr.push([`/${path}`, React.lazy(lazyComponent)]);
          setAllRoutes([].concat(arr));
        } else {
          const result = await asyncGetRemoteEntry(path, remoteEntry);
          if (result) {
            arr.push(result)
            setAllRoutes([].concat(arr));
          }
        }
      }
    }
  }
  useEffect(() => {
    const path = pathname.split('/')[1];
    callbackWhenPathName(path)
  }, [pathname])
  return (
    <Suspense fallback={<Skeleton />}>
      <CacheSwitch>
        {allRoutes.map(([path, component]) => <Route path={path} component={component} />)}
        <CacheRoute path="*" component={NoMacth} />
      </CacheSwitch>
    </Suspense>
  );
}
export default AutoRouter;

唯一的變化就是在pathname變化時,通過環(huán)境變量找到對應的remoteEntry的地址來加載。

4.線上部署

在各個分服務的CI中,我們需要增加上傳打包后文件的操作,這里我們選擇的是MINIO服務器,將各個服務通過webpack打包后的dist文件(當然dist文件中也包含了remoteEntry.js文件)上傳在MINIO上,可直接通過url訪問到文件內容即可。

在以前的版本中,總前端需要依賴各個服務進行一個裝包,編譯部署的過程

// 總前端的package.json
"dependencies": {
    "架構-A": "x.x.x",
    "服務B": "x.x.x",
    "服務C": "x.x.x",
    "服務D": "x.x.x,
    ...
  },

但是現(xiàn)在我們的總前端只需要一個框架類的服務A,其余服務都只通過環(huán)境變量的方法來引入就行了。

// 總前端的.env文件
remote_B=$MINIO_URL/remoteB/$版本號/remoteEntry.js
remote_C=$MINIO_URL/remoteC/$版本號/remoteEntry.js
remote_D=$MINIO_URL/remoteD/$版本號/remoteEntry.js
remote_E=$MINIO_URL/remoteE/$版本號/remoteEntry.js

5.問題記錄

  • 在配置ModuleFederationPluginremotes時,最好用JSON.stringify包裹一下,不然會導致編譯之后生成的remote地址為變量名而不是字符串。
  • 如果出現(xiàn)fn is not function錯誤,多半是expose導出寫的有問題,如果實在解決不了,建議使用官方推薦的loadComponent方法。
  • webpackoptimization方法貌似與ModuleFederationPlugin不兼容,建議去掉。
  • 如果出現(xiàn)shared模塊共享問題,可通過增加一個bootStrap方法。
import("./bootstrap.js")
import App from './App.jsx' 
import ReactDOM from 'react-dom';
import React from 'react';
ReactDOM.render(<App />, document.getElementById("app"));

以上就是React 模塊聯(lián)邦多模塊項目實戰(zhàn)詳解的詳細內容,更多關于React 模塊聯(lián)邦多模塊的資料請關注腳本之家其它相關文章!

相關文章

  • react 實現(xiàn)圖片正在加載中 加載完成 加載失敗三個階段的原理解析

    react 實現(xiàn)圖片正在加載中 加載完成 加載失敗三個階段的原理解析

    這篇文章主要介紹了react 實現(xiàn)圖片正在加載中 加載完成 加載失敗三個階段的,通過使用loading的圖片來占位,具體原理解析及實現(xiàn)代碼跟隨小編一起通過本文學習吧
    2021-05-05
  • React 源碼調試方式

    React 源碼調試方式

    這篇文章主要為大家介紹了React源碼調試方式實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-08-08
  • ReactJS中不同類型的狀態(tài)詳解

    ReactJS中不同類型的狀態(tài)詳解

    這篇文章主要為大家介紹了ReactJS中不同類型的狀態(tài)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-12-12
  • React根據(jù)寬度自適應高度的示例代碼

    React根據(jù)寬度自適應高度的示例代碼

    本篇文章主要介紹了React根據(jù)寬度自適應高度的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-10-10
  • React反向代理及樣式獨立詳解

    React反向代理及樣式獨立詳解

    這篇文章主要介紹了React反向代理及樣式獨立詳解,文章圍繞主題展開詳細的內容介紹,具有一定的參考價值,需要的朋友可以參考一下
    2022-08-08
  • React hooks的優(yōu)缺點詳解

    React hooks的優(yōu)缺點詳解

    這篇文章主要介紹了React hooks的優(yōu)缺點詳解,幫助大家更好的理解和學習使用React,感興趣的朋友可以了解下
    2021-04-04
  • ReactHook使用useState更新變量后,如何拿到變量更新后的值

    ReactHook使用useState更新變量后,如何拿到變量更新后的值

    這篇文章主要介紹了ReactHook使用useState更新變量后,如何拿到變量更新后的值問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • concent漸進式重構react應用使用詳解

    concent漸進式重構react應用使用詳解

    這篇文章主要為大家介紹了concent漸進式重構react應用的使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-11-11
  • 使用reactjs優(yōu)化了進度條頁面性能提高70%

    使用reactjs優(yōu)化了進度條頁面性能提高70%

    這篇文章主要介紹了使用reactjs優(yōu)化了進度條后頁面性能提高了70%的操作技巧,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-04-04
  • React?Router?v6路由懶加載的2種方式小結

    React?Router?v6路由懶加載的2種方式小結

    React?Router?v6?的路由懶加載有2種實現(xiàn)方式,1是使用react-router自帶的?route.lazy,2是使用React自帶的?React.lazy,下面我們就來看看它們的具體實現(xiàn)方法吧
    2024-04-04

最新評論