基於 qiankun 的 CMS 應用微前端實踐

圖片來源: https://zhuanlan.zhihu.com/p/...
本文做者:史志鵬

前言

LOOK 直播運營後臺工程是一個迭代了 2+ 年,累計超過 10+ 位開發者參與業務開發,頁面數量多達 250+ 的「巨石應用」。代碼量的龐大,帶來了構建、部署的低效,此外該工程依賴內部的一套 Regularjs 技術棧也已經完成了歷史使命,相應的 UI 組件庫、工程腳手架也被推薦中止使用,走向了少維護或者不維護的階段。所以, LOOK 直播運營後臺基於 React 新建工程、作工程拆分被提上了工做日程。一句話描述目標就是:新的頁面將在基於 React 的新工程開發, React 工程能夠獨立部署,而 LOOK 直播運營後臺對外輸出的訪問地址指望維持不變。
本文基於 LOOK 直播運營後臺的微前端落地實踐總結而成。主要介紹在既有「巨石應用」、 Regularjs 和 React 技術棧共存的場景下,使用微前端框架 qiankun ,實現CMS應用的微前端落地歷程。
關於 qiankun 的介紹,請移步至官方查閱,本文不會側重於介紹有關微前端的概念。javascript

一.背景

1.1 現狀

  1. 如上所述,存在一個以下圖所示的 CMS 應用,這個應用的工程咱們稱之爲 liveadmin ,訪問地址爲:https://example.com/liveadmin,訪問以下圖所示。

  1. 咱們但願再也不在 liveadmin 舊工程新增新業務頁面,所以咱們基於內部的一個 React 腳手架新建了一個稱爲 increase 的新工程,新的業務頁面都推薦使用這個工程開發,這個應用能夠獨立部署獨立訪問,訪問地址爲:https://example.com/lookadmin,訪問以下圖所示:

1.2 目標

咱們但願使用微前端的方式,集成這兩個應用的全部菜單,讓用戶無感知這個變化,依舊按照原有的訪問方式 https://example.com/liveadmin,能夠訪問到 liveadmin 和 increase 工程的全部頁面。
針對這樣一個目標,咱們須要解決如下兩個核心問題:html

  1. 兩個系統的菜單合成展現;
  2. 使用原有訪問地址訪問兩個應用的頁面。

對於第 2 個問題,相信對 qiankun 瞭解的同窗能夠和咱們同樣達成共識,至於第 1 個問題,咱們在實踐的過程當中,經過內部的一些方案獲得解決。下文在實現的過程會加以描述。這裏咱們先給出整個項目落地的效果圖:

能夠看到, increase 新工程的一級菜單被追加到了 liveadmin 工程的一級菜單後面,原始地址能夠訪問到兩個工程的全部的菜單。前端

1.3 權限管理

說到 CMS,還須要說一下權限管理系統的實現,下文簡稱 PMS。html5

  1. 權限:目前在咱們的 PMS 裏定義了兩種類型的權限:頁面權限(決定用戶是否能夠看到某個頁面)、功能權限(決定用戶是否能夠訪問某個功能的 API )。前端負責頁面權限的實現,功能權限則由服務端進行管控。
  2. 權限管理:本文僅闡述頁面權限的管理。首先每一個前端應用都關聯一個 PMS 的權限應用,好比 liveadmin 關聯的是 appCode = live_backend 這個權限應用。在前端應用工程部署成功後,經過後門的方式推送前端工程的頁面和頁面關聯的權限碼數據到 PMS。風控運營在 PMS 系統中找到對應的權限應用,按照角色粒度分配頁面權限,擁有該角色的用戶便可訪問該角色被分配的頁面。
  3. 權限控制:在前端應用被訪問時,最外層的模塊負責請求當前用戶的頁面權限碼列表,而後根據此權限碼列表過濾出能夠訪問的有效菜單,並註冊有效菜單的路由,最後生成一個當前用戶權限下的合法菜單應用。

二.實現

2.1 lookcms 主應用

  1. 首先,新建一個 CMS 基礎工程,定義它爲主應用 lookcms,具備基本的請求權限和菜單數據、渲染菜單的功能。

入口文件執行如下請求權限和菜單數據、渲染菜單的功能。java

// 使用 Redux Store 處理數據
const store = createAppStore(); 
// 檢查登陸狀態
store.dispatch(checkLogin());
// 監聽異步登陸狀態數據
const unlistener = store.subscribe(() => {
 unlistener();
 const { auth: { account: { login, name: userName } } } = store.getState();
 if (login) { // 若是已登陸,根據當前用戶信息請求當前用戶的權限和菜單數據
 store.dispatch(getAllMenusAndPrivileges({ userName }));
 subScribeMenusAndPrivileges();
 } else {
 injectView(); // 未登陸則渲染登陸頁面
 }
});
// 監聽異步權限和菜單數據
const subScribeMenusAndPrivileges = () => {
 const unlistener = store.subscribe(() => {
 unlistener();
 const { auth: { privileges, menus, allMenus, account } } = store.getState();
 store.dispatch(setMenus(menus)); // 設置主應用的菜單,據此渲染主應用 lookcms 的菜單
 injectView(); // 掛載登陸態的視圖
 // 啓動qiankun,並將菜單、權限、用戶信息等傳遞,用於後續傳遞給子應用,攔截子應用的請求
 startQiankun(allMenus, privileges, account, store); 
 });
};
// 根據登陸狀態渲染頁面
const injectView = () => {
 const { auth: { account: { login } } } = store.getState();
 if (login) {
 new App().$inject('#j-main');
 } else {
 new Auth().$inject('#j-main');
 window.history.pushState({}, '', `${$config.rootPath}/auth?redirect=${window.location.pathname}`);
 }
};
  1. 引入 qiankun,註冊 liveadmin 和 increase 這兩個子應用。

定義好子應用,按照 qiankun 官方的文檔,肯定 name、entry、container 和 activeRule 字段,其中 entry 配置注意區分環境,並接收上一步的 menus, privileges等數據,基本代碼以下:node

// 定義子應用集合
const subApps = [{ // liveadmin 舊工程
 name: 'music-live-admin', // 取子應用的 package.json 的 name 字段
 entrys: { // entry 區分環境
 dev: '//localhost:3001',
 // liveadmin這裏定義 rootPath爲 liveadminlegacy,便於將原有的 liveadmin 釋放給主應用使用,以達到使用原始訪問地址訪問頁面的目的。
 test: `//${window.location.host}/liveadminlegacy/`,
 online: `//${window.location.host}/liveadminlegacy/`,
 },
 pmsAppCode: 'live_legacy_backend', // 權限處理相關
 pmsCodePrefix: 'module_livelegacyadmin', // 權限處理相關
 defaultMenus: ['welcome', 'activity']
}, { // increase 新工程
 name: 'music-live-admin-react',
 entrys: {
 dev: '//localhost:4444',
 test: `//${window.location.host}/lookadmin/`,
 online: `//${window.location.host}/lookadmin/`,
 },
 pmsAppCode: 'look_backend',
 pmsCodePrefix: 'module_lookadmin',
 defaultMenus: []
}];
// 註冊子應用
registerMicroApps(subApps.map(app => ({
 name: app.name,
 entry: app.entrys[$config.env], // 子應用的訪問入口
 container: '#j-subapp', // 子應用在主應用的掛載點
 activeRule: ({ pathname }) => { // 定義加載當前子應用的路由匹配策略,此處是根據 pathname 和當前子應用的菜單 key 比較來作的判斷
 const curAppMenus = allMenus.find(m => m.appCode === app.pmsAppCode).subMenus.map(({ name }) => name);
 const isInCurApp = !!app.defaultMenus.concat(curAppMenus).find(headKey => pathname.indexOf(`${$config.rootPath}/${headKey}`) > -1);
 return isInCurApp;
 },
 // 傳遞給子應用的數據:菜單、權限、帳戶,可使得子應用再也不請求相關數據,固然子應用須要作好判斷
 props: { menus: allMenus.find(m => m.appCode === app.pmsAppCode).subMenus, privileges, account }
})));
// ...
start({ prefetch: false });
  1. 主應用菜單邏輯

咱們基於已有的 menus 菜單數據,使用內部的 UI 組件完成了菜單的渲染,對每個菜單綁定了點擊事件,點擊後經過 pushState 的方式,變動窗口的路徑。好比點擊 a-b 菜單,對應的路由即是 http://example.com/liveadmin/a/b,qiankun 會響應路由的變化,根據定義的 activeRule 匹配到對應的的子應用,接着子應用接管路由,加載子應用對應的頁面資源。詳細的實現過程能夠參考 qiankun 源碼,基本的思想是清洗子應用入口返回的 html 中的 <script> 標籤 ,fetch 模塊的 Javascript 資源,而後經過 eval 執行對應的 Javascript。react

2.2 liveadmin 子應用

  1. 按照 qiankun 官方文檔的作法,在子應用的入口文件中導出相應的生命週期鉤子函數。
if (window.__POWERED_BY_QIANKUN__) { // 注入 Webpack publicPath, 使得主應用正確加載子應用的資源
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
if (!window.__POWERED_BY_QIANKUN__) { // 獨立訪問啓動邏輯
 bootstrapApp({});
}
export const bootstrap = async () => { // 啓動前鉤子
 await Promise.resolve(1);
};
export const mount = async (props) => { // 集成訪問啓動邏輯,接手主應用傳遞的數據
 bootstrapApp(props);
};
export const unmount = async (props) => {  // 卸載子應用的鉤子
 props.container.querySelector('#j-look').remove();
};
  1. 修改 Webpack 打包配置。
output: {
 path: DIST_PATH,
 publicPath: ROOTPATH,
 filename: '[name].js',
 chunkFilename: '[name].js',
 library: `${packageName}-[name]`,
 libraryTarget: 'umd', // 指定打包的 Javascript UMD 格式
 jsonpFunction: `webpackJsonp_${packageName}`,
},
  1. 處理集成訪問時,隱藏子應用的頭部和側邊欄元素。
const App = Regular.extend({
 template: window.__POWERED_BY_QIANKUN__
 ? `
 <div class="g-wrapper" r-view></div>
 `
 : `
 <div class="g-bd">
 <div class="g-hd mui-row">
 <AppHead menus={headMenus}
 moreMenus={moreMenus}
 selected={selectedHeadMenuKey}
 open={showSideMenu}
 on-select={actions.selectHeadMenu($event)}
 on-toggle={actions.toggleSideMenu()}
 on-logout={actions.logoutAuth}></AppHead>
 </div>
 <div class="g-main mui-row">
 <div class="g-sd mui-col-4" r-hide={!showSideMenu}>
 <AppSide menus={sideMenus} 
 selected={selectedSideMenuKey}
 show={showSideMenu}
 on-select={actions.selectSideMenu($event)}></AppSide>
 </div>
 <div class="g-cnt" r-class={cntClass}>
 <div class="g-wrapper" r-view></div>
 </div>
 </div> 
 </div> 
 `,
 name: 'App',
 // ...
})
  1. 處理集成訪問時,屏蔽權限數據和登陸信息的請求,改成接收主應用傳遞的權限和菜單數據,避免冗餘的 HTTP 請求和數據設置。
if (props.container) { // 集成訪問時,直接設置權限和菜單
 store.dispatch(setMenus(props.menus))
 store.dispatch({
 type: 'GET_PRIVILEGES_SUCCESS',
 payload: {
 privileges: props.privileges,
 menus: props.menus
 }
 });
} else { // 獨立訪問時,請求用戶權限,菜單直接讀取本地的配置
 MixInMenus(props.container);
 store.dispatch(getPrivileges({ userName: name }));
}
if (props.container) {  // 集成訪問時,設置用戶登陸帳戶
 store.dispatch({
 type: 'LOGIN_STATUS_SUCCESS',
 payload: {
 user: props.account,
 loginType: 'OPENID'
 }
 });
} else { // 獨立訪問時,請求和設置用戶登陸信息
 store.dispatch(loginStatus());
}
  1. 處理集成訪問時,路由 base 更改

由於集成訪問時要統一 rootPath 爲 liveadmin,因此集成訪問時註冊的路由要修改爲主應用的 rootPath 以及新的掛載點。webpack

const start = (container) => {
 router.start({
 root: config.base,
 html5: true,
 view: container ? container.querySelector('#j-look') : Regular.dom.find('#j-look')
 });
};

2.3 increase 子應用

同 liveadmin 子應用作的事相似。git

  1. 導出相應的生命週期鉤子。
if (window.__POWERED_BY_QIANKUN__) {
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
const CONTAINER = document.getElementById('container');
if (!window.__POWERED_BY_QIANKUN__) {
 const history = createBrowserHistory({ basename: Config.base });
 ReactDOM.render(
 <Provider store={store()}>
 <Symbol />
 <Router path="/" history={history}>
 {routeChildren()}
 </Router>
 </Provider>,
 CONTAINER
 );
}
export const bootstrap = async () => {
 await Promise.resolve(1);
};
export const mount = async (props) => {
 const history = createBrowserHistory({ basename: Config.qiankun.base });
 ReactDOM.render(
 <Provider store={store()}>
 <Symbol />
 <Router path='/' history={history}>
 {routeChildren(props)}
 </Router>
 </Provider>,
 props.container.querySelector('#container') || CONTAINER
 );
};
export const unmount = async (props) => {
 ReactDOM.unmountComponentAtNode(props.container.querySelector('#container') || CONTAINER);
};
  1. Webpack 打包配置。
output: {
 path: DIST_PATH,
 publicPath: ROOTPATH,
 filename: '[name].js',
 chunkFilename: '[name].js',
 library: `${packageName}-[name]`,
 libraryTarget: 'umd',
 jsonpFunction: `webpackJsonp_${packageName}`,
},
  1. 集成訪問時,去掉頭部和側邊欄。
if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-line
 return (
 <BaseLayout location={location} history={history} pms={pms}>
 <Fragment>
 {
 curMenuItem && curMenuItem.block
 ? blockPage
 : children
 }
 </Fragment>
 </BaseLayout>
 );
}
  1. 集成訪問時,屏蔽權限和登陸請求,接收主應用傳遞的權限和菜單數據。
useEffect(() => {
 if (login.status === 1) {
 history.push(redirectUrl);
 } else if (pms.account) { // 集成訪問,直接設置數據
 dispatch('Login/success', pms.account);
 dispatch('Login/setPrivileges', pms.privileges);
 } else { // 獨立訪問,請求數據
 loginAction.getLoginStatus().subscribe({
 next: () => {
 history.push(redirectUrl);
 },
 error: (res) => {
 if (res.code === 301) {
 history.push('/login', {
 redirectUrl,
 host
 });
 }
 }
 });
 }
});
  1. 集成訪問時,更改 react-router base。
export const mount = async (props) => {
 const history = createBrowserHistory({ basename: Config.qiankun.base });
 ReactDOM.render(
 <Provider store={store()}>
 <Symbol />
 <Router path='/' history={history}>
 {routeChildren(props)}
 </Router>
 </Provider>,
 props.container.querySelector('#container') || CONTAINER
 );
};

2.4 權限集成(可選步驟)

  1. 上文提到,一個前端應用關聯一個 PMS 權限應用,那麼若是經過微前端的方式組合了每一個前端應用,而每一個前端子應用若是還依然對應本身的 PMS 權限應用的權限,那麼站在權限管理人員的角度而言,就須要關注多個 PMS 權限應用,進行分配權限、管理角色,操做起來都很麻煩,好比兩個子應用的頁面區分,兩個子應用同一權限的角色管理等。所以,須要考慮將子應用對應的 PMS 權限應用也統一塊兒來,這裏僅描述咱們的處理方式,僅供參考。
  2. 要儘可能維持原有的權限管理方式(權限管理人員經過前端應用後門推送頁面權限碼到 PMS,而後到 PMS 進行頁面權限分配),則微前端場景下,權限集成須要作的事情能夠描述爲:github

    1. 各個子應用先推送本工程的菜單和權限碼數據到到各自的 PMS 權限應用。
    2. 主應用加載各子應用的菜單和權限碼數據,修改每一個菜單和權限碼的數據爲主應用對應的 PMS 權限應用數據,而後統一推送到主應用對應的 PMS 權限應用,權限管理人員能夠在主應用對應的 PMS 權限應用內進行權限的統一分配管理。
  3. 在咱們的實踐中,爲了使權限管理人員依舊不感知這種拆分應用帶來的變化,依舊使用原 liveadmin 應用對應的 appCode = live_backend PMS 權限應用進行權限分配,咱們須要把 liveadmin 對應的 PMS 權限應用更改成 lookcms 主應用對應的 PMS 權限應用,而爲 liveadmin 子應用新建一個 appCode = live_legacy_backend 的 PMS 權限應用,新的 increase 子應用則繼續對應 appCode = look_backend 這個PMS 權限應用。以上兩個子應用的菜單和權限碼數據按照上一步描述的第 2 點各自上報給對應的 PMS 權限應用。最後 lookcms 主應用同時獲取 appCode = live_legacy_backend 和 appCode = look_backend 這兩個 PMS 權限應用的前端子應用菜單和權限碼數據,修改成 appCode = live_backend 的 PMS 權限應用數據,推送到 PMS,總體的流程以下圖所示,左邊是原有的系統設計,右邊是改造的系統設計。

2.5 部署

  1. liveadmin 和 increase 各自使用雲音樂的前端靜態部署系統進行獨立部署,主應用 lookcms 也是獨立部署。
  2. 處理好主應用訪問子應用資源跨域的問題。在咱們的實踐過程當中,因爲都部署在同一個域下,資源打包遵循了同域規則。

2.6 小結

自此,咱們已經完成了基於 qiankun LOOK 直播運營後臺的微前端的實現,主要是新建了主工程,劃分了主應用的職責,同時修改了子工程,使得子應用能夠被集成到主應用被訪問,也能夠保持原有獨立訪問功能。總體的流程,能夠用下圖描述:

三.依賴共享

qiankun 官方並無推薦具體的依賴共享解決方案,咱們對此也進行了一些探索,結論能夠總結爲:對於 Regularjs,React 等 Javascript 公共庫的依賴的能夠經過 Webpack 的 externals 和 qiankun 加載子應用生命週期函數以及 import-html-entry 插件來解決,而對於組件等須要代碼共享的場景,則可使用 Webapck 5 的 module federation plugin 來解決。具體方案以下:
3.1. 咱們整理出的公共依賴分爲兩類
3.1.1. 一類是基礎庫,好比 Regularjs,Regular-state,MUI,React,React Router 等指望在整個訪問週期中不要重複加載的資源。
3.1.2. 另外一類是公共組件,好比 React 組件須要在各子應用之間互相共享,不須要進行工程間的代碼拷貝。
3.2. 對於以上兩類依賴,咱們作了一些本地的實踐,由於尚未迫切的業務需求以及 Webpack 5 暫爲發佈穩定版(截至本文發佈時,Webpack 5 已經發布了 release 版本,後續看具體的業務需求是否上線此部分 feature ),所以尚未在生產環境驗證,但在這裏能夠分享下處理方式和結果。
3.2.1. 對於第一類公共依賴,咱們實現共享的指望的是:在集成訪問時,主應用能夠動態加載子應用強依賴的庫,子應用自身再也不加載,獨立訪問時,子應用自己又能夠自主加載自身須要的依賴。這裏就要處理好兩個問題:a. 主應用怎麼蒐集和動態加載子應用的依賴 b. 子應用怎麼作到集成和獨立訪問時對資源加載的不一樣表現。
3.2.1.1. 第一個問題,咱們須要維護一個公共依賴的定義,即在主應用中定義每一個子應用所依賴的公共資源,在 qiankun 的全局微應用生命週期鉤子 beforeLoad 中經過插入 <script> 標籤的方式,加載當前子應用所需的 Javascript 資源,參考代碼以下。

// 定義子應用的公共依賴
const dependencies = {
 live_backend: ['regular', 'restate'],
 look_backend: ['react', 'react-dom']
};
// 返回依賴名稱
const getDependencies = appName => dependencies[appName];
// 構建script標籤
const loadScript = (url) => {
 const script = document.createElement('script');
 script.type = 'text/javascript';
 script.src = url;
 script.setAttribute('ignore', 'true'); // 避免重複加載
 script.onerror = () => {
 Message.error(`加載失敗${url},請刷新重試`);
 };
 document.head.appendChild(script);
};
// 加載某個子應用前加載當前子應用的所需資源
beforeLoad: [
 (app) => {
 console.log('[LifeCycle] before load %c%s', 'color: green;', app.name);
 getDependencies(app.name).forEach((dependency) => {
 loadScript(`${window.location.origin}/${$config.rootPath}${dependency}.js`);
 });
 }
],

這裏還要注意經過 Webpack 來生產好相應的依賴資源,咱們使用的是 copy-webpack-plugin 插件將 node_modules 下的 release 資源轉換成包成能夠經過獨立 URL 訪問的資源。

// 開發
plugins: [
 new webpack.DefinePlugin({
 'process.env': {
 NODE_ENV: JSON.stringify('development')
 }
 }),
 new webpack.NoEmitOnErrorsPlugin(),
 new CopyWebpackPlugin({
 patterns: [
 { from: path.join(__dirname, '../node_modules/regularjs/dist/regular.js'), to: '../s/regular.js' },
 { from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 { from: path.join(__dirname, '../node_modules/react/umd/react.development.js'), to: '../s/react.js' },
 { from: path.join(__dirname, '../node_modules/react-dom/umd/react-dom.development.js'), to: '../s/react-dom.js' }
 ]
 })
],
// 生產
new CopyWebpackPlugin({
 patterns: [
 { from: path.join(__dirname, '../node_modules/regularjs/dist/regular.min.js'), to: '../s/regular.js' },
 { from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 { from: path.join(__dirname, '../node_modules/react/umd/react.production.js'), to: '../s/react.js' },
 { from: path.join(__dirname, '../node_modules/react-dom/umd/react-dom.production.js'), to: '../s/react-dom.js' }
 ]
})

3.2.1.2. 關於子應用集成和獨立訪問時,對公共依賴的二次加載問題,咱們採用的方法是,首先子應用將主應用已經定義的公共依賴經過 copy-webpack-plugin 和 html-webpack-externals-plugin 這兩個插件使用 external 的方式獨立出來,不打包到 Webpack bundle 中,同時經過插件的配置,給 <script> 標籤加上 ignore 屬性,那麼在 qiankun 加載這個子應用時使用,qiankun 依賴的 import-html-entry 插件分析到 <script> 標籤時,會忽略加載有 ignore 屬性的 <script> 標籤,而獨立訪問時子應用自己能夠正常加載這個 Javascript 資源。

plugins: [
 new CopyWebpackPlugin({
 patterns: [
 { from: path.join(__dirname, '../node_modules/regularjs/dist/regular.js'), to: '../s/regular.js' },
 { from: path.join(__dirname, '../node_modules/regular-state/restate.pack.js'), to: '../s/restate.js' },
 ]
 }),
 new HtmlWebpackExternalsPlugin({
 externals: [{
 module: 'remoteEntry',
 entry: 'http://localhost:3000/remoteEntry.js'
 }, {
 module: 'regularjs',
 entry: {
 path: 'http://localhost:3001/regular.js',
 attributes: { ignore: 'true' }
 },
 global: 'Regular'
 }, {
 module: 'regular-state',
 entry: {
 path: 'http://localhost:3001/restate.js',
 attributes: { ignore: 'true' }
 },
 global: 'restate'
 }],
 })
],

3.2.2. 針對第二類共享代碼的場景,咱們調研了 Webpack 5 的 module federation plugin, 經過應用之間引用對方導入導出的 Webpack 編譯公共資源信息,來異步加載公共代碼,從而實現代碼共享。
3.2.2.1. 首先,咱們實踐所定義的場景是:lookcms 主應用同時提供基於 Regularjs 的 RButton 組件和基於 React 的 TButton 組件分別共享給 liveadmin 子應用和 increase 子應用。
3.2.2.2. 對於 lookcms 主應用,咱們定義 Webpack5 module federation plugin 以下:

plugins: [
 // new BundleAnalyzerPlugin(),
 new ModuleFederationPlugin({
 name: 'lookcms',
 library: { type: 'var', name: 'lookcms' },
 filename: 'remoteEntry.js',
 exposes: {
 TButton: path.join(__dirname, '../client/exports/rgbtn.js'),
 RButton: path.join(__dirname, '../client/exports/rcbtn.js'),
 },
 shared: ['react', 'regularjs']
 }),
],

定義的共享代碼組件以下圖所示:

3.2.2.3. 對於 liveadmin 子應用,咱們定義 Webpack5 module federation plugin 以下:

plugins: [
 new BundleAnalyzerPlugin(),
 new ModuleFederationPlugin({
 name: 'liveadmin_remote',
 library: { type: 'var', name: 'liveadmin_remote' },
 remotes: {
 lookcms: 'lookcms',
 },
 shared: ['regularjs']
 }),
],

使用方式上,子應用首先要在 html 中插入源爲 http://localhost:3000/remoteEntry.js 的主應用共享資源的入口,能夠經過 html-webpack-externals-plugin 插入,見上文子應用的公共依賴 external 處理。
對於外部共享資源的加載,子應用都是經過 Webpack 的 import 方法異步加載而來,而後插入到虛擬 DOM 中,咱們指望參考 Webapck 給出的 React 方案作 Regularjs 的實現,很遺憾的是 Regularjs 並無相應的基礎功能幫咱們實現 Lazy 和 Suspense。
經過一番調研,咱們選擇基於 Regularjs 提供的 r-component API 來條件渲染異步加載的組件。
基本的思想是定義一個 Regularjs 組件,這個 Regularjs 組件在初始化階段從 props 中獲取要加載的異步組件 name ,在構建階段經過 Webpack import 方法加載 lookcms 共享的組件 name,並按照 props 中定義的 name 添加到 RSuspense 組件中,同時修改 RSuspense 組件 r-component 的展現邏輯,展現 name 綁定的組件。
因爲 Regularjs 的語法書寫受限,咱們不便將上述 RSuspense 組件邏輯抽象出來,所以採用了 Babel 轉換的方式,經過開發人員定義一個組件的加載模式語句,使用 Babel AST 轉換爲 RSuspense 組件。最後在 Regularjs 的模版中使用這個 RSuspense 組件便可。

// 支持定義一個 fallback
const Loading = Regular.extend({
 template: '<div>Loading...{content}</div>',
 name: 'Loading'
});
// 寫成一個 lazy 加載的模式語句
const TButton = Regular.lazy(() => import('lookcms/TButton'), Loading);
// 模版中使用 Babel AST 轉換好的 RSuspense 組件
`<RSuspense origin='lookcms/TButton' fallback='Loading' />`

經過 Babel AST 作的語法轉換以下圖所示:

實際運行效果以下圖所示:

3.2.2.4. 對於 increase 子應用,咱們定義 Webpack 5 module federation plugin 以下:

plugins: [
 new ModuleFederationPlugin({
 name: 'lookadmin_remote',
 library: { type: 'var', name: 'lookadmin_remote' },
 remotes: {
 lookcms: 'lookcms',
 },
 shared: ['react']
 }),
],

使用方式上,參考 Webpack 5 的官方文檔便可,代碼以下:

const RemoteButton = React.lazy(() => import('lookcms/RButton'));
const Home = () => (
 <div className="m-home">
 歡迎
 <React.Suspense fallback="Loading Button">
 <RemoteButton />
 </React.Suspense>
 </div>
);

實際運行效果以下圖所示:

  1. 總結

四.注意事項

  1. 跨域資源
    若是你的應用內經過其餘方式實現了跨域資源的加載,請注意 qiankun 是經過 fetch 的方式加載全部子應用資源的,所以跨域的資源須要經過 CORS 實現跨域訪問。
  2. 子應用的 html 標籤
    可能你的某個子應用的 html 標籤上設置了某些屬性或者附帶了某些功能,要注意 qiankun 實際處理中剝離掉了子應用的 html 標籤,所以若是由設置 rem 的需求,請注意使用其餘方式適配。

五.將來

  1. 自動化
    子應用的接入經過平臺的方式接入,固然這須要子應用遵照的規範行程。
  2. 依賴共享
    Webpack 5 已經發布了其正式版本,所以對於 module federation plugin 的使用能夠提上工做日程。

六.總結

LOOK 直播運營後臺基於實際的業務場景,使用 qiankun 進行了微前端方式的工程拆分,目前在生產環境平穩運行了近 4 個月,在實踐的過程當中,確實在需求確立和接入 qiankun 的實現以及部署應用幾個階段碰到了一些難點,好比開始的需求確立,咱們對要實現的主菜單功能有過斟酌,在接入 qiankun 的過程當中常常碰到報錯,在部署的過程當中也遇到內部部署系統的抉擇和阻礙,好在同事們給力,項目能順利的上線和運行。

參考資料

本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe(at)corp.netease.com!