微前端是一种架构风格,通过将一个单体应用程序拆分成多个小型独立应用程序来实现。每个小型应用程序都专注于一项特定功能,并且可以独立部署、扩展和维护。
大型单体应用程序随着时间的推移变得越来越复杂,难以维护
微前端允许团队根据其能力和需求独立工作,而不需要干扰其他团队
使用不同的技术堆栈开发各个独立应用程序,而无需考虑整体应用程序的技术选型
主框架不限制接入应用的技术栈,子应用具备完全自主权
子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新
独立运行时每个子应用之间状态隔离,运行时状态不共享
通过添加新应用程序来扩展系统而无需修改原始代码
每个应用程序都可以独立部署、减少了系统的停机时间
由于应用程序之间相互独立,因此如果一个应用程序发生故障,其他应用程序可以继续运行
导致依赖项冗余,增加用户的流量负担
团队自洽程度的增加,可能会破坏协作
qiankun能很方便的将一个巨石应用改造成一个基于微前端框架的系统,并且不再需要去关注各种过程中的技术细节,做到真正的开箱即用和生产可用
任意js框架均可使用,微应用接入像使用接入一个iframe系统一样简单,但实际不是iframe
几乎包含所有构建微前端系统时所需的基本能力,如样式隔离、js沙箱、预加载等
已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖
创建项目
npm init vue@latest
安装qiankun
yarn add qiankun
main.ts
注册微应用
import { registerMicroApps, addGlobalUncaughtErrorHandler, start } from 'qiankun';
// 注册子应用
registerMicroApps([
{
// 子应用名称,name值必须与子应用vite.config.ts文件中plugins属性qiankun的第一个参数值一致
name: 'subApp',
// 默认会加载这个路径下的html,解析里面的js
entry: '//localhost:5174',
// 加载的容器(微应用会显示到这个容器里面,一定要保证主应用中有这个容器)
container: '#subAppContainerVue3', // 和app.vue配置的节点一致
// 匹配的路由
activeRule: '/junminronghe', // 访问:http://localhost:5174/juminronghe
props: {
mag: '我是主应用main', // 主应用向微应用传递参数
domain: 'http://localhost:5174',
},
}
// 再有其他子应用,同理
]);
// 启动 qiankun
start({
prefetch:'all', // 预加载
sandbox: {
experimentalStyleIsolation: true, // 开启沙箱模式,实验性方案
},
});
// 添加全局异常捕获
addGlobalUncaughtErrorHandler((handler) => {
console.log("异常捕获", handler);
});
app.vue
创建挂载节点,如再有其他子应用,也是同理
<div id="subAppContainerVue3">div>
访问子应用
子应用
安装vite-plugin-qiankun
(qiankun
官方不支持vite
)需要安装插件
yarn add vite-plugin-qiankun
vite.config.ts
import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [
vue(),
// qiankun的第一个参数必须与主应用在main.ts中registerMicroApps的name值一致
qiankun('vue3', {
useDevMode: true
})
],
server:{
headers: {
'Access-Control-Allow-Origin': '*', // 主应用获取子应用时跨域响应头
},
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
main.ts
import { createApp } from 'vue'
import { renderWithQiankun, qiankunWindow, QiankunProps } from 'vite-plugin-qiankun/dist/helper'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
// app.mount('#app')
// renderWithQiankun: 为子应用导出一些生命周期函数 供主应用在特殊的时机调用
// qiankunWindow: qiankunWindow.POWERED_BY_QIANKUN 可判断是否在qiankun环境下
const initQianKun = () => {
renderWithQiankun({
// bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap
// 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等
bootstrap() {
console.log('bootstrap');
},
// 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法,也可以接受主应用传来的参数
mount(_props: any) {
console.log('mount', _props);
render(_props.container)
},
// 应用每次 切出/卸载 会调用的unmount方法,通常在这里我们会卸载微应用的应用实例
unmount(_props: any) {
console.log('unmount', _props);
},
update: function (props: QiankunProps): void | Promise<void> {
console.log('update');
}
});
}
const render = (container) => {
// 如果是在主应用的环境下就挂载主应用的节点,否则挂载到本地
const appDom = container ? container : "#app"
app.mount(appDom)
}
// 判断是否为乾坤环境,否则会报错[qiankun]: Target container with #subAppContainerVue3 not existed while subAppVue3 mounting!
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render(null)
router——》index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('../views/home/index.vue')
},
// 文章详情
{
path: '/content',
name: 'content',
component: () => import('../views/content/index.vue')
},
];
const router = createRouter({
history: createWebHistory(
// 在乾坤环境下时,将所有的路由地址前加上'/junminronghe',否则将跳转至主应用的相应页面(eg:跳转至这里的文章详情页面,如果不加'/junminronghe'的话,将跳转至主应用的'/content'页面,如果主应用没有这个页面,将跳转至404页面)
qiankunWindow.__POWERED_BY_QIANKUN__ ? '/junminronghe' : '/'
),
routes
})
export default router
解决方法:
在子应用的vite.config.ts文件的server对象中配置origin
属性,配置为子应用的链接地址
server:{
port: 5174,
// origin:用于定义开发调试阶段生成资产的 origin
// 子应用的地址,用于解决主应用中子应用出现静态地址404问题,控制台报错显示该资源在主应用路劲下
origin: 'http://localhost:5174',
}
在创建axios
实例时,配置baseURL
子应用:
// 创建 axios 实例
const instance = axios.create({
// API 请求的默认前缀
// "/junminrongheApi"要跟主应用,vite.config.ts中,代理的地址一致,给所有的接口请求地址前加“/junminrongheApi"字符串,表明是junminronghe子应用的接口
baseURL: qiankunWindow.__POWERED_BY_QIANKUN__ ? '/junminrongheApi' : '',
timeout: 60 * 1000, //设置超时
headers: {
"Content-Type": "application/json;charset=UTF-8;",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
".AspNetCore.Culture": "c=zh-Hans|uic=zh-Hans",
},
});
主应用:vite.config.ts
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// "/junminrongheApi"要跟子应用,service——》index.ts中,baseURL的地址一致(即axios的baseURL地址一致),
// 将junminronghe子应用的接口地址代理到其自己的地址(即target与主应用main.ts中registerMicroApps的entry值,保持一致)即可
"/junminrongheApi": {
target: 'http://localhost:5174', // 后台接口
changeOrigin: true,
secure: false, // 如果是https接口,需要配置这个参数
// ws: true, //websocket支持
rewrite: (path) => path.replace(/^\/junminrongheApi/, ""),
},
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
注:这个是因为这里的图片地址不是常规的地址,而是像这样的地址:sysFileInfo/preview?id=1666025519278534658,所以才会出现这个问题,这个问题是特例,正常的图片地址(eg:https://vitejs.cn/vite3-cn/logo.svg
)就不会有这个问题
解决方法:
在所有的图片地址前,手动拼接一个请求地址,可以封装一个图片组件,统一管理
eg:
<n-image :object-fit="props.objectFit" :src="previewUrl + props.url" />
富文本中的图片,在加载时,图片地址(sysFileInfo/preview?id=1666025519278534658)前会被拼上主应用的地址及前面配置的子应用路由拼接的'/junminronghe'
(目前不知道是为啥)
解决方法:
主应用vite.config.ts
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// "/junminrongheApi"要跟子应用,service——》index.ts中,baseURL的地址一致(即axios的baseURL地址一致),
// 将junminronghe子应用的接口地址代理到其自己的地址(即target与主应用main.ts中registerMicroApps的entry值,保持一致)即可
"/junminrongheApi": {
target: 'http://localhost:5174', // 后台接口
changeOrigin: true,
secure: false, // 如果是https接口,需要配置这个参数
// ws: true, //websocket支持
rewrite: (path) => path.replace(/^\/junminrongheApi/, ""),
},
// 加这个代理是因为像子应用中如果有富文本,并且富文本中有图片的话,请求的图片会在主应用的地址后面拼上子应用的路由地址(即这里的junminronghe)后再是图片的地址,
// 当然是因为这里的子应用的图片地址不是常规的地址,而是像这样的地址:sysFileInfo/preview?id=1666025519278534658,才会有这样的问题
"/junminronghe": {
target: 'http://localhost:5174', // 后台接口
changeOrigin: true,
secure: false, // 如果是https接口,需要配置这个参数
// ws: true, //websocket支持
rewrite: (path) => path.replace(/^\/junminronghe/, ""),
},
}
},
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})