成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

也許這才是你想要的微前端方案

開發 前端
微前端是當下的前端熱詞,稍具規模的團隊都會去做技術探索,作為一個不甘落后的團隊,我們也去做了。

前言

微前端是當下的前端熱詞,稍具規模的團隊都會去做技術探索,作為一個不甘落后的團隊,我們也去做了。也許你看過了Single-Spa,qiankun這些業界成熟方案,非常強大:JS沙箱隔離、多棧支持、子應用并行、子應用嵌套,但仔細想想它真的適合你嗎?

對于我來說,太重了,概念太多,理解困難。先說一下背景,我們之所以要對我司的小貸管理后臺做微前端改造,主要基于以下幾個述求:

  •  系統從接手時差不多30個頁面,一年多時間,發展到目前150多個頁面,并還在持續增長;
  •  項目體積變大,帶來開發體驗很差,打包構建速度很慢(初次構建,1分鐘以上);
  •  小貸系統開發量占整個web組50%的人力,每個迭代都有兩三個需求在這一個系統上開發,代碼合并沖突,上線時間交叉。帶來的是開發流程管理復雜;
  •  業務人員是分類的,沒有誰會用到所有的功能,每個業務人員只擁有其中30%甚至更少的功能。但不得不加載所有業務代碼,才能看到自己想要的頁面;

所以和市面上很多前端團隊引入微前端的目的不同的是,我們是拆,而更多的團隊是合。所以本方案適合和我目的一致的前端團隊,將自己維護的巨嬰系統瓦解,然后通過微前端"框架"來聚合,降低項目管理難度,提升開發體驗與業務使用體驗。

巨嬰系統技術棧: Dva + Antd

方案參考美團一篇文章:微前端在美團外賣的實踐

在做這個項目的按需提前加載設計時,自己去深究過webpack構建出的項目代碼運行邏輯,收獲比較多:webpack 打包的代碼怎么在瀏覽器跑起來的?, 不了解的可以看看

方案設計

基于業務角色,我們將巨嬰系統拆成了一個基座系統和四個子系統(可以按需擴展子系統),如下圖所示:

基座系統除了提供基座功能,即系統的登錄、權限獲取、子系統的加載、公共組件共享、公共庫的共享,還提供了一個基本所有業務人員都會使用的業務功能:用戶授(guan)信(li)。

子系統以靜態資源的方式,提供一個注冊函數,函數返回值是一個Switch包裹的組件與子系統所有的models。

路由設計

子系統以組件的形式加載到基座系統中,所以路由是入口,也是整個設計的第一步,為了區分基座系統頁面和子系統頁面,在路由上約定了下面這種形式: 

  1. // 子系統路由匹配,偽代碼  
  2. function Layout(layoutProps) {  
  3.   useEffect(() => {  
  4.       const apps = getIncludeSubAppMap();  
  5.       // 按需加載子項目;  
  6.       apps.forEach(subKey => startAsyncSubapp(subKey));  
  7.   }, []); 
  8.   return (  
  9.     <HLayout {...props}>  
  10.       <Switch>  
  11.           {/* 企業用戶管理 */}  
  12.           <Route exact path={Paths.PRODUCT_WHITEBAR} component={pages.ProductManage} breadcrumbName="企業用戶管理" />  
  13.           {/* ...省略一百行 */}  
  14.           <Route path="/subPage/" component={pages.AsyncComponent} />  
  15.       </Switch>  
  16.     </HLayout>  

即只要以subPage路徑開頭,就默認這個路由對應的組件為子項目,從而通過AsyncComponent組件去異步獲取子項目組件。

異步加載組件設計

路由設計完了,然后異步加載組件就是這個方案的靈魂了,流程是這樣的:

  •  通過路由,匹配到要訪問的具體是那個子項目;
  •  通過子項目id,獲取對應的manifest.json文件;
  •  通過獲取manifest.json,識別到對應的靜態資源(js,css)
  •  加載靜態資源,加載完,子項目執行注冊
  •  動態加載model,更新子項目組件

直接上代碼吧,簡單明了,資源加載的邏輯后面再詳講,需要注意的是model和component的加載順序: 

  1. export default function AsyncComponent({ location }) {  
  2.   // 子工程資源是否加載完成  
  3.   const [ayncLoading, setAyncLoaded] = useState(true);  
  4.   // 子工程組件加載存取  
  5.   const [ayncComponent, setAyncComponent] = useState(null);  
  6.   const { pathname } = location;  
  7.   // 取路徑中標識子工程前綴的部分, 例如 '/subPage/xxx/home' 其中xxx即子系統路由標識  
  8.   const id = pathname.split('/')[2];  
  9.   useEffect(() => {  
  10.     if (!subAppMapInfo[id]) {  
  11.       // 不存在這個子系統,直接重定向到首頁去  
  12.       goBackToIndex();  
  13.     }  
  14.     const status = subAppRegisterStatus[id];  
  15.     if (status !== 'finish') {  
  16.       // 加載子項目  
  17.       loadAsyncSubapp(id).then(({ routes, models }) => {  
  18.         loadModule(id, models);  
  19.         setAyncComponent(routes);  
  20.         setAyncLoaded(false);  
  21.         // 已經加載過的,做個標記  
  22.         subAppRegisterStatus[id] = 'finish';  
  23.       }).catch((error = {}) => {  
  24.         // 如果加載失敗,顯示錯誤信息  
  25.         setAyncLoaded(false);  
  26.         setAyncComponent(  
  27.           <div style={{  
  28.             margin: '100px auto',  
  29.             textAlign: 'center',  
  30.             color: 'red',  
  31.             fontSize: '20px'  
  32.           }}  
  33.           >  
  34.             {error.message || '加載失敗'}  
  35.           </div>);  
  36.       });  
  37.     } else {  
  38.       const models = subappModels[id];  
  39.       loadModule(id, models);  
  40.       // 如果能匹配上前綴則加載相應子工程模塊  
  41.       setAyncLoaded(false);  
  42.       setAyncComponent(subappRoutes[id]);  
  43.     }  
  44.   }, [id]);  
  45.   return (  
  46.     <Spin spinning={ayncLoading} style={{ width: '100%', minHeight: '100%' }}>  
  47.       {ayncComponent}  
  48.     </Spin>  
  49.   );  

子項目設計

子項目以靜態資源的形式在基座項目中加載,需要暴露出子系統自己的全部頁面組件和數據model;然后在打包構建上和以前也稍許不同,需要多生成一個manifest.json來搜集子項目的靜態資源信息。

子項目暴露出自己自愿的代碼長這樣: 

  1. // 子項目資源輸出代碼  
  2. import routes from './layouts';  
  3. const models = {};  
  4. function importAll(r) {  
  5.   r.keys().forEach(key => models[key] = r(key).default);  
  6.  
  7. // 搜集所有頁面的model  
  8. importAll(require.context('./pages', true, /model\.js$/));  
  9. function registerApp(dep) {  
  10.   return {  
  11.     routes, // 子工程路由組件  
  12.     models, // 子工程數據模型集合  
  13.   };  
  14.  
  15. // 數組第一個參數為子項目id,第二個參數為子項目模塊獲取函數  
  16. (window["registerApp"] = window["registerApp"] || []).push(['collection', registerApp]); 

子項目頁面組件搜集: 

  1. import menus from 'configs/menus';  
  2. import { Switch, Redirect, Route } from 'react-router-dom';  
  3. import pages from 'pages';  
  4. function flattenMenu(menus) {  
  5.   const result = [];  
  6.   menus.forEach((menu) => {  
  7.     if (menu.children) {  
  8.       result.push(...flattenMenu(menu.children));  
  9.     } else {  
  10.       menu.Component = pages[menu.component];  
  11.       result.push(menu);  
  12.     }  
  13.   });  
  14.   return result;  
  15.  
  16. // 子項目自己路徑分別 + /subpage/xxx   
  17. const prefixRoutes = flattenMenu(menus);  
  18. export default (  
  19.   <Switch>  
  20.     {prefixRoutes.map(child =>  
  21.       <Route  
  22.         exact  
  23.         key={child.key}  
  24.         path={child.path}  
  25.         component={child.Component}  
  26.         breadcrumbName={child.title}  
  27.       />  
  28.     )}  
  29.     <Redirect to="/home" />  
  30.   </Switch>); 

靜態資源加載邏輯設計

開始做方案時,只是設計出按需加載的交互體驗:即當業務切換到子項目路徑時,開始加載子項目的資源,然后渲染頁面。但后面感覺這種改動影響了業務體驗,他們以前只需要加載數據時loading,現在還需要承受子項目加載loading。所以為了讓業務盡量小的感知系統的重構,將按需加載換成了按需提前加載。簡單點說,就是當業務登錄時,我們會去遍歷他的所有權限菜單,獲取他擁有那些子項目的訪問權限,然后提前加載這些資源。

遍歷菜單,提前加載子項目資源: 

  1. // 本地開發環境不提前按需加載  
  2. if (getDeployEnv() !== 'local') {  
  3.   const apps = getIncludeAppMap();  
  4.   // 按需提前加載子項目資源;  
  5.   apps.forEach(subKey => startAsyncSubapp(subKey));  

然后就是show代碼的時候了,思路參考webpackJsonp,就是通過攔截一個全局數組的push操作,得知子項目已加載完成: 

  1. import { subAppMapInfo } from './menus';  
  2. // 子項目靜態資源映射表存放:  
  3. /**  
  4.  * 狀態定義:  
  5.  * '': 還未加載  
  6.  * ‘start’:靜態資源映射表已存在;  
  7.  * ‘map’:靜態資源映射表已存在;  
  8.  * 'init': 靜態資源已加載;  
  9.  * 'wait': 資源加載已完成, 待注入;  
  10.  * 'finish': 模塊已注入;  
  11. */  
  12. export const subAppRegisterStatus = {};  
  13. export const subappSourceInfo = {};  
  14. // 項目加載待處理的Promise hash 表  
  15. const defferPromiseMap = {};  
  16. // 項目加載待處理的錯誤 hash 表  
  17. const errorInfoMap = {};  
  18. // 加載css,js 資源  
  19. function loadSingleSource(url) {  
  20.   // 此處省略了一寫代碼  
  21.   return new Promise((resolove, reject) => {  
  22.     link.onload = () => {  
  23.       resolove(true);  
  24.     };  
  25.     link.onerror = () => {  
  26.       reject(false);  
  27.     };  
  28.   });  
  29.  
  30. // 加載json中包含的所有靜態資源  
  31. async function loadSource(json) {  
  32.   const keys = Object.keys(json);  
  33.   const isOk = await Promise.all(keys.map(key => loadSingleSource(json[key])));  
  34.   if (!isOk || isOk.filter(res => res === true) < keys.length) {  
  35.     return false;  
  36.   }  
  37.   return true;  
  38.  
  39. // 獲取子項目的json 資源信息  
  40. async function getManifestJson(subKey) {  
  41.   const url = subAppMapInfo[subKey];  
  42.   if (subappSourceInfo[subKey]) {  
  43.     return subappSourceInfo[subKey];  
  44.   }  
  45.   const json = await fetch(url).then(response => response.json())  
  46.     .catch(() => false);  
  47.   subAppRegisterStatus[subKey] = 'map';  
  48.   return json;  
  49.  
  50. // 子項目提前按需加載入口  
  51. export async function startAsyncSubapp(moduleName) {  
  52.   subAppRegisterStatus[moduleName] = 'start'; // 開始加載  
  53.   const json = await getManifestJson(moduleName);  
  54.   const [, reject] = defferPromiseMap[moduleName] || [];  
  55.   if (json === false) {  
  56.     subAppRegisterStatus[moduleName] = 'error';  
  57.     errorInfoMap[moduleName] = new Error(`模塊:${moduleName}, manifest.json 加載錯誤`);  
  58.     reject && reject(errorInfoMap[moduleName]);  
  59.     return;  
  60.   }  
  61.   subAppRegisterStatus[moduleName] = 'map'; // json加載完畢  
  62.   const isOk = await loadSource(json);  
  63.   if (isOk) {  
  64.     subAppRegisterStatus[moduleName] = 'init';  
  65.     return;  
  66.   }  
  67.   errorInfoMap[moduleName] = new Error(`模塊:${moduleName}, 靜態資源加載錯誤`);  
  68.   reject && reject(errorInfoMap[moduleName]);  
  69.   subAppRegisterStatus[moduleName] = 'error';  
  70.  
  71. // 回調處理  
  72. function checkDeps(moduleName) {  
  73.   if (!defferPromiseMap[moduleName]) {  
  74.     return;  
  75.   }  
  76.   // 存在待處理的,開始處理;  
  77.   const [resolove, reject] = defferPromiseMap[moduleName];  
  78.   const registerApp = subappSourceInfo[moduleName];  
  79.   try {  
  80.     const moduleExport = registerApp();  
  81.     resolove(moduleExport);  
  82.   } catch (e) {  
  83.     reject(e);  
  84.   } finally {  
  85.     // 從待處理中清理掉  
  86.     defferPromiseMap[moduleName] = null;  
  87.     subAppRegisterStatus[moduleName] = 'finish';  
  88.   }  
  89.  
  90. // window.registerApp.push(['collection', registerApp])  
  91. // 這是子項目注冊的核心,靈感來源于webpack,即對window.registerApp的push操作進行攔截  
  92. export function initSubAppLoader() {  
  93.   window.registerApp = [];  
  94.   const originPush = window.registerApp.push.bind(window.registerApp);  
  95.   // eslint-disable-next-line no-use-before-define  
  96.   window.registerApp.push = registerPushCallback 
  97.   function registerPushCallback(module = []) {  
  98.     const [moduleName, register] = module;  
  99.     subappSourceInfo[moduleName] = register;  
  100.     originPush(module);  
  101.     checkDeps(moduleName);  
  102.   }  
  103.  
  104. // 按需提前加載入口  
  105. export function loadAsyncSubapp(moduleName) {  
  106.   const subAppInfo = subAppRegisterStatus[moduleName];  
  107.   // 錯誤處理優先  
  108.   if (subAppInfo === 'error') {  
  109.     const error = errorInfoMap[moduleName] || new Error(`模塊:${moduleName}, 資源加載錯誤`);  
  110.     return Promise.reject(error);  
  111.   }  
  112.   // 已經提前加載,等待注入  
  113.   if (typeof subappSourceInfo[moduleName] === 'function') {  
  114.     return Promise.resolve(subappSourceInfo[moduleName]());  
  115.   }  
  116.   // 還未加載的,就開始加載,已經開始加載的,直接返回  
  117.   if (!subAppInfo) {  
  118.     startAsyncSubapp(moduleName);  
  119.   }  
  120.   return new Promise((resolve, reject = (error) => { throw error; }) => {  
  121.     // 加入待處理map中;  
  122.     defferPromiseMap[moduleName] = [resolve, reject];  
  123.   });  

這里需要強調一下子項目有兩種加載場景:

  •  從基座頁面路徑進入系統, 那么就是按需提前加載的場景, 那么startAsyncSubapp先執行,提前緩存資源;
  •  從子項目頁面路徑進入系統, 那就是按需加載的場景,就存在loadAsyncSubapp先執行,利用Promise完成發布訂閱。至于為什么startAsyncSubapp在前但后執行,是因為useEffect是組件掛載完成才執行;

至此,框架的大致邏輯就交代清楚了,剩下的就是優化了。

其他難點

其實不難,只是怪我太菜,但這些點確實值得記錄,分享出來共勉。

公共依賴共享

我們由于基座項目與子項目技術棧一致,另外又是拆分系統,所以共享公共庫依賴,優化打包是一個特別重要的點,以為就是webpack配個external就完事,但其實要復雜的多。

antd 構建

antd 3.x就支持了esm,即按需引入,但由于我們構建工具沒有做相應升級,用了babel-plugin-import這個插件,所以導致了兩個問題,打包冗余與無法全量導出antd Modules。分開來講:

  •  打包冗余,就是通過BundleAnalyzer插件發現,一個模塊即打了commonJs代碼,也打了Esm代碼;
  •  無法全量導出,因為基座項目不知道子項目會具體用哪個模塊,所以只能暴力的導出Antd所有模塊,但babel-plugin-import這個插件有個優化,會分析引入,然后刪除沒用的依賴,但我們的需求和它的目的是沖突的;

結論:使用babel-plugin-import這個插件打包commonJs代碼已經過時, 其存在的唯一價值就是還可以幫我們按需引入css 代碼;

項目公共組件共享

項目中公共組件的共享,我們開始嘗試將常用的組件加入公司組件庫來解決,但發現這個方案并不是最理想的,第一:很多組件和業務場景強相關,加入公共組件庫,會造成組件庫臃腫;第二:沒有必要。所以我們最后還是采用了基座項目收集組件,并統一暴露: 

  1. function combineCommonComponent() {  
  2.  const contexts = require.context('./components/common', true, /\.js$/);  
  3.  return contexts.keys().reduce((next, key) => {  
  4.    // 合并components/common下的組件  
  5.    const compName = key.match(/\w+(?=\/index\.js)/)[0];  
  6.    next[compName] = contexts(key).default;  
  7.    return next;  
  8.  }, {});  

webpackJsonp 全局變量污染

如果對webpack構建后的代碼不熟悉,可以先看看開篇提到的那篇文章。

webpack構建時,在開發環境modules是一個對象,采用文件path作為module的key; 而正式環境,modules是一個數組,會采用index作為module的key。

由于我基座項目和子項目沒有做沙箱隔離,即window被公用,所以存在webpackJsonp全局變量污染的情況,在開發環境,這個污染沒有被暴露,因為文件Key是唯一的,但在打正式包時,發現qa 環境子項目無法加載,最后一分析,發現了window.webpackJsonp 環境變量污染的bug。

最后解決的方案就是子項目打包都擁有自己獨立的webpackJsonp變量,即將webpackJsonp重命名,寫了一個簡單的webpack插件搞定: 

  1. // 將webpackJsonp 重命名為 webpackJsonpCollect  
  2. config.plugins.push(new RenameWebpack({ replace: 'webpackJsonpCollect' })); 

子項目開發熱加載

基座項目為什么會成為基座,就因為他迭代少且穩定的特殊性。但開發時,由于子項目無法獨立運行,所以需要依賴基座項目聯調。但做一個需求,要打開兩個vscode,同時運行兩個項目,對于那個開發,這都是一個不好的開發體驗,所以我們希望將dev環境作為基座,來支持本地的開發聯調,這才是最好的體驗。

將dev環境的構建參數改成開發環境后,發現子項目能在線上基座項目運行,但webSocket通信一直失敗,最后找到原因是webpack-dev-sever有個host check邏輯,稱為主機檢查,是一個安全選項,我們這里是可以確認的,所以直接注釋就行。 

 

責任編輯:龐桂玉 來源: segmentfault
相關推薦

2016-12-16 19:06:02

擴展數據庫架構

2022-10-09 14:05:24

前端single-spa

2021-12-15 07:24:56

SocketTCPUDP

2013-11-28 14:34:30

微軟WP

2022-01-26 00:05:00

AOPRPC遠程調用

2021-06-21 09:36:44

微信語音轉發

2020-05-28 10:45:31

Git分支合并

2012-05-17 11:04:18

匈牙利命名法

2013-02-22 09:49:43

大數據谷歌大數據全球技術峰會

2021-10-28 18:58:57

動態規劃數據結構算法

2024-09-25 08:22:06

2021-11-03 05:37:22

Windows 11操作系統微軟

2015-02-11 09:35:09

iPhone6

2019-01-02 10:49:54

Tomcat內存HotSpot VM

2024-06-03 09:52:08

2017-10-25 14:44:37

PackageManaApp信息

2017-04-13 10:15:18

機器人工作

2015-12-02 11:38:48

物聯網智慧時代

2022-04-26 18:08:21

C語言代碼編程規范

2020-03-05 16:47:51

Git內部儲存
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产精品成人一区二区三区 | 精品欧美一区二区三区久久久小说 | 美女啪啪国产 | 亚洲一区二区在线视频 | 狠狠爱一区二区三区 | 久久久久久综合 | 国产91丝袜在线18 | 日韩欧美三区 | 成年人在线观看 | 欧美人人| 99精品网| 色橹橹欧美在线观看视频高清 | 日日夜夜天天久久 | 亚洲成人午夜在线 | 另类一区| 国产小u女发育末成年 | 国产欧美一区二区三区日本久久久 | 欧美日在线 | 久久国产精品视频免费看 | 欧美福利影院 | 欧美色a v | 一区二区三区国产视频 | 亚洲精品一区在线观看 | 一区二区三区四区不卡 | 欧美一级在线观看 | 手机看片1 | 亚洲一区视频在线播放 | a久久| 手机av在线 | 国产精品久久久久久久久久久久 | 久久99视频精品 | 成人免费在线播放视频 | 久久网一区二区三区 | 啪啪免费| 日韩精品在线播放 | 一区影视| 亚洲欧洲一区二区 | 91视视频在线观看入口直接观看 | 精品久久久久国产免费第一页 | 久久久成人一区二区免费影院 | 中文字幕在线不卡 |