前言
今天剛剛學習了一個微前端框架——乾坤,正著熱乎勁刁俭,寫一篇入門博客。這篇文章不會討論太多的原理和實現(xiàn)畴蒲,只是一個入門寫 Hello World 的教程虹统。
文章的很多地方都參考官網(wǎng),但是官網(wǎng)的教程太簡潔了吨岭,個人覺得還是做不到無腦上手拉宗,希望這篇文章可以幫到正在入門微前端的伙伴。
想直接看代碼的辣辫,我寫了個比官網(wǎng)更簡單的例子旦事,點擊即可學會。
什么是微前端
首先急灭,來了解一下微前端是個啥姐浮。
當我們寫了一個又一個的 SPA 應用。突然有一天化戳,老板說要將這些應用合并单料,前端工程們就頭大了——每個應用的代碼都是一座搖搖欲墜的??山,別說合并了点楼,就算動都不敢動呀扫尖。
雖然很麻煩,但是前端工程師還是把這個問題解決了掠廓,而這個將多個 SPA 合并成一個 Web App 的解決方案就是微前端换怖。
為什么要微前端
“多個 SPA 合并成一個 Web App?”蟀瞧,可能有人會想到用 <iframe/>
也可以實現(xiàn)一個網(wǎng)頁里內(nèi)嵌多個網(wǎng)頁呀沉颂。原因有:
- 不感知 url 狀態(tài)条摸,比如前進、后退沒法玩
- UI 不同步铸屉、DOM 不同享钉蒲。
<iframe/>
本質(zhì)上是頁面的硬隔離,所以如果你有個遮罩層彻坛,可能只能在那一小片區(qū)域才展示遮罩層 - 頁面之間的通信很麻煩
- 每次都要加載子應用顷啼,速度很慢
而微前端正好可以補足上面的缺點。
微前端的優(yōu)勢
除了解決了上面的問題昌屉,微前端還有如下的優(yōu)點:
- 子應用技術棧無關钙蒙,即類似上頁說的頁面硬隔離,但是是以 sandbox 的方式實現(xiàn)的
- 合并多個子應用间驮,相對地躬厌,也可以將大應用拆解成多個應用,實現(xiàn)業(yè)務解耦
- 子應用高度自治竞帽,發(fā)布扛施、報錯、測試流程僅限于子應用抢呆,不會受別的業(yè)務影響煮嫌,同時也不影響別的業(yè)務
乾坤由來
最原始的微前端框架并不是乾坤,而是 single-spa抱虐。但是這個框架只提供最基本的功能昌阿,而且全是英文,文檔寫得也很繁瑣恳邀,應該沒人想去看懦冰。
阿里的乾坤則是基于 single-spa 開發(fā)的又一個微前端框架,提供了更多的功能谣沸,也解決一些坑刷钢,官網(wǎng)也很簡潔。
不過乳附,個人覺得有點太簡潔了内地,寫 Hello World 的時候還是遇到一些坑,只能看 Github 的 /examples 目錄學習赋除。
主應用 VS 子應用
首先阱缓,要知道現(xiàn)在項目并不是只有一個了,而是區(qū)分出 主應用 和 子應用举农,關系如下:
兩者區(qū)別:
-
主應用
- 概念:就是要統(tǒng)治各個子應用的應用荆针,也即合并結(jié)果頁面
- 負責子應用的注冊、路由分發(fā)『奖常可以簡單理解為 React.js 和 Vue.js 里的 App 組件喉悴,主要做一些初始化、路由注冊玖媚、全局狀態(tài)注冊箕肃、銷毀時的動作
-
子應用
- 概念:各個 SPA 應用,可以理解為 SPA 里的頁面組件
- 負責暴露一些函數(shù)最盅,以此對接主應用突雪,讓主應用知道:哦,原來你是子應用涡贱,要和我對接。常見的對應函數(shù)有:
bootstrap
,mount
,unmount
項目創(chuàng)建
乾坤官網(wǎng)最推薦的做法是將主應用和子應用分成兩個項目惹想,各自管理问词。當然,也可以一個項目里分成不同的目錄來存放嘀粱。
├── main # 主應用
├── baidu # 子應用
└── taobao # 子應用
如果你覺得 官方的例子 太復雜激挪,也可以看我自己建的 qiankun-bigass-app,子應用只有兩個用 React.js 的項目锋叨。我把很多無關的代碼都刪了垄分。
實現(xiàn)主應用
理清上面的關系后,我們直接干代碼娃磺,先看主應用薄湿。
首先,我們弄一個 .html 文件出來偷卧,作為主頁面的 HTML 模板:
<body>
<div class="mainapp">
<!-- 標題欄 -->
<header class="mainapp-header">
<h1>QianKun</h1>
</header>
<div class="mainapp-main">
<!-- 側(cè)邊欄 -->
<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');
});
上面主要完成:初始化仔蝌、注冊子應用泛领、設置配置全局狀態(tài)、設置默認進入子應用掌逛、啟動應用师逸。
至于初始渲染函數(shù),可以這么寫:
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);
}
實現(xiàn)子應用
子應用其實和官網(wǎng)的差不多,這里以 React.js 子應用舉例篓像。首先用 create-react-app
來創(chuàng)建子應用:
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__
用于判斷現(xiàn)在是否作為子應用被訪問动知,其它地方與普通 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({});
}
// 監(jiān)聽全局狀態(tài)
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 出來就改不了,這里官網(wǎng)推薦使用 @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 | 主應用用來注冊多個子應用的函數(shù) | 類似于 Vue 和 React 的路由 |
start | 啟動主應用 | 類似于 React.js 的 render 函數(shù)和 Vue.js 的 new Vue()
|
loadMicroApp | 手動加載子應用 | 也類似于 React.js 的 render 函數(shù)和 Vue.js 的 new Vue() 辰晕,只不過更自由了 |
prefetchApps | 預加載子應用 | 類似于 Webpack 的 prefetch 功能 |
addGlobalUncaughtErrorHandler | 頁面報錯時可以用于上報和兜底 | - |
removeGlobalUncaughtErrorHandler | 都懂的 | - |
initGlobalState | 初始化全局狀態(tài) | 類似于 Redux 的 createStore 和 Vue 的 new Vue.Store()
|
FAQ
請看 常見問題蛤迎,最佳實踐和遇到的問題都能在這里解決。
(完)