什么是微前端?
微前端(Micro-Frontends)是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署
qiankun
qiankun是一个基于single-spa的微前端实现库,qiankun 对于用户而言只是一个类似 jQuery 的库,你需要调用几个 qiankun 的 API 即可完成应用的微前端改造
项目背景
vue3的生态日渐成熟,于是乎一狠心新产品直接上vue3。由于已有基于qiankun的生产项目,也算比较熟悉,新产品的微前端方案也就同样选择了qiankun,容易踩坑的地方不少,下面直接上干货
项目构成如下:
- 项目A: 子应用
Vue2 + Webpack
- 项目B: 子应用
Vue3 + Vite
- 项目C: 主应用
Vue3 + Vite
项目A,Webpack老项目介入
按照官网教程即可
- 安装依赖
npm i qiankun
- 配置基础路径
main.ts
中导出qiankun的生命周期钩子,mount
时加载,unmount
时销毁
let instance = null;
function render(props) {
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
}
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
} else {
render();
}
export async function mount(props) {
render(props);
}
export async function unmount(props) {
instance?.$destroy();
}
项目B,新的子应用
目前为止官方还没有支持vite,直接使用的话只能解决build后的集成,经过一系列的尝试,最终选择使用了 vite-plugin-qiankun 插件接入
- 安装依赖
npm i vite-plugin-qiankun
- 在
vite.config.ts
中添加插件
import qiankun from 'vite-plugin-qiankun';
defineConfig({
base: 'http://localhost:3002/',
server: {
port: 3002,
cors: true,
origin: 'http://localhost:3002'
},
plugins: [
vue(),
qiankun('flow-graph', {
useDevMode: true
})
]
})
需要注意的点有几个:
base
需要使用绝对路径,做为子应用时需要跨域访问useDevMode: true
这个地方,引用作者的原话:
因为开发环境作为子应用时与热更新插件(可能与其他修改html的插件也会存在冲突)有冲突,所以需要额外的调试配置
server.origin
需要使用绝对路径,否则会引起dev时静态资源无法访问 这个地方要多说几句,如果静态资源放在public目录下,使用如下的写法加载,在应用单独运行和发布时是没有问题的,但是在dev时作为子应用无法正确加载资源
<img src="/img/add.png" />
资源放置在 assets
目录下,使用如下的相对路径引用,再配合 server.origin
经测试在dev和prod时作为子应用都可以正确加载资源
<img src="@/assets/add.png" />
main.ts
中添加qiankun的生命周期钩子,同样是mount
时加载,unmount
时销毁
import {
renderWithQiankun,
qiankunWindow
} from 'vite-plugin-qiankun/dist/helper';
let app: VueApp<Element>;
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
createApp(App).use(router).use(ElementPlus).use(createPinia()).mount('#app');
} else {
renderWithQiankun({
mount(props) {
console.log('--mount');
app = createApp(App);
app
.use(router)
.use(ElementPlus)
.use(createPinia())
.mount(
(props.container
? props.container.querySelector('#app')
: document.getElementById('app')) as Element
);
},
bootstrap() {
console.log('--bootstrap');
},
update() {
console.log('--update');
},
unmount() {
console.log('--unmount');
app?.unmount();
}
});
}
这里要注意的是qiankun使用 window.__POWERED_BY_QIANKUN__
判断是否在子应用环境中,而插件使用 qiankunWindow.__POWERED_BY_QIANKUN__
进行判断
项目C,主应用
第一步,先跑起来
- 安装依赖
npm i qiankun
- 在
App.vue
添加id为subApp
的div main.ts
中注册子应用路由
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'data-source',
entry: 'http://localhost:3001/',
container: '#subApp',
activeRule: '/data-source'
},
{
name: 'flow-graph',
entry: 'http://localhost:3002/',
container: '#subApp',
activeRule: '/flow-graph'
}
]);
start();
这时路由地址变化,子应用就会对应加载到div的位置上了
二级路由加载
我们在实际应用中很多时候都是在二级路由页面中加载子应用,现在只需要稍加改造就可以了
- 添加二级路由页面
<script setup lang="ts">
import { start } from 'qiankun';
import { onMounted } from 'vue';
onMounted(() => {
if (!window.qiankunStarted) {
window.qiankunStarted = true;
start({ sandbox: { experimentalStyleIsolation: true } });
}
});
</script>
<template>
<div class="layout">
<el-menu
class="navigation-menu-left"
active-text-color="#ffd04b"
background-color="#545c64"
text-color="#fff"
router
>
<el-menu-item index="/flow-graph">流程</el-menu-item>
<el-menu-item index="/data-source">数据</el-menu-item>
</el-menu>
<div class="main-container">
<router-view />
<div id="subApp" class="sub-app-container" />
</div>
</div>
</template>
<style lang="scss" scoped>
.layout {
.navigation-menu-left {
width: 100px;
}
.main-container {
position: absolute;
left: 100px;
top: 0;
height: 100%;
width: calc(100% - 100px);
.sub-app-container {
height: 100%;
width: 100%;
}
}
}
</style>
这里在路由页面 mounted
时调用qiankun的 start
,删除 main.ts
里的 start
调用,因为需要子应用的容器加载之后再进行调用
- 在主应用中注册二级路由
{
path: '/data-source/:chapters*',
component: () => import('@/layout/index.vue')
},
{
path: '/flow-graph/:chapters*',
component: () => import('@/layout/index.vue')
}
这里要注意的是路由使用通配符*无法正确加载,使用参数通配符 :chapters*
可以正确加载
配置
加载子应用需要配置绝对路径的base,因为vite不支持运行时publicPath,只能在打包时写死base配置,如果添加在每个子应用的 .env
配置文件中,修改起来十分麻烦,项目采用monorepo时的两个解决方案
- 将
.env
配置提出到根目录,在vite.config.ts
按如下配置:
{
envDir: '../../',
base: `${loadEnv(mode, resolve(process.cwd(), '../../')).VITE_APP_BASE}:${
loadEnv(mode, resolve(process.cwd(), '../../')).VITE_APP_DATA_SOURCE_PORT
}`,
}
配置 envDir
路径,并且读取时也添加相对路径,这样来使所有项目读取公共的 .env
配置文件
- 在根目录新建一个类似如下的自定义配置文件,直接读取:
const dev = {
microApps: [
{
name: 'data-source',
entry: 'http://localhost:3001/',
container: '#subApp',
activeRule: '/data-source'
},
{
name: 'flow-graph',
entry: 'http://localhost:3002/',
container: '#subApp',
activeRule: '/flow-graph'
}
]
};
const prod = {
icroApps: [
{
name: 'data-source',
entry: 'http://192.168.1.22:3001/',
container: '#subApp',
activeRule: '/data-source'
},
{
name: 'flow-graph',
entry: 'http://192.168.1.22:3002/',
container: '#subApp',
activeRule: '/flow-graph'
}
]
};
export default (mode: string) => ({
port: 3000,
...(mode === 'development' ? dev : prod)
});
小结
项目搭建的过程和容易踩坑的地方都记录下来了,希望本文能成为最全面的 vite + qiankun
搭建指导。