一、什么是微前端
- 微前端:一种类似于微服务的架构,它将微服务的理念应用于浏览器端,将单页面前端应用由单一的单体应用,转变为多个小型前端应用聚合为一的应用
- qiankun:是由蚂蚁金服推出的基于Single-Spa实现的前端微服务框架,本质是路由分发式。官方链接:qiankun.umijs.org/zh/guide
- 父应用:提供一个能够访问到所有的子应用的入口
- 子应用:对应单独的业务项目或者是业务模块,每个子应用都是一个独立的项目,部署后可独立被访问
二、使用微前端的背景
- 复杂Web应用的发展:随着Web应用的不断演进,前端应用变得越来越复杂,功能丰富且交互性强。这些应用往往由许多相对独立的功能模块组成,每个模块可能由不同的团队负责开发。
- 跨团队协作的需求:在大型项目中,通常有多个团队共同参与开发。这些团队可能分布在不同的地域,具有不同的专业背景和技能。微前端架构有助于实现跨团队的协作,让每个团队能够独立开发、测试和部署自己的功能模块。
- 技术栈的多样性和灵活性:微前端架构不限制接入应用的技术栈,这意味着每个微应用可以选择最适合自己的技术栈进行开发。这种灵活性使得开发团队能够根据实际情况选择最佳的技术方案,从而提高开发效率和代码质量。
- 增量升级和渐进式重构:对于已经存在的系统,进行全面的技术栈升级或重构往往是非常困难的。微前端架构提供了一种实施渐进式重构的手段和策略,使得系统可以在不影响整体功能的前提下,逐步进行技术升级和模块重构。
- 前后端分离的趋势:随着前后端分离架构的普及,前端开发越来越独立于后端。微前端架构进一步推动了这种趋势,使得前端应用能够更加自主地进行开发、测试和部署,而无需过多依赖后端环境。
三、微前端的优势
- 技术栈无关性:微前端允许不同的前端应用使用不同的技术栈。这意味着开发团队可以根据项目需求和个人偏好选择最适合的技术。这种灵活性有助于提高开发效率和代码质量,因为每个团队都可以使用他们最熟悉和擅长的技术。
- 独立开发、部署和升级:微前端架构将前端应用拆分为多个小型、独立的应用,每个应用都可以独立开发、部署和升级。这种独立性带来了很多好处,包括减少代码冲突、提高开发并行度、加快上线速度等。同时,它也使得每个微应用可以独立地进行版本控制和迭代,从而实现了更灵活、更高效的开发流程。
- 增量升级:在微前端架构中,可以对单个微应用进行增量升级,而不需要对整个前端应用进行全量更新。这大大降低了升级过程中的风险和成本,同时提高了系统的可用性和稳定性。
- 团队协同与整合:微前端架构使得不同团队之间可以更加独立地工作,同时又能轻松地整合各自的成果。这种协同工作模式有助于提高开发效率和代码质量,同时也为团队之间的合作提供了更大的灵活性。
- 实施渐进式重构:对于大型的前端应用来说,重构往往是一个庞大而复杂的任务。然而,在微前端架构中,可以通过逐个重构微应用的方式来实现渐进式重构,从而降低重构的难度和风险。
- 更好的用户体验:微前端架构有助于实现页面的快速加载和渲染,从而提高用户体验。此外,由于每个微应用都是独立的,因此可以更容易地实现按需加载和代码分割,进一步优化页面加载速度和性能。
四、为什么需要接入qiankun
公司的前台项目是基于Nuxt3(主)、vue2(子)以及iframe实现的微前端,它的一些痛点想必前端开发的小伙伴们都有了解,诸如跨域、白屏时间长、浏览器返回按钮异常、应用通信等等一系列问题,会给用户带来不好的体验,基于这些,我们决定将微前端iframe替换为qiankun框架。
五、为什么选用qiankun
- 微前端目前主流框架 wujie、microApp、qiankun
- wujie(太新、对旧版本浏览器兼容性不太好)
- microApp(功能丰富导致配置项与api太多,成本比较大)
- qiankun(子应用接入成本高、样式隔离问题、解决方案多)
但目前npm周下载量 依旧是qiankun首居第一
主要从接入成本、功能稳定性、长期维护性三方面来衡量:
- 接入成本: wujie > microApp > qiankun (由低到高)
- 功能稳定性:qiankun > microApp > wujie
- 长期维护性:qiankun > microApp > wujie
原文链接---juejin.cn/post/730947…
六、具体实现
1.主应用nuxt3改造
a.下载依赖包qiankun
npm install qiankun
b. plugins目录下新建qiankun.client.ts文件,注册微应用,内容如下
import { registerMicroApps, start } from 'qiankun';
import actions from '../src/actions.ts';
const registerApps = () => {
registerMicroApps([
{
name: 'app1', // 此处的name是子应用的package.json中的name
entry: process.env.NODE_ENV === 'development' ? '//localhost:8080' : '/club/',
container: '#qiankun-content',
activeRule: '/app1',
props: {
actions
}
},
{
name: 'app2', // 此处的name是子应用的package.json中的name
entry: process.env.NODE_ENV === 'development' ? '//localhost:8090' : '/support/',
container: '#qiankun-content',
activeRule: '/app2',
props: {
actions
}
}
],
{
beforeLoad: () => {
// console.log("加载前");
},
beforeMount: () => {
// console.log("挂载前");
},
afterMount: () => {
// console.log("挂载后");
},
beforeUnmount: () => {
// console.log("销毁前");
},
afterUnmount: () => {
// console.log("销毁后");
},
}
);
start({
sandbox: {
experimentalStyleIsolation: true, // css scoped 样式隔离
},
})
}
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('registerMicroApps', registerApps());
});
c. 全局layouts文件夹下的default.vue文件中新建qiankun dom挂载元素
<template>
<HeaderPage />
<main class="layouts-default" >
<div v-show="isShowQiankunDom" id="qiankun-content"></div> // 重点是这句,需要在父应用中添加挂载点
<div v-show="!isShowQiankunDom">
<slot name="default" />
</div>
</main>
<FooterPage />
</template>
d. 新建actions.ts文件,作用: 主子应用通信以及数据状态更新
import { initGlobalState, MicroAppStateActions } from "qiankun";
const initialState = {
sign: '',
scroll: 0,
behavior: 'smooth'
};
/**
* 事件传递还是要少用,毕竟作为qiankun的子应用是可以独立运行的
* sign 子应用传给父应用的事件
*/
// 定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法
const actions: MicroAppStateActions = initGlobalState(initialState);
// 父应用通过 actions.onGlobalStateChange 监听子应用状态变化
actions.onGlobalStateChange((state, prev) => {
// console.log('主应用检测到state变更:', state);
// state: 变更后的状态; prev 变更前的状态
switch (state.sign) {
case 'aaa':
handleEvent()
break;
...
}
});
const handleEvent = () => {
// 此处写方法
}
export default actions;
e.全局components文件夹下新建app1和app2文件夹作为路由文件,内容如下
<template>
<div></div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
f.nuxt.config.ts文件中配置入口路由
export default defineNuxtConfig({
// 下面这段是核心
hooks: {
'pages:extend': (pages) => {
// 追加自定义的路由
pages.push(
{
path: '/app1',
file: resolve(__dirname, 'components//app1/index.vue'),
children: [
{
path: '//app1/:slug(.*)*', // 一定要加上这段兜底,不然qiankun匹配不到子应用的路由
file: resolve(__dirname, 'components/app1/index.vue')
},
]
},
{
path: '/app2',
file: resolve(__dirname, 'components/app2/index.vue'),
children: [
{
path: '/app2/:slug(.*)*', // 一定要加上这段兜底,不然qiankun匹配不到子应用的路由
file: resolve(__dirname, 'components/app2/index.vue')
},
]
}
)
}
}
});
2.子应用vue2改造
a.src目录下新建public-path.js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
b.main.js引入public-path.js
import './public-path.js'
c.main.js配置qiankun
let instance = null;
let router = null;
function render(props = {}) {
const { container,actions } = props;
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app1/' : '/',
mode: 'history',
routes,
});
instance = new Vue({
router,
store,
data(){
return {
parentActions: actions || '',
}
},
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
}
// 独立运行时
if (!window.__POWERED_BY_QIANKUN__) {
// console.log('独立运行时')
render();
}
export async function bootstrap() {
// console.log('子应用初始化');
}
}
export async function mount(props) {
// console.log('子应用初进入',props)
render(props);
}
export async function unmount() {
// console.log('子应用初卸载')
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
d.vue.config.js配置打包以及跨域等等devServer
const { name:packageName } = require('./package');
module.exports = {
/* 打包配置 */
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${packageName}` // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
/* webpack-dev-server 相关配置 */
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
};
至此,qiankun就已接入完成
七、主子应用通信以及事件处理
1. 事件处理
2. 主应用传值、子应用使用
// nuxt项目中 qiankun.client.js
registerMicroApps([
{
name: 'app1',
entry: process.env.NODE_ENV === 'development' ? '//localhost:8080' : '/club/',
container: '#qiankun-content',
activeRule: '/app1',
props: {
actions,
}
}
]
// 子应用在main.js中
export async function mount(props) {
console.log('bbs项目进入了',props)
proxy(props)
render(props);
}
// mount钩子中可以拿到传值
// 然后挂到vue实例中,详见如下
instance = new Vue({
router,
store,
data(){
return {
parentActions: actions || '',
}
},
render: (h) => h(App),
}).$mount(container ? container.querySelector('#app') : '#app');
// 在.vue页面可以这样使用
this.$root.parentActions.setGlobalState({
sign: '...',
})
八、部署
1.目前现状是主、子3个应用分别部署在不同的服务器中,部署配置详见 qiankun.umijs.org/zh/cookbook
九、踩坑血泪史
1.nuxt项目是SSR
(Server-Side Rendering):服务器端渲染,导致在服务端渲染时没有window
解决方案: plugins文件夹下的qiankun.client.js 这个文件一定要加.client
2.nuxt3匹配不到路由
3.进入子应用第一次能进,刷新页面就404
针对这个问题,根本原因是在iframe时,启用了nginx配置转发,qiankun则用不到nginx,所以导致这个问题
解决方案: 删除nginx相关配置
4.路由跳转偶现undefined
详见blog.csdn.net/chaoPerson/…
解决方案:
a.主应用nuxt3 plugins目录下新建routerBeforeEach.client.ts
// 解决qiankun模式下路由跳转undefined问题
const routerBeforeEach = () => {
const router = useRouter();
router.beforeEach((to, from, next) => {
if (process.client) {
window.scrollTo(0, 0)
if (window.history.state === null) {
history.replaceState({
back: from.path,
current: to.path,
forward: null,
position: NaN,
replaced: false,
scroll: null
},window.location.origin + to.path)
}
}
next()
})
}
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.component('routerBeforeEach', routerBeforeEach());
});
b.子应用路由的router.afterEach钩子中配置
router.afterEach((to, from, next) => {
window.history.pushState(null, ''); // // 解决qiankun模式下路由跳转undefined问题
})
5.样式污染
- qiankun提供了shadowDOM样式隔离 css-in-js ShadowDOM隔离css,但并不是完美的解决方案(可以自行了解)
- 建议使用约定式编程,即各主/子应用各自修改css class前缀
- base样式的污染建议维护一份通用css样式,主子应用统一引入
- ui组件库的样式污染可以找插件替换组件库的样式前缀,例如antd配置ConfigProvider、element-ui找插件配置postcss.config.js实现替换前缀
- 本项目使用experimentalStyleIsolation的方式实现隔离
- 带来的问题-子应用弹窗挂载点错误,导致样式丢失以及dom元素位置不正确
// 解决思路: 子应用main.js在加载时做document.appendChild的代理,如下
// 保留初始appendChild方法
const originalAppendChild = document.body.appendChild;
// 写一个代理appendChild方法
const proxy = ({ container }) => {
if (document.body.appendChild.__isProxy__) return
let revocable = Proxy.revocable(document.body.appendChild, {
apply(target, thisArg, [node]) {
if (container) {
container.appendChild(node)
} else {
target.call(thisArg, node)
}
}
})
document.body.appendChild = revocable.proxy
document.body.appendChild.__isProxy__ = true
}
export async function mount(props) {
// console.log('support项目进入了',props)
render(props);
proxy(props)
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
// console.log('support项目卸载了')
document.body.appendChild.__isProxy__ = undefined
document.body.appendChild = originalAppendChild
}
十、对于qiankun接入后的配置调整
1.因为是前台页面涉及到帖子、评论、回复等肯定在里面有链接等,替换qiankun后如果还是这些链接的话地址会有问题
解决方法: 备份数据库的帖子、评论、回复、通过写脚本匹配iframe的路由,再替换成qiankun的路由即可