最近需要在老的vue2运营后台中集成新的模块,看着越发臃肿的代码洒家陷入了沉思,这样下去工作的幸福感要去哪里寻找呢?况且没有typescript这可是个大问题,要升级改造老项目风险太大。
一般遇到这种问题,第一想到的是iframe加载的方式,但是这会存在几个问题。
- 浏览器的完全隔离,文件与数据共享很难被共享,一旦有主应用跟子应用有复杂交互的时候很难维护,不过一般不敏感的参数通过url也都能实现。
- 资源加载都要刷新,主应用的返回前进路由无法控制。
不过之前遇到这问题,我们通过在主项目中的静态文件目录public中加载子项目的dist文件,通过主应用的路由与iframe的路由做映射去访问,很好的优化了上诉问题,数据共享可以通过本地存储搞定,也可以写全局监听。
这个方案如果对项目的隔离部署有啥要求,并且对包的大小不敏感,可以说是很好处理方式,很传统,但是很稳当,可以让你安心下班!
BUT,日新月异的技术变化,微前端始终是一个绕不过去问题,SO,继续折腾吧。
目前比较主流的微前端框架有几个
阿里的乾坤 qiankun.umijs.org/zh/cookbook
腾讯的无界 wujie-micro.github.io/doc/
京东的micro-app zeroing.jd.com/micro-app/d…
single_spa single-spa.js.org/
最开始看的乾坤,但是其文档写的实在是,怎么说呢,惜字如金!而且对项目的侵入性很强,改的面目全非还不能跑起来,着实是恶心到人了。遂转战micro-app,顿感清新舒畅,接下来大概描述一下其使用过程。
这里用vue3做演示,默认你已经安装好了最新版的脚手架 vue-cli cli.vuejs.org/zh/guide/ ,创建一个mian_app的主项目,一个child_app_1的子项目
安装嵌入一条龙服务看文档即可 zeroing.jd.com/micro-app/d…
这里把需要单独配置的文件贴出来
//main_app、child_app_1的配置文件调整,主要是子应用的跨域配置
//vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: false, //关闭eslint检查
devServer: {
host: '0.0.0.0',
port: 8080,//子应用配置为3000
headers: {
'Access-Control-Allow-Origin': '*',//子应用必须开启跨域,不然无法访问
}
},
})
//main_app的router.js
import { createRouter, createWebHistory } from "vue-router";
import Test from './view/Test.vue';
import Test2 from './view/Test2.vue';
const routes = [
{
path: "",
redirect:'/test'
},
{
path: '/test2', // vue-router@4.x path的写法为:'/my-page/:page*'
name: 'test2',
component: Test2,
},
{
path: '/test/:page*', // vue-router@4.x path的写法为:'/my-page/:page*'
name: 'test',
component: Test,
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
//这里是为了处理返路由返回时候的问题
if (window.__MICRO_APP_ENVIRONMENT__) {
// 如果__MICRO_APP_BASE_ROUTE__为 `/基座应用基础路由/子应用基础路由/`,则应去掉`/基座应用基础路由`
// 如果对这句话不理解,可以参考案例:https://github.com/micro-zoe/micro-app-demo
const realBaseRoute = window.__MICRO_APP_BASE_ROUTE__
router.beforeEach(() => {
if (typeof window.history.state?.current === 'string') {
window.history.state.current = window.history.state.current.replace(new RegExp(realBaseRoute, 'g'), '')
}
})
router.afterEach(() => {
if (typeof window.history.state === 'object') {
window.history.state.current = realBaseRoute + (window.history.state.current || '')
}
})
}
export default router;
//main_app 入口文件调整
import { createApp } from 'vue';
import microApp from '@micro-zoe/micro-app';
import App from './App.vue';
import router from './router';
const app = createApp(App);
app
.use(router)
.mount('#app');
//main_app的Test.vue
<template>
<h1 class="red">主应用基座</h1>
<router-link to="/test2">test2,打开控制台看卸载监听</router-link>
<br/><br/>
<button @click="sendChildData">发送数据给子应用并监听</button>
<br/><br/>
<div class="childBox">
<micro-app
name="app1"
url="http://localhost:3000/"
baseroute="/test"
inline
@created='created'
@beforemount='beforemount'
@mounted='mounted'
@unmount='unmount'
@error='error'
:data="sendData"
@datachange='handleDataChange'
></micro-app>
</div>
</template>
<script setup>
import { ref } from "vue";
import micro from '@micro-zoe/micro-app';
const handleDataChange = (e)=>{
console.log('来自子应用的数据:', e.detail.data)
}
const sendChildData = ()=>{
micro.setData('app1', {type: '新的数据'})
}
const sendData = ref({
data:'基座给子应用的数据string'
})
const created = () => {
console.log('micro-app元素被创建');
}
const beforemount = () => {
console.log('即将被渲染');
}
const mounted = () => {
console.log('已经渲染完成');
}
const unmount = () => {
console.log('已经卸载');
}
const error = () => {
console.log('渲染出错');
}
</script>
<style>
.childBox {
border: 1px solid red;
}
</style>
//main_app的Test2.vue,主要用来测试跳转生命周期监听
<template>
<h1 class="red">其他页面</h1>
</template>
<script setup>
import { ref } from "vue";
</script>
<style>
.childBox {
border: 1px solid red;
}
</style>
分割线,子应用配置====================================================
//chlld_app_1 入口文件
import './public-path'
import { createApp } from 'vue';
import microApp from '@micro-zoe/micro-app';
import App from './App.vue';
import router from './router';
import './assets/style.css';
microApp.start();
const app = createApp(App);
app
.use(router)
.use(microApp)
.mount('#app');
// 子应用卸载
window.addEventListener('unmount', function () {
console.log('我是子应用,我被卸载了')
})
if (window.__MICRO_APP_ENVIRONMENT__) {
console.error('我在微前端环境中,基座给我的名字是',__MICRO_APP_NAME__)
}
function dataListener (data) {
console.log('来自基座应用的数据', data);
setTimeout(() => {
console.log('数据被更新了',window.microApp.getData());
}, 1000);
}
console.log('window.microApp==>',window.microApp)
window.microApp.addDataListener(dataListener)
//子应用 './public-path' 路径判断文件
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
//router.js文件
import { createRouter, createWebHistory } from "vue-router";
import Test from './view/Test.vue';
import Home from './view/Home.vue';
const routes = [
{
path: "",
redirect:"/home"
},
{
path: "/home",
name: 'home',
component: Home,
},
{
// 👇 非严格匹配,/my-page/* 都指向 MyPage 页面
path: '/app1/:page*', // vue-router@4.x path的写法为:'/my-page/:page*'
name: 'app1',
component: Test,
}
]
const router = createRouter({
history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL),
routes
});
export default router;
// view/Home.vue 文件
<template>
<h1 class="red">子应用 Home</h1>
<br/><br/>
<button @click="callBase">向基座发送数据</button>
</template>
<script setup>
import { ref } from 'vue';
const data = window.microApp.getData();
console.log('我在子应用获取到基座数据',data);
const callBase = ()=>{
window.microApp.dispatch({type: '我是子应用发送过来的数据'});
}
</script>
// view/Test.vue文件,用于跳转测试
<template>
<h1 class="red">子应用 app1</h1>
</template>
<script setup>
import { ref } from 'vue';
</script>
准备就绪,启动基座与子应用服务,准备起飞=================================================
控制台可以看到通信信息