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
方法。示例如下,对于子应用,我们分别使用vue
,react
和原生项目。
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
| 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' } ])
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
| 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
项目
改造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
| import Vue from 'vue' import App from './App.vue' import router from './router'
Vue.config.productionTip = false
if (window.__POWERED_BY_QIANKUN__) { __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(); }
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-router
的base路径为
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
| 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( '../views/AboutView.vue') } ]
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
| const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ devServer: { headers: { 'Access-Control-Allow-Origin': '*', }, }, configureWebpack: { output: { library: { name: 'vue-app', type: '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
| 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__) { __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
| 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
| "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
中导出qiankun
的render
函数和生命周期函数就行
1 2 3 4
| ... <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
| 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)