一、为什么选择qiankun作为微前端落地方案?
经过调研,现在市场上微前端方案大概三种:
(1)iframe:关于不使用iframe作为微前端的方案,已经有很多前辈出来分析了,这里直接贴上Why Not Iframe的链接,大家有想了解的可以看下。其中,弹窗等UI组件不能居中显示,是放弃此方案的主要原因
(2)EMP:此方案放弃的原因是目前使用人数较少,怕踩到一些坑找不到好的解决方案。有想了解此方案的可以看下此处的官网介绍。
(3)qiankun:此方案属于国内比较早的微前端落地方案,目前使用人数也比较多
二、业务场景示意图及代码库地址
1.整体业务示意图:
2.代码仓库地址(欢迎star)
a.主应用:
cli模板:creact-react-app
github地址:GitHub - wangliang101/qiankun-react-main: 微前端qiankun实现react主应用
b.react子应用:
cli模板:creact-react-app
github地址:GitHub - wangliang101/qiankun-react-sub: 微qiankun实现react子项目
c.vue子应用
cli模板:vue-cli
github地址:GitHub - wangliang101/qiankun-vue-sub: 微前端qiankun实现vue子项目
三、qiankun接入
主应用(基座)和子应用1使用create-react-app搭建,子应用2使用vue-cli搭建
1.主应用
a.初始化项目
create-react-app react-main
b.安装qiankun
yarn add qiankun
c.新增两个文件,配置主应用端口及不同环境的子应用入口(环境变量一定要以REACT_APP_开头)
// .env.development.local 开发环境使用
REACT_APP_REACT_SUB=//localhost:3001
REACT_APP_VUE_SUB=//localhost:3002
PORT=3000
// .env.production.local 生成环境中使用
REACT_APP_REACT_SUB=react-sub
REACT_APP_VUE_SUB=vue-sub
d.修改/src/app.js内容,修改为我们需要的布局,并且挂载子应用
import { useState } from 'react';
import { registerMicroApps, start } from 'qiankun';
import './App.css';
// 注册子应用
registerMicroApps([
{
name: 'reactApp',
entry: process.env.REACT_SUB,
container: '#container',
activeRule: '/react',
},
{
name: 'vueApp',
entry: process.env.VUE_SUB,
container: '#container',
activeRule: '/vue',
},
]);
// 启动 qiankun
start();
function App() {
const [activeId, setActiveId] = useState(1);
// 修改默认布局,增加子应用挂在容器“#container”
return (
<div className="app">
<header className="app_header"></header>
<div className="app_body">
<aside className="menu">
<div className={`menu_button ${activeId === 1 && 'active'}`} onClick={() => setActiveId(1)}>react-sub</div>
<div className={`menu_button ${activeId === 2 && 'active'}`} onClick={() => setActiveId(2)}>vue-sub</div>
</aside>
<div id="container" className="content"></div>
</div>
</div>
);
}
export default App;
e.修改主应用样式
.app{
height: 100vh;
display: flex;
flex-direction: column;
}
.app_header{
height: 60px;
background-color:tan;
}
.app_body{
display: flex;
flex: 1;
}
.menu{
width: 300px;
background-color: steelblue;
display: flex;
flex-direction: column;
}
.menu_button{
height: 50px;
line-height: 50px;
text-align: center;
background-color: wheat;
border: 1px solid darkcyan;
}
.menu_button:hover{
background-color: darkseagreen;
}
.menu_button.active{
background-color: darkseagreen;
}
.content{
background-color: thistle;
flex: 1;
}
2.react 子应用
a.初始化react子项目
create-react-app react-sub
b.安装react-app-rewired
# 为了不reject,所以使用react-app-rewired
yarn add react-app-rewired
# 安装react-router-dom,qiankun需设置 history 模式路由的 base
yarn add react-router-dom
c.新增.env文件,端口要和主应用配置中的一致
// .env文件
PORT=3001
d.新增/src/public-path.js文件
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
e.修改子应用布局并配置basename
import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom';
import Home from './pages/home';
import About from './pages/about'
import './App.css';
function App() {
return (
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react' : '/'}>
<div className="app_react_sub">
<header className="app_react_sub_header">
<NavLink className="button" to="/">HOME</NavLink>
<NavLink className="button" to="/about">ABOUT</NavLink>
</header>
<div className="app_react_sub_body">
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
</Routes>
</div>
</div>
</BrowserRouter>
);
}
export default App;
f.修改子应用样式
.app_react_sub{
height: 100%;
display: flex;
flex-direction: column;
}
.app_react_sub_header{
height: 100px;
line-height: 100px;
display: flex;
justify-content: center;
}
.button:nth-child(1){
margin-right: 50px;
}
.app_react_sub_body{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: lightsalmon;
font-size: 30px;
}
g.修改/src/index.js文件,子应用导出相应的生命周期钩子
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './public-path';
function render(props) {
const { container } = props;
ReactDOM.render(<App />, container ? container.querySelector('#react-sub-root') : document.querySelector('#react-sub-root'));
}
if (!window.__POWERED_BY_QIANKUN__) {
render({});
}
export async function bootstrap() {
console.log('[react16] react app bootstraped');
}
export async function mount(props) {
console.log('[react16] props from main framework', props);
render(props);
}
export async function unmount(props) {
const { container } = props;
ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#root'));
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
h.新增config-overrides.js,覆盖create-react-app的webpack配置
const { name } = require('./package');
module.exports = {
webpack: config => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = 'umd';
config.output.jsonpFunction = `webpackJsonp_${name}`;
return config;
},
devServer: (configFunction) => {
return (proxy, allowedHost) => {
const config = configFunction(proxy, allowedHost);
config.historyApiFallback = true;
config.open = false;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
config.headers = {
'Access-Control-Allow-Origin': '*',
};
return config;
}
}
}
I.修改package.json下script
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
3.vue子应用
a.初始化项目
vue create vue-sub
b.安装vue-router
yarn add vue-router@4
c.新增.env文件,端口要和主应用配置中的一致
// .env文件
PORT=3002
d.新增/src/public-path.js文件
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
e.修改子应用布局(/src/App.vue)
<template>
<div class="app_vue_sub">
<header class="app_vue_sub_header">
<router-link class="button" to="/">HOME</router-link>
<router-link class="button" to="/about">ABOUT</router-link>
</header>
<div class="app_vue_sub_body">
<router-view/>
</div>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
.app_vue_sub{
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
height: 100%;
display: flex;
flex-direction: column;
}
.app_vue_sub_header{
height: 100px;
line-height: 100px;
display: flex;
justify-content: center;
}
.button:nth-child(1){
margin-right: 50px;
}
.app_vue_sub_body{
flex: 1;
display: flex;
align-items: center;
justify-content: center;
color: peru;
font-size: 30px;
}
</style>
f.新增路由并配置basename
// /src/router/index.js
import { createRouter, createWebHashHistory} from 'vue-router';
import Home from '../pages/home';
import About from '../pages/about';
const router = createRouter({
history: createWebHashHistory(window.__POWERED_BY_QIANKUN__ ? '/vue' : '/'),
routes: [
{path: '/', component: Home},
{path: '/about', component: About}
]
})
export default router
g.修改/src/main.js文件,子应用导出相应的生命周期钩子
import { createApp } from 'vue'
import App from './App.vue';
import route from './router';
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = route;
instance = createApp(App);
instance.use(router);
instance.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() {
console.log('instance', instance)
instance.unmount();
instance._container.innerHTML = '';
instance = null;
router = null;
}
h.新增vue.config.js,修改webpack配置
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
四、主应用和子应用通信
此主主要是测试主子应用显示初始状态及主/子应用修改全局状态后,其他应用是否能正常监听及显示
1.主应用
a.新建src/store/index.js,此处主要定义全局初始状态及一个获取全局状态的方法
import { initGlobalState } from 'qiankun';
const initState = {
name: 'react-main',
value: 'react-main date'
}
// 初始化 state
const actions = initGlobalState(initState);
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log('main store',state, prev);
for(const key in state){
initState[key] = state
}
});
// actions.setGlobalState(state);
// actions.offGlobalStateChange();
// 获取全局状态
actions.getGlobalState = (key) => {
return initState[key] ? initState[key] : initState
}
export default actions;
b.主应用显示全局状态及修改全局状态,在src/App.js修改
import { useState, useEffect } from 'react';
import { registerMicroApps, start } from 'qiankun';
import './App.css';
import storeAction from './store';
......
function App() {
const [activeId, setActiveId] = useState(1);
const [headerText, setHeadetText] = useState(storeAction.getGlobalState())
const handleClick = (id) => {
window.history.pushState(null, null, id === 1 ? '/react' : '/vue')
setActiveId(id)
}
// 修改默认布局,增加子应用挂在容器“#container”
const changeGlobalData = () => {
storeAction.setGlobalState({
value: 'react-main change data'
})
}
useEffect(() => {
storeAction.onGlobalStateChange((state, pre) => {
setHeadetText(state)
})
return () => {
storeAction.offGlobalStateChange()
}
}, [])
return (
<div className="app">
<header className="app_header">
<span>{headerText.name}:{headerText.value}</span>
<button onClick={changeGlobalData}>改变主应用数据</button>
</header>
<div className="app_body">
<aside className="menu">
<div className={`menu_button ${activeId === 1 && 'active'}`} onClick={() => handleClick(1)}>react-sub</div>
<div className={`menu_button ${activeId === 2 && 'active'}`} onClick={() => handleClick(2)}>vue-sub</div>
</aside>
<div id="container" className="content"></div>
</div>
</div>
);
}
export default App;
2.react子应用
a.修改src/index.js在render时,将设置及监听全局状态的方法传递给App
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import './public-path';
// 在render时,将设置及监听全局状态的方法传给App组件
function render(props) {
const { container, setGlobalState, onGlobalStateChange } = props;
ReactDOM.render(<App setGlobalState={setGlobalState} onGlobalStateChange={onGlobalStateChange}/>, container ? container.querySelector('#root') : document.querySelector('#root'));
}
......
b.在home中显示并增加修改全局状态的方法
import React, {useState, useEffect} from 'react';
const Home = ({onGlobalStateChange, setGlobalState}) => {
const [text, setText] = useState('');
const changeGlobalData = () => {
setGlobalState && setGlobalState({
value: 'react-sub change data'
})
}
useEffect(() => {
onGlobalStateChange && onGlobalStateChange((state, pre) => {
setText(state)
})
}, [onGlobalStateChange])
return(
<div style={{display: 'flex', flexDirection: 'column'}}>
<div>react-sub home</div>
<div>主应用数据:{text.value}</div>
<div onClick={changeGlobalData}><button>修改主应用数据</button></div>
</div>
)
}
export default Home;
3.vue子应用
a.修改src/main.js在render时,将设置及监听全局状态的方法挂在vue实例上
import { createApp } from 'vue'
import App from './App.vue';
import route from './router';
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
router = route;
instance = createApp(App);
instance.use(router);
instance.config.globalProperties.$setGlobalState = props.setGlobalState;
instance.config.globalProperties.$onGlobalStateChange = props.onGlobalStateChange;
instance.mount(container ? container.querySelector('#app') : '#app');
}
......
b.在home中显示并增加修改全局状态的方法
<template>
<div>
<div class='sub_wrap'>vue-sub home</div>
<div>主应用数据:{{text.value}}</div>
<div><button @click='changeGlobalData'>修改主应用数据</button></div>
</div>
</template>
<script>
import { onMounted, getCurrentInstance, ref } from 'vue';
export default {
name: "Home",
setup (){
const { proxy } = getCurrentInstance();
let text = ref({})
const changeGlobalData = () => {
proxy.$setGlobalState && proxy.$setGlobalState({
value: 'react-sub change data'
})
}
onMounted(() => {
proxy.$onGlobalStateChange && proxy.$onGlobalStateChange((state) => {
text.value = state
})
})
return {
changeGlobalData,
text
}
},
}
</script>
<style scoped>
.sub_wrap{
display: flex;
flex-direction: column;
}
</style>>
五、部署
项目采用docker部署,项目间通过nginx来代理。有以下两点需要单独说明一下:
a.子应用build时需要增加public-path,本项目中,react子应用为"/react-sub",vue子应用为"/vue-sub",仅供参考,如果需要更换,主应用中nginx.conf配置也需要同步更换
b.至于每个项目的打包方案,将不作为本次内容的重点,如果有感兴趣的看下github中对应项目
1.整体服务的docker-compose.yml文件如下(子应用如不需要单独访问,端口可不设置):
version: '2.3'
services:
react-mian:
image: wangliang4468/qiankun:react-main-1.0.0
restart: always
ports:
- 5000:80
react-sub:
image: wangliang4468/qiankun:react-sub-1.0.0
restart: always
ports:
- 5001:80
vue-sub:
image: wangliang4468/qiankun:vue-sub-1.0.0
restart: always
ports:
- 5002:80
2.react-main nginx配置
server {
listen 80;
server_name 127.0.0.1;
charset utf-8;
location ^~ /react-sub {
resolver 127.0.0.11 valid=10s;
set $upstream_server react-sub;
proxy_pass http://$upstream_server:80;
rewrite ^/react-sub(.*)$ /$1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
location ^~ /vue-sub {
resolver 127.0.0.11 valid=10s;
set $upstream_server vue-sub;
proxy_pass http://$upstream_server:80;
rewrite ^/vue-sub(.*)$ /$1 break;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}
location ^~ / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
}
3.react-sub nginx配置
server {
listen 80;
server_name 127.0.0.1;
charset utf-8;
location ^~ / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
}
4.vue-sub nginx配置
server {
listen 80;
server_name 127.0.0.1;
charset utf-8;
location ^~ / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Last-Modified $date_gmt;
add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
if_modified_since off;
expires off;
etag off;
}
}
六.使用qiankun踩坑小计
a.主应用和子应用类名尽量不要一样,否则可能会导致样式错乱
b.解决开发环境“Cannot GET /hash.hot-update.json”问题 解决方案:链接
c.开发环境子组件使用nginx代理导致静态资源一直请求(暂时不支持)