一 什么是微前端
- 微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略
- 微前端架构具备以下几个核心价值:
- 技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 - 独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 - 增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略 - 独立运行时
每个微应用之间状态隔离,运行时状态不共享
- 微前端和iframe对比
1、iframe
iframe 作为一项非常古老的技术,也可以用于实现微前端。通过 iframe,我们可以很方便的将一个应用嵌入到另一个应用中,而且两个应用之间的 css 和 javascript 是相互隔离的,不会互相干扰。
优点:
实现简单;
css 和 js 天然隔离,互不干扰;
完全技术栈无关;
多个子应用可以并存;
不需要对现有应用进行改造;
缺点:
用户体验不好,每次切换应用时,浏览器需要重新加载页面;
UI 不同步,DOM 结构不共享;
全局上下文完全隔离,内存变量不共享,子应用之间通信、数据同步过程比较复杂;
对 SEO 不友好;
子应用切换时可能需要重新登录,体验不好;
2、single-spa:最早的微前端框架,兼容多种前端技术栈
现在前端应用开发的主流模式为基于 vue / react/ angular 的单页应用开发模式。在这种模式下,我们需要维护一个路由注册表,每个路由对应各自的页面组件 url。切换路由时,如果是一个新的页面,需要动态获取路由对应的 js 脚本,然后执行脚本并渲染出对应的页面;如果是一个已经访问过的页面,那么直接从缓存中获取已缓存的页面方法,执行并渲染出对应的页面。
在 single-spa 方案中,应用被分为两类:基座应用和子应用。其中,子应用就是需要聚合的子应用;而基座应用,是另外的一个单独的应用,用于聚合子应用。
和单页应用的实现原理类似,single-spa 会在基座应用中维护一个路由注册表,每个路由对应一个子应用。基座应用启动以后,当我们切换路由时,如果是一个新的子应用,会动态获取子应用的 js 脚本,然后执行脚本并渲染出相应的页面;如果是一个已经访问过的子应用,那么就会从缓存中获取已经缓存的子应用,激活子应用并渲染出对应的页面。
优点:
切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
完全技术栈无关;
多个子应用可并存;
生态丰富;
缺点:
需要对原有应用进行改造,应用要兼容接入 sing-spa 和独立使用;
有额外的学习成本;
使用复杂,关于子应用加载、应用隔离、子应用通信等问题,需要框架使用者自己实现;
子应用间相同资源重复加载;
启动应用时,要先启动基座应用;
3、qiankun :基于Single-Spa,阿里系开源微前端框架
和 single-spa 一样,qiankun 也能给我们提供类似单页应用的用户体验。qiankun 是在 single-spa 的基础上做了二次开发,在框架层面解决了使用 single-spa 时需要开发人员自己编写子应用加载、通信、隔离等逻辑的问题,是一种比 single-spa 更优秀的微前端方案。
优点:
切换应用时,浏览器不用重载页面,提供和单页应用一样的用户体验;
相比 single-spa,解决了子应用加载、应用隔离、子应用通信等问题,使用起来相对简单;
完全和技术栈无关;
多个子应用可并存;
缺点:
需要对原有应用进行改造,应用要兼容接入 qiankun 和独立使用;
有额外的学习成本;
相同资源重复加载;
启动应用时,要先启动基座应用;
二 qiankun (vue3+vite+ts+history)
1. 安装qiankun
npm i qiankun -S
2. 在主应用中注册微应用
- 新建
src/qiankun/index.ts
文件,进行单独的抽离
import { registerMicroApps,setDefaultMountApp} from "qiankun";
const microApps = [
{
// - 必选,微应用的名称,微应用之间必须确保唯一
name: "vue-app",
// - 必选,微应用的入口
entry: import.meta.env.MODE === 'development'? '//localhost:9001/' : '/subPath/',
// - 必选,微应用的容器节点的选择器或者 Element 实例
container: "#subApp",
// - 必选,微应用的激活规则
//支持直接配置字符串或字符串数组,如 activeRule: '/app1' 或 activeRule: ['/app1', '/app2'],当配置为字符串时会直接跟 url 中的路径部分做前缀匹配,匹配成功表明当前应用会被激活。
//支持配置一个 active function 函数或一组 active function。函数会传入当前 location 作为参数,函数返回 true 时表明当前微应用会被激活。如 location => location.pathname.startsWith('/app1')。
activeRule: "/subPath", //匹配所有以/subPath开头的为子应用
//loader - (loading: boolean) => void - 可选,loading 状态发生变化时会调用的方法。
//可选,主应用需要传递给微应用的数据。
props: {
_parent_base: '/subPath/',
msg:'这是主应用传给子应用的消息'
},
},
];
// 乾坤提供的子应用生命周期钩子 可用于加载loading
const mount={
beforeLoad: () => {
//开始加载loading
console.log('子应用加载前')
},
beforeMount: () => {
console.log('子应用挂载前')
},
afterMount: () => {
//结束加载loading
console.log('子应用挂载后')
},
beforeUnmount: () => {
console.log('子应用卸载前')
},
afterUnmount: () => {
console.log('子应用卸载后')
},
}
//注册子应用
registerMicroApps(microApps,mount);
//设置默认启动子应用
setDefaultMountApp('/subPath');
//启动qiankun | 不可重复启动 | 如果子应用入口在app.vue里可以在这启动否则会报错找不到子应用结点
//start()
- 在
main.ts
导入
import "./qiankun"
//其他配置不变
- 挂载微应用节点
<!-- 子程序入口 layouts/index.vue -->
<el-main>
<router-view></router-view>
<div id="subApp"></div>
</el-main>
<!--在vue3的onMounted里启动qiankun -->
onMounted(() => {
<!--启动qiankun -->
start()
})
- 配置vite.config.ts
import { defineConfig } from 'vite'
export default defineConfig(({ command, mode }) => {
return {
//········
server: {
host: '0.0.0.0',
port: 9003,
hmr: true,
disableHostCheck: true,
proxy: {
'/api': { //主服务代理
target: 'http://192.168.3.101:1171/', //服务
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
'/subApi': { //子应用代理
target: 'http://192.168.3.101:3434/', //服务
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}
})
3. 子应用中使用
- qiankun目前暂不支持vite项目 vite项目需要安装社区依赖
vite-plugin-qiankun
npm install vite-plugin-qiankun
- 修改
vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
export default ({ mode }) => {
return defineConfig({
/**
* 代理的问题:微应用打包的 base 必须跟主应用中的代理地址 entry 值一致
* 但是,加上 '/subPath 之后,就必须部署 '/subPath 目录,否则无法独立访问。
*/
base: mode === 'development' ? 'http://localhost:9001/' : '/subPath/',
plugins: [
vue(),
qiankun('vue-app', { // 微应用名字,与主应用注册的微应用名字保持一致
useDevMode: true
})
],
server: {
headers: {
'Access-Control-Allow-Origin': '*', //防止主应用访问跨域
},
host: '0.0.0.0',
port: 9001,
origin: 'http://localhost:9001', //防止去主应用请求静态资源出现404
hmr: true,
disableHostCheck: true,
proxy: {
'/api': {
target: 'http://192.168.3.101:3434/', //服务
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
}
})
- 修改
main.ts
import { App as VueApp, createApp } from 'vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let app: VueApp<Element>
//判断是否为qiankun环境
if (!qiankunWindow.__POWERED_BY_QIANKUN__) {
//自立独立环境正常挂在加载
// router(''),独立运行,路由前缀为空
createApp(App).use(router('')).use(store).use(iweb).use(dispatchEventStrage).mount('#app')
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
createApp(App).component(key, component)
}
createApp(App).directive('load', vLoading)
} else {
//qiankun环境
renderWithQiankun({
mount(props) {
//props为主应用传过来的data
app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.directive('load', vLoading)
//正常注册 最后挂载在主应用上
//路由前缀添加router(props._parent_base),_parent_base是从主应用中传过来的
app
.use(router)
.use(store(props._parent_base))
.use(iweb)
.use(dispatchEventStrage)
.mount(
(props.container
? props.container.querySelector('#app') //挂载在主应用
: document.getElementById('app')) as Element
)
},
bootstrap() {
console.log('--app 01 bootstrap')
},
update() {
console.log('--app 01 update')
},
unmount() {
console.log('--app 01 unmount')
app?.unmount() //离开时卸载
},
})
}
- 修改
axios.ts
中代理
import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let baseURL: string = ''
if (!qiankunWindow.__POWERED_BY_QIANKUN__) { //独立运行
baseURL = '/api'
} else {
baseURL = '/subApi' //微应用与主应用中配置的子应用代理一致
}
- 修改入口文件 如果是微应用访问取消菜单栏等,只显示main
4.部署nginx - 目录结构
-
主应用
nginx/web
-
子应用
nginx/web/subPath
(subPath
为子应用打包base对应的目录)
- nginx配置
server { #主应用
listen 88;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root web;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
#主应用代理
location /api/ {
proxy_pass http://192.168.3.101:1171/;
}
#代理到子应用
location /p1800/ {
proxy_pass http://localhost:89/p1800/;
}
#子应用的代理 1
location /subApi/ {
proxy_pass http://192.168.3.101:3434/;
}
}
server { #子应用
listen 89;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location /p1800 { #对应文件夹名称
root web;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location /api/ {
proxy_pass http://192.168.3.101:3434/;
}
}