微前端框架qiankun初探

qiankun 是由蚂蚁集团开源的一个基于 single-spa 的微前端实现库,它是目前主流的微前端解决方案之一。它对 single-spa 进行了大量的封装和加强,比如样式隔离。它让我们使用起来毫不费力。在本文中主要会讲解如何使用qiankun搭建微前端,以及它是如何工作的。

对于微前端框架来说,有主应用和子应用两个概念,主应用主要为子应用提供容器功能。

主应用搭建

这里选择使用create-react-app脚手架搭建,目前react版本号为18.2.0,创建后安装qiankun

1
2
3
create-react-app base
cd base
yarn add qiankun

子应用想在主应用中显示,则需要进行注册,qiankun提供了registerMicroApps方法,之后执行start方法。示例如下,对于子应用,我们分别使用vuereact和原生项目。

1
2
3
4
5
6
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
...
])

start()

我们添加react的路由和antd,把界面改造成一个中后台的样子。

1
2
yarn add react-router-dom
yarn add antd

qiankun是根据路由变化来匹配子应用的,这里需要对不同子应用配置一个activeRule,方便切换,这里的activeRule也和App.js中的Link路径相对应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// base/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'
import './index.css';
import 'antd/dist/antd.css';
import App from './App';

import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vueApp',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app-vue'
},
{
name: 'reactApp',
entry: '//localhost:4000',
container: '#container',
activeRule: '/app-react'
},
{
name: 'vanillaApp',
entry: '//localhost:9896',
container: '#container',
activeRule: '/app-vanilla'
}
])

// 启动 qiankun
start()
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// base/src/App.js
import { useState } from 'react';
import { Layout, Menu } from 'antd';
import { DesktopOutlined, PieChartOutlined } from '@ant-design/icons';
import { Link } from 'react-router-dom'
import './App.css';

const { Header, Content, Footer, Sider } = Layout;

const App = () => {
const [collapsed, setCollapsed] = useState(false);

const onCollapse = collapsed => {
setCollapsed(collapsed);
};
const menuItems = [
{
key:'vue',
icon:<PieChartOutlined />,
label:<Link to="/app-vue">Vue应用</Link>
},
{
key:'react',
icon:<DesktopOutlined />,
label:<Link to="/app-react">React应用</Link>
},
{
key:'vanilla',
icon:<DesktopOutlined />,
label:<Link to="/app-vanilla">Vanilla应用</Link>
}
]
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible collapsed={collapsed} onCollapse={onCollapse}>
<div className="logo" />
<Menu theme="dark" defaultSelectedKeys={['vue']} mode="inline" items={menuItems}>

</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div id="container" className="site-layout-background" style={{ minHeight: 360 }}></div>
</Content>
<Footer style={{ textAlign: 'center' }}>©2022</Footer>
</Layout>
</Layout>
);
}

export default App;
1
2
3
4
5
6
7
8
9
10
// base/src/App.css
#components-layout-demo-side .logo {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}

.site-layout .site-layout-background {
background: #fff;
}

此时我们将看到搭建好的主应用基座界面:

基座

子应用

Vue

使用脚手架创建vue项目

1
vue create app-vue

改造main.js,将render函数提取出来,根据全局变量__POWERED_BY_QIANKUN__来判断是qiankun环境还是独立部署。同时加入qiankun子应用的生命周期,需要在unmount中将实例卸载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app-vue/src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
render: (h) => h(App),
}).$mount(container ? container.querySelector("#app") : "#app");
}

// 独立运行时 直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render();
}

// qiankun 子应用生命周期
export async function bootstrap() {
console.log("[vue] vue app bootstraped");
}

export async function mount(props) {
console.log("[vue] props from main framework", props);
render(props);
}

export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}

同时我们需要修改vue-routerbase路径为qiankun`中配置的子应用路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// app-vue/src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'

Vue.use(VueRouter)

const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
// 同样是根据全局变量__POWERED_BY_QIANKUN__判断
const router = new VueRouter({
mode: 'history',
base: window.__POWERED_BY_QIANKUN__ ? "/app-vue/" : process.env.BASE_URL,
routes
})

export default router

修改webpack的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// app-vue/vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: {
name: 'vue-app',
type: 'umd' // 子应用打包为umd
}
},
},
});

此时vue子应用项目搭建完成。

React

创建基本react框架,这里react版本和主应用一样,18.2.0,同时使用react-app-rewired修改webpack配置。同样,在unmount生命周期中使用root.unmount卸载实例

1
2
3
create-react-app app-react
yarn add react-router-dom
yarn add react-app-rewired -D
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// app-react/src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom'

import './index.css';
import App from './App';
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
let root;
function render(props) {
const { container } = props;
root = ReactDOM.createRoot(container ? container.querySelector('#root') : document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>
<App />
</BrowserRouter>
</React.StrictMode>);
}

if (!window.__POWERED_BY_QIANKUN__) {
render({});
}


export async function bootstrap() {
console.log('[react18] react app bootstraped');
}

export async function mount(props) {
console.log('[react18] props from main framework', props);
render(props);
}

export async function unmount(props) {
const { container } = props;
root.unmount(container ? container.querySelector('#root') : document.querySelector('#root'));
}

在项目根目录新建config-overrides.js文件夹修改webpack配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// app-react/config-overrides.js
module.exports = {
webpack: (config) => {
config.output.library = `react-[name]`;
config.output.libraryTarget = 'umd';
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;
},
};

修改package.json的启动命令和端口号

1
2
3
4
5
6
7
// app-react/package.json
"scripts": {
"start": "set PORT=4000 && react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},

此时react子应用项目搭建完成

Vanilla

原生项目比较简单,只需要在js中导出qiankunrender函数和生命周期函数就行

1
2
3
4
<!-- app-vanilla/index.html -->
...
<script src="./entry.js" entry></script>
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// app-vanilla/entry.js
const render = ($) => {
return Promise.resolve();
};

((global) => {
global['vanilla'] = {
bootstrap: () => {
console.log('vanilla bootstrap');
return Promise.resolve();
},
mount: () => {
console.log('vanilla mount');
return render();
},
unmount: () => {
console.log('vanilla unmount');
return Promise.resolve();
},
};
})(window);

完成搭建

这时我们分别运行主应用和三个子应用,可以看到子应用已经成功运行在主应用中了,同时子应用也可以独立运行。

完成

参考

项目实践 - qiankun (umijs.org)

可能是你见过最完善的微前端解决方案 - 知乎 (zhihu.com)

微前端的核心价值 - 知乎 (zhihu.com)