10分钟弄懂微应用框架——乾坤,真香!
前言
今天剛剛學習了一個微前端框架——乾坤,正著熱乎勁,寫一篇入門博客。這篇文章不會討論太多的原理和實現,只是一個入門寫 Hello World 的教程。
文章的很多地方都參考官網,但是官網的教程太簡潔了,個人覺得還是做不到無腦上手,希望這篇文章可以幫到正在入門微前端的伙伴。
想直接看代碼的,我寫了個比官網更簡單的例子,點擊即可學會。
什么是微前端
首先,來了解一下微前端是個啥。
當我們寫了一個又一個的 SPA 應用。突然有一天,老板說要將這些應用合并,前端工程們就頭大了——每個應用的代碼都是一座搖搖欲墜的💩山,別說合并了,就算動都不敢動呀。
雖然很麻煩,但是前端工程師還是把這個問題解決了,而這個將多個 SPA 合并成一個 Web App 的解決方案就是微前端。
為什么要微前端
“多個 SPA 合并成一個 Web App?”,可能有人會想到用 <iframe/> 也可以實現一個網頁里內嵌多個網頁呀。原因有:
不感知 url 狀態,比如前進、后退沒法玩
UI 不同步、DOM 不同享。<iframe/> 本質上是頁面的硬隔離,所以如果你有個遮罩層,可能只能在那一小片區域才展示遮罩層
頁面之間的通信很麻煩
每次都要加載子應用,速度很慢
而微前端正好可以補足上面的缺點。
微前端的優勢
除了解決了上面的問題,微前端還有如下的優點:
子應用技術棧無關,即類似上頁說的頁面硬隔離,但是是以 sandbox 的方式實現的
合并多個子應用,相對地,也可以將大應用拆解成多個應用,實現業務解耦
子應用高度自治,發布、報錯、測試流程僅限于子應用,不會受別的業務影響,同時也不影響別的業務
乾坤由來
最原始的微前端框架并不是乾坤,而是 single-spa。但是這個框架只提供最基本的功能,而且全是英文,文檔寫得也很繁瑣,應該沒人想去看。
阿里的乾坤則是基于 single-spa 開發的又一個微前端框架,提供了更多的功能,也解決一些坑,官網也很簡潔。
不過,個人覺得有點太簡潔了,寫 Hello World 的時候還是遇到一些坑,只能看 Github 的 /examples 目錄學習。
主應用 VS 子應用
首先,要知道現在項目并不是只有一個了,而是區分出 主應用 和 子應用,關系如下:
兩者區別:
主應用
概念:就是要統治各個子應用的應用,也即合并結果頁面
負責子應用的注冊、路由分發。可以簡單理解為 React.js 和 Vue.js 里的 App 組件,主要做一些初始化、路由注冊、全局狀態注冊、銷毀時的動作
子應用
概念:各個 SPA 應用,可以理解為 SPA 里的頁面組件
負責暴露一些函數,以此對接主應用,讓主應用知道:哦,原來你是子應用,要和我對接。常見的對應函數有:bootstrap, mount, unmount
項目創建
乾坤官網最推薦的做法是將主應用和子應用分成兩個項目,各自管理。當然,也可以一個項目里分成不同的目錄來存放。
├── main # 主應用 ├── baidu # 子應用 └── taobao # 子應用如果你覺得 官方的例子 太復雜,也可以看我自己建的 qiankun-bigass-app,子應用只有兩個用 React.js 的項目。我把很多無關的代碼都刪了。
實現主應用
理清上面的關系后,我們直接干代碼,先看主應用。
首先,我們弄一個 .html 文件出來,作為主頁面的 HTML 模板:
<body><div?class="mainapp"><!--?標題欄?--><header?class="mainapp-header"><h1>QianKun</h1></header><div?class="mainapp-main"><!--?側邊欄?--><ul?class="mainapp-sidemenu"><li?onclick="push('/taobao')">淘寶</li><li?onclick="push('/baidu')">百度</li></ul><!--?子應用??--><main?id="subapp-container"></main></div></div><script>function?push(subapp)?{?history.pushState(null,?subapp,?subapp)?}</script> </body>然后,使用 Webpack,指定為 template HTML,并配置 dev server,注意一定要配置 headers,不然會有跨域的問題,子應用同理:
//?webpack.config.js const?HtmlWebpackPlugin?=?require('html-webpack-plugin');module.exports?=?{entry:?'./index.js',devtool:?'source-map',devServer:?{open:?true,port:?'7099',clientLogLevel:?'warning',disableHostCheck:?true,compress:?true,headers:?{'Access-Control-Allow-Origin':?'*',},historyApiFallback:?true,overlay:?{?warnings:?false,?errors:?true?},},output:?{publicPath:?'/',},mode:?'development',resolve:?{extensions:?['.js',?'.jsx',?'.ts',?'.tsx'],},module:?{rules:?[{test:?/\.jsx?$/,exclude:?/node_modules/,use:?{loader:?'babel-loader',options:?{presets:?['@babel/preset-env'],plugins:?['@babel/plugin-transform-react-jsx'],},},},{test:?/\.(le|c)ss$/,use:?['style-loader',?'css-loader',?'less-loader'],},],},plugins:?[new?HtmlWebpackPlugin({filename:?'index.html',template:?process.env.MODE?===?'multiple'???'./multiple.html'?:?'./index.html',minify:?{removeComments:?true,collapseWhitespace:?true,},}),], };入口文件 index.js 就比較重要了,需要完成主應用的很多事情:
import?{?registerMicroApps,?runAfterFirstMounted,?setDefaultMountApp,?start,?initGlobalState?}?from?'qiankun'; import?'./index.less';/***?主應用?**可以使用任意技術棧***?以下分別是?React?和?Vue?的示例,可切換嘗試*/ import?render?from?'./Render'; //?import?render?from?'./render/VueRender';/***?Step1?初始化應用(可選)*/ render({?loading:?true?});const?loader?=?loading?=>?render({?loading?});/***?Step2?注冊子應用*/registerMicroApps([{name:?'taobao',entry:?'//localhost:7101',container:?'#subapp-viewport',loader,activeRule:?'/taobao',},{name:?'baidu',entry:?'//localhost:7102',container:?'#subapp-viewport',loader,activeRule:?'/baidu',},],{beforeLoad:?[app?=>?{console.log('[LifeCycle]?before?load?%c%s',?'color:?green;',?app.name);},],beforeMount:?[app?=>?{console.log('[LifeCycle]?before?mount?%c%s',?'color:?green;',?app.name);},],afterUnmount:?[app?=>?{console.log('[LifeCycle]?after?unmount?%c%s',?'color:?green;',?app.name);},],}, );const?{?onGlobalStateChange,?setGlobalState?}?=?initGlobalState({user:?'qiankun', });onGlobalStateChange((value,?prev)?=>?console.log('[onGlobalStateChange?-?master]:',?value,?prev),?true);setGlobalState({ignore:?'master',user:?{name:?'master',}, });/***?Step3?設置默認進入的子應用*/ setDefaultMountApp('/taobao');/***?Step4?啟動應用*/ start();runAfterFirstMounted(()?=>?{console.log('[MainApp]?first?app?mounted'); });上面主要完成:初始化、注冊子應用、設置配置全局狀態、設置默認進入子應用、啟動應用。
至于初始渲染函數,可以這么寫:
import?React?from?'react'; import?ReactDOM?from?'react-dom';/***?渲染子應用*/ function?Render(props)?{const?{?loading?}?=?props;return?(<>{loading?&&?<h4?className="subapp-loading">Loading...</h4>}<div?id="subapp-viewport"?/></>); }export?default?function?render({?loading?})?{const?container?=?document.getElementById('subapp-container');ReactDOM.render(<Render?loading={loading}?/>,?container); }實現子應用
子應用其實和官網的差不多,這里以 React.js 子應用舉例。首先用 create-react-app 來創建子應用:
create-react-app?baidu在 src 目錄下新增 public-path.js:
if?(window.__POWERED_BY_QIANKUN__)?{__webpack_public_path__?=?window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }設置 history 模式路由的 base:
const?RouteExample?=?()?=>?{return?(<Router?basename={window.__POWERED_BY_QIANKUN__???'/baidu'?:?'/'}><nav><Link?to="/">Home</Link><Link?to="/about">About</Link></nav><Suspense?fallback={null}><Switch><Route?path="/"?exact?component={Home}?/><Route?path="/about"?component={About}?/></Switch></Suspense></Router>); };export?default?function?App()?{return?(<div?className="app-main"><h1>淘寶Taobao</h1><hr/><RouteExample?/></div>); }__POWERED_BY_QIANKUN__ 用于判斷現在是否作為子應用被訪問,其它地方與普通 React.js App 沒差別。
去掉一些無用的文件后,在入口配置子應用:
function?render(props)?{const?{?container?}?=?props;ReactDOM.render(<App?/>,?container???container.querySelector('#root')?:?document.querySelector('#root')); }if?(!window.__POWERED_BY_QIANKUN__)?{render({}); }//?監聽全局狀態 function?storeTest(props)?{props.onGlobalStateChange((value,?prev)?=>?console.log('淘寶',?`[onGlobalStateChange?-?${props.name}]:`,?value,?prev),?true);props.setGlobalState({ignore:?props.name,user:?{name:?props.name,},}); }export?async?function?bootstrap()?{console.log('[淘寶]?react?app?bootstraped'); }export?async?function?mount(props)?{console.log('[淘寶]?props?from?main?framework',?props);storeTest(props);render(props); }export?async?function?unmount(props)?{const?{?container?}?=?props;ReactDOM.unmountComponentAtNode(container???container.querySelector('#root')?:?document.querySelector('#root')); }注意上面的 bootstrap, mount 和 unmount 一定要 export 出去,不然沒人知道這個是子應用。
下一步,是修改 Webpack 的配置。但是 creat-react-app 造出來的 React App 不 eject 出來就改不了,這里官網推薦使用 @rescripts/cli 來修改:
yarn?add?-D?@rescript/cli在根目錄添加 .rescriptsrc.js,并加上:
const?{?name?}?=?require('./package');module.exports?=?{webpack:?config?=>?{config.output.library?=?`${name}-[name]`;config.output.libraryTarget?=?'umd';config.output.jsonpFunction?=?`webpackJsonp_${name}`;config.output.globalObject?=?'window';return?config;},devServer:?_?=>?{const?config?=?_;config.headers?=?{'Access-Control-Allow-Origin':?'*',};config.historyApiFallback?=?true;config.hot?=?false;config.watchContentBase?=?false;config.liveReload?=?false;return?config;}, };Webpack 配置同樣很重要,一個是配置 historyApiFallback 處理單頁的 404 問題,另一個是通過 Access-Control-Allow-Origin 解決主應用訪問子應用的跨域問題。
在上面的主應用里看到我們是要訪問不同的端口的,那端口要怎么配置呢?可以通過 .env 來配置:
SKIP_PREFLIGHT_CHECK=true BROWSER=none PORT=7101 WDS_SOCKET_PORT=7102更多框架的配置可見這里。
API 粗講
乾坤的 API 也不是很多,詳見這里。簡單講一下用處:
| registerMicroApps | 主應用用來注冊多個子應用的函數 | 類似于 Vue 和 React 的路由 |
| start | 啟動主應用 | 類似于 React.js 的 render 函數和 Vue.js 的 new Vue() |
| loadMicroApp | 手動加載子應用 | 也類似于 React.js 的 render 函數和 Vue.js 的 new Vue(),只不過更自由了 |
| prefetchApps | 預加載子應用 | 類似于 Webpack 的 prefetch 功能 |
| addGlobalUncaughtErrorHandler | 頁面報錯時可以用于上報和兜底 | - |
| removeGlobalUncaughtErrorHandler | 都懂的 | - |
| initGlobalState | 初始化全局狀態 | 類似于 Redux 的 createStore 和 Vue 的 new Vue.Store() |
最后
查看:2021年總結記錄
關注前端開發博客,在后臺回復以下關鍵字可以獲取資源。
回復「小抄」,領取Vue、JavaScript 和 WebComponent 小抄 PDF
回復「Vue腦圖」獲取 Vue 相關腦圖
回復「思維圖」獲取 JavaScript 相關思維圖
回復「簡歷」獲取簡歷制作建議
回復「簡歷模板」獲取精選的簡歷模板
回復「加群」進入500人前端精英群
回復「電子書」下載我整理的大量前端資源,含面試、Vue實戰項目、CSS和JavaScript電子書等。
回復「知識點」下載高清JavaScript知識點圖譜
點贊和在看就是最大的支持??
總結
以上是生活随笔為你收集整理的10分钟弄懂微应用框架——乾坤,真香!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 创建多币种多系统 EA 交易
- 下一篇: jQuery 实现淘宝精品案例