目前在做的项目,涉及到了多个管理端的系统交互,随着需求的迭代升级,有部分老的代码面临着重构优化的窘态,但是新的需求也一直在来。同时也为了让用户能有更好的体验感,发挥单页面应用的优点,因此有了将项目改造成微前端的想法。现有的项目多为webpack+vue2
和vite+vue3
的项目,将其改造成微应用,再使用vite+Vue3
搭建一个主应用来注册上述改造后的微应用,迁移所有的权限控制相关的功能到主应用,由主应用统一进行权限控制。
一、到底要不要用微前端?
当考虑微前端时,面临着一个最前提的条件,我们的项目需不需要用微前端,或者说适合不适合使用微前端?然后便看到了这篇文章《你可能并不需要微前端》,另外就是要先弄清楚微前端的优点和难点。
- 优点
- 解耦,模块化开发
- 技术栈无关
- 并行开发
- 独立部署
- 难点
- 性能问题
- 技术复杂度问题
- 安全性问题
二、qiankun2.X
与vite
的"情感纠葛"
目前
qiankun2.X
官方并不支持vite
的构建
在这个过程中首先遇到的问题是,qiankun2.x
目前不支持vite
,此处参考来自文章《 微应用(qiankun)之 vite 构建》。
1. 存在问题
vite
的模块加载方式是esm
,而qiankun
并不支持在非module script
标签内解析ems
格式的代码,导致子应用无法正确加载。- 对于
react-refresh
需要全局变量__vite_plugin_react_preamble_installed
, 由于qiankun
使用window+proxy
实现了js
沙箱,所以这里的全局变量实际上会被挂载到window.proxy
上,导致后续访问全局变量会报错。 vite
不支持运行时publicPath
的配置,而qiankun
需要在运行时动态修改publicPath
来加载子应用的资源。
2. 解决方案
当前可以借助vite-plugin-qiankun
插件解决上述问题,它通过 dynamic import()
的形式来加载子应用程序,从而使得可以在模块系统【 ESM
和 CommonJS
】下均可正确加载子应用程序;同时它还会在 window
上挂载 __INJECTED_PUBLIC_PATH_BY_QIANKUN__
全局变量,用于在运行时动态修改 publicPath
来加载子应用的资源。详情参考
三、使用vue3 + vite
构建主应用(hash
路由模式)
主应用不限技术栈,只需要提供一个容器
DOM
,然后注册微应用并start
即可。
因为目前的项目都是hash
路由模式,所以先尝试了基于hash
路由模式搭建vue
框架的qiankun
主应用,虽然这种方式并不推荐。
- 使用
vite
初始化一个vue3
项目
bash
# 使用npm
$ npm create vite@latest qiankun-main --template vue-ts
# 使用yarn
$ yarn create vite qiankun-main ----template vue-ts
- 安装
qiankun
bash
$ yarn add qiankun
- 新建
qiankun/index.ts
文件
ts
import {registerMicroApps, start} from 'qiankun'
const DEV = import.meta.env.DEV
/**
* 要注册的微应用
*/
const microApps = [
{
name: 'foo', // 微应用名称,具有唯一性,可理解为微应用的唯一id
entry: DEV ? '//localhost:8081' : '/main/microApps/foo', // 微应用入口,通过该地址加载微应用
container:'#container', // 微应用挂载节点,微应用加载完成后将挂载在该节点上
activeRule:'#/micro/foo', // 微应用触发的路由规则,触发路由规则后将加载该微应用
props:{} // 主应用需要传递给微应用的数据
},
{
name: 'bar', // 微应用名称,具有唯一性,可理解为微应用的唯一id
entry: DEV ? '//localhost:8082' : '/main/microApps/bar', // 微应用入口,通过该地址加载微应用
container:'#container', // 微应用挂载节点,微应用加载完成后将挂载在该节点上
activeRule:'#/micro/bar', // 微应用触发的路由规则,触发路由规则后将加载该微应用
props:{} // 主应用需要传递给微应用的数据
}
]
/**
* 在主应用中注册微应用
*/
registerMicroApps(microApps,{
beforeLoad:(app) => {
console.log('before load',app.name)
return Promise.resolve()
}
})
// 启动qiankun 推荐在页面中开启qiankun
start()
微应用加载方式
- 基于路由配置:通过将微应用关联到一些
url
规则的方式,实现当浏览器url
发生变化时,自动加载相应的微应用的功能。 - 手动加载微应用:适用于需要手动 加载/卸载 一个微应用的场景
- 在
mian.ts
文件中引入qiankun/index.ts
文件
ts
import './qiankun/index.ts
- 修改
router/index.ts
文件
ts
import {createRouter, createWebHashHistory} from 'vue-router'
import microAppVue = '@/views/microApp.vue'
const routes = [
{
path:'/',
components:() => import('@/views/home.vue')
},
{
path:'/micro/:chapters+',
components:microAppVue
}
]
const router = createRouter({
hisrory:createWebHashHistory(),
routes
})
export default router
- 新增
views/microApp.vue
文件
vue
<template>
<div id="container"> </div>
</template>
四、使用vue2 + webpack
构建微应用(hash路由模式)
-
使用
vue-cli
构建一个vue2
或者vue3
的项目略......
-
在
src
目录新增public-path.js
文件
js
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 修改
main.js
文件,为了避免根 id#app
与其他的DOM
冲突,需要限制查找范围。
js
import './public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router/index.js';
Vue.config.productionTip = false;
let router = null;
let instance = null;
function render(props = {}) {
const { container } = props;
instance = new Vue({
router,
store,
render: (h) => h(App),
}).$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() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
router = null;
}
- 修改
router/index.js
文件
js
import VueRouter from 'vue-router'
const routes = [
{
path:'/micro/foo/home', // 此处的path前缀要与主应用里的activeRule:'#/micro/bar'一一对应,但不需要#
component:() => import('@/views/home.vue')
}
]
- 修改
vue.config.js
文件
js
const { name } = require('./package');
module.exports = {
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd', // 把微应用打包成 umd 库格式
jsonpFunction: `webpackJsonp_${name}`, // webpack 5 需要把 jsonpFunction 替换成 chunkLoadingGlobal
},
},
};
五、使用vue3 + vite
构建微应用
- 安装
yarn add vite-plugin-qiankun
- 修改
vite.config.js
ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
// https://vitejs.dev/config/
export default defineConfig({
base: DEV? '/' : '/main/microApps/bar', // 部署时此路径应与主应用注册的entry需保持一致
plugins: [
vue(),
qiankun('bar', {
useDevMode: true
})
],
server: {
port: 7001,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
})
- 修改路由配置
js
import { createRouter, createWebHistory } from 'vue-router'
import routes from './routes'
import { qiankunWindow } from 'vite-plugin-qiankun/es/helper'
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
- 在
main.js
文件,添加生命周期
js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/es/helper'
let app
const render = (container) => {
app = createApp(App)
app
.use(router)
.mount(container ? container.querySelector('#app') : '#app')
}
const initQianKun = () => {
renderWithQiankun({
mount(props) {
const { container } = props
render(container)
},
bootstrap() {},
unmount() {
app.unmount()
}
})
}
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()
六、服务器部署结构
js
└── main // 主应用文件夹
├── microApp // 所有微应用的父级文件夹
│ └── foo // 微应用一
│ └── bar // 微应用二
├── css
├── favicon.ico
├── index.html
└── js