10分鐘弄懂微應用框架——乾坤,真香!
前言
今天剛剛學習了一個微前端框架——乾坤,正著熱乎勁,寫一篇入門部落格。這篇文章不會討論太多的原理和實現,只是一個入門寫 Hello World 的教程。
文章的很多地方都參考官網,但是官網的教程太簡潔了,個人覺得還是做不到無腦上手,希望這篇文章可以幫到正在入門微前端的夥伴。
想直接看程式碼的,我寫了個比官網更簡單的例子,點選即可學會。
什麼是微前端
首先,來了解一下微前端是個啥。
當我們寫了一個又一個的 SPA 應用。突然有一天,老闆說要將這些應用合併,前端工程們就頭大了——每個應用的程式碼都是一座搖搖欲墜的:hankey:山,別說合併了,就算動都不敢動呀。
雖然很麻煩,但是前端工程師還是把這個問題解決了,而這個將多個 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 也不是很多,詳見這裡。簡單講一下用處:
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() |
最後
關注前端開發部落格,在後臺回覆以下關鍵字可以獲取資源。
-
回覆「小抄」,領取Vue、JavaScript 和 WebComponent 小抄 PDF
-
回覆「Vue腦圖」獲取 Vue 相關腦圖
-
回覆「思維圖」獲取 JavaScript 相關思維圖
-
回覆「簡歷」獲取簡歷製作建議
-
回覆「簡歷模板」獲取精選的簡歷模板
-
回覆「加群」進入500人前端精英群
-
回覆「電子書」下載我整理的大量前端資源,含面試、Vue實戰專案、CSS和JavaScript電子書等。
-
回覆「知識點」下載高清JavaScript知識點圖譜
點贊和在看就是最大的支援 :heart: