微前端-大规模使用记录

202 阅读10分钟

1、概述

1、iframe嵌入子系统,弊端太多,体验太差;微前端可以很好的解决其弊端

2、但是微前端作为一种非标准技术方案,漏洞比较多

  • 样式污染
  • 脚本污染

3、目前系统中应用需要区分主、微应用的技术方案差异带来的兼容

  • vue2,vue3
  • hash、history
  • element-ui、element-plus、ant-design
  • 原生html微应用

4、组件中渲染容器存在问题,应用中需要避开

5、微应用分为嵌入系统(复数公用)、局部嵌入某一页面,两种应用情况

6、遗留的问题,重写微应用router实例存在问题(router实例重复,路由跳转麻烦)

2、解决方案

2.1、样式污染

qiankun的沙箱隔离,在我实际使用中发现,很多情况下无法避免污染,甚至很多环境无法生效

  • 不允许使用全局样式,必须使用scoped
  • 脚本挂载的全局样式必须指定dom,不能随意挂载向html、body等根元素
  • 切换其他微应用,前一个微应用依然会残留影响无法去除;因此如果必须写全局样式,需存在命名空间

2.2、脚本污染

  • vue-router相互影响是最大的问题,解决方案如下

3、技术差异

  1. vue2使用官方推荐解决方案

  2. vue3使用vite-plugin-qiankun的方案

  3. 主微同是element-ui时,微应用使用独立entry打包element样式,主应用加载微应用时将其剔除(版本有差异,保证主应用版本高于微应用),主题统一

  4. 主是element-ui,微是element-plus

    1. 微应用须自定义element命名空间,避免其对主应用的影响,主题不统一
    2. 主应用通过改变html标签的css变量实现主题统一
  5. 主是anti-design,微是element-plus;同上

  6. 原生html微应用,使用qiankun的fetch自定义,使其注入必要的生命周期,不考虑主题

4、容器

在组件template中渲染容器,存在路由加载速度问题

  1. 主应用进入路由A,渲染A组件,组件A中包含渲染微应用的容器,加载微应用
  2. 主应用切换路由至其他路由B
  3. 主应用再次进入路由A,在该步骤会出错

步骤a完成后,页面中存在qiankun实例,监听location变化;

步骤c中主应用第二次进入路由A时,主应用首先是location变化,然后渲染对应的组件A,此时qiankun程序执行早于组件A的渲染,导致qiankun找不到挂载的容器元素报错

两个处理方法:

4.1、主应用的布局组件中常驻微应用容器

<template>
  <section class="main-layout" :class="{ 'is-micro-on': microAppVisible }">
    <Breadcrumb />
    <transition name="fade-transform" mode="out-in">
      <router-view v-if="!$route.meta.keepAlive" :key="key" />
    </transition>
    <transition name="fade-transform" mode="out-in">
      <keep-alive>
        <!-- 需要缓存的视图组件 -->
        <router-view v-if="$route.meta.keepAlive" />
      </keep-alive>
    </transition>
    <transition name="fade-transform" mode="out-in">
      <div v-show="microAppVisible" id="micro-app-container" class="micro-app-container" :class="'micro_' + appRoot" />
    </transition>
  </section>
</template>

4.2、动态渲染一个微应用容器

在微应用第一挂载时渲染一个div,该div不受vue影响会一直存在;其dom中位置与上面的方法一样

<template>
  <div ref="mark"> </div>
</template>
<script lang="ts" setup>
  import { onMounted, ref } from 'vue';
  const containerId = 'micro-app-container';
  let container: HTMLElement;
  const isExist = !!document.querySelector('#' + containerId);
  if (!isExist) {
    container = document.createElement('div');
    container.setAttribute('id', containerId);
    container.setAttribute('class', 'ele-body');
    container.setAttribute('style', 'display:none');

    //......  
  }
  const mark = ref();
  onMounted(async () => {
    if (!isExist) {
      mark.value.parentElement.appendChild(container);
      //......  
    }
  });
</script>

5、应用情况

5.1、微应用脚手架配置

5.1.1、webpack

  publicPath:"/xxx", // base保持与微应用名称一致
  configureWebpack: {
      output: {
          library: `${require('./package.json').name}-[name]`,
          libraryTarget: 'umd', // 把微应用打包成 umd 库格式
          jsonpFunction: `webpackJsonp_${require('./package.json').name}`
      }
  },
  chainWebpack(config) {
      config.entryPoints.clear(); // 清空默认入口
      // elementUI主题样式引入
      config.entry('element-theme').add(path.resolve('./src/styles/element-variables.scss'));

      config.entry('app').add(path.resolve('./src/main.js')); // 重新设置
      
      //......
  },
  transpileDependencies: ['qiankun', 'import-html-entry']

5.1.2、vite

import qiankun from 'vite-plugin-qiankun';

    base:"/xxx", // base保持与微应用名称一致
    plugins: [
        qiankun('xxx', {         // 微应用名字,与主应用注册的微应用名字保持一致
            useDevMode: true,
        })
    ]

5.2、嵌入系统(复数公用)

qiankun官方推荐的方案,需要指定加载的微应用;此处处理为根据url自动加载对应的子系统

主应用有hash、history,此处以history为例,hash参考后续的应用案例

主应用

  1. 主应用创建占位路由
  2. 布局组件中监听路由变化,识别是否加载微应用
  3. 识别出加载的微应用
  4. 如果前一个微应用与当前相同,不重复;如果不同卸载前一个
  5. 调整微应用base,调整微应用路由,调整微应用的push和replace
  6. 沙箱加载微应用

hash微应用

  1. 微应用生命周期导出,支持主应用调整
  2. base兼容主应用当前路径,或/
  3. 路由中以/micro-app开头兼容
  4. router.push/router.replace支持以/micro-app开头(此处与上述冲突要兼容)

history微应用

  1. 微应用生命周期导出,支持主应用调整
  2. base兼容主应用当前路径(/micro-app加进去了,所以后续设置不需要了),必须设置
  3. 支持跳转至hash路由

5.2.1、主应用创建占位路由

该路由没有什么用,只是主应用需要有对应的页面;此处布局容器常驻容器的做法,如果容器选择动态渲染,参考上述案例

      {
          // 微前端应用-hash
          path: 'micro-app',
          name: 'micro-app',
          component: () => import('@/views/micro-app.vue'),
          meta: { title: '', microApp: true }
      },
      { 
          // 微前端应用-history
          path: 'micro-app/*',
          name: 'micro-app',
          component: () => import('@/views/micro-app.vue'),
          meta: { title: '', microApp: true }
      }
<template>   <div /> </template>

5.2.2、识别微应用

通过location.href路径来判断是否加载微应用,path中包含“/micro-app”即加载微应用,其后跟随的即对应的系统,例如:“/micro-app/XXX”,XXX就是子系统

layout.vue文件中监听路由变化,判断是否微应用加载;

注意区分hash、history模式,

import routerAdapter from '@/utils/microRouterAdapter'

watch: {
    $route: {
        immediate: true,
        handler(to) {
              this.displayMicroApp(to);
        }
    }
},
methods: {
    async displayMicroApp(to) {
        const TAG = '/micro-app'
        let displayMicro = to.path.includes(TAG);
        if (IEVersion() !== -1) displayMicro = false; // 不是ie时启用微前端,ie用iframe
        if (!displayMicro) {
            return (this.microAppVisible = false); // 不是微前端微应用路径
        }
        let appRoot;
        if (this.$route.params.pathMatch) {
            // 微应用history模式
            window._isMicroHash = false;
            // xxx/../..
            appRoot = this.$route.params.pathMatch.split('/')[0];
        } else {
            // 微应用hash模式
            window._isMicroHash = true;
            // #/micro-app/xxx/../..
            appRoot = location.hash.split('/')[2];
        }
        if (!appRoot) return (this.microAppVisible = false); // 没有微应用名称 
        this.microAppVisible = true;
        this.appRoot = appRoot 
        // 同一微应用不重复加载 
        if (window._microAppRoot && appRoot === window._microAppRoot) { 
            console.log('the same micro app with last one, no need to load again')  
            return;   
        }      
        window._microAppRoot = appRoot; 
        // 如果已经存在另一个微应用了, 切换微应用 
        if (window._microApp) {      
            console.log('another micro app is running, unmount it first')  
            await window._microApp?.unmount();  
        }      
        window._microApp = null;    
        const app = {      
            name: appRoot,     
            entry: `//${location.host}/${appRoot}/?_=${Math.random()}`,   
            container: '#micro-app-container',   
            activeRule: TAG   
        };   
        if (!window._isMicroHash) {   
            // history模式     
            const baseUrl = location.pathname.split(TAG)[0] + TAG + '/' + appRoot; 
            const modifyRoutes = routes => routes;   
            app.props = { baseUrl, modifyRoutes, routerAdapter }  
        }   
        const config = { }    
        config.sendbox = { strictStyleIsolation: true, experimentalStyleIsolation: true }  
        // const sendbox = true  
        config.singular = true   
        config.fetch = (url) => { 
            if (url.includes('element-theme')) { 
                // 剔除微应用主题文件      
                url = `/${appRoot}/favicon.ico`;   
            }       
            // if (/element-plus-[0-9a-z]*.css/.test(url)) url = 'favicon.ico';  
            return window.fetch(url);   
        };   
        window._microApp = loadMicroApp(app, config);  
    }
}

microRouterAdapter.js,调整微应用路由跳转,此处调整history微应用跳至hash页面,使用主应用的router

import router from '../router/index'

export default function(microRouter) { 
    function adapter(fn) {   
        return async function(to) {   
            // 判断当前是否为微前端嵌入c场景
            let path;      
            if (typeof to === 'string') {  
                console.log(to)      
                path = to    
            } else if (!!to && to.path) {
                path = to.path || ''     
            }    
            if (path?.includes('#/micro-app')) {  
                router.push(path.replace('/user-management', ''))     
                return;   
            }      
            return await fn(to);   
        };
    } 
    microRouter.push = adapter(microRouter.push);
    microRouter.replace = adapter(microRouter.replace);  
    return microRouter; 
}

5.2.3、hash微应用

1、base处理

微应用不要设置base,或设为‘/’

new Router({   routes: constantRoutes }

2、main.js

如果router中设置了base,则需要将其设置与主应用当前路径一致,即#之前的要一样

/** ***************************微前端改造****************************************/ 
let instance = null
function render(props = {}) { 
    if (location.href.indexOf('layout=none') === -1 && isIE()) return;  
    const { container, modifyRoutes, baseUrl, routerAdapter } = props;  
    // router = new Router({   
    //  base: baseUrl,   
    //  routes: modifyRoutes(routes)
    // }) 
    // routerAdapter?.(router) 
    instance = new Vue({   
        router,   
        store,  
        render: (h) => h(App) 
    }).$mount(container ? container.querySelector('#app-wrap') : '#app-wrap')
}  
// 独立运行时 
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
}  

3、路由兼容

微应用原本的路径中没有/micro-app的,需要加入,并且在路由跳转时也要加入

export const constantRoutes = [ 
    {    
        path: '/:micro?/' + preName, 
        name: preName,    
        component: () => import('@/layout/white'),
    }
]

//......

function judgeRoutePath(fn) { 
    return function(payload, onComplete, onAbort) {  
        // 判断当前是否为微前端嵌入   
        if (location.href.includes('/micro-app')) { 
            if (typeof payload === 'string') {     
                // 绝对路径,且不以/micro-app开头     
                if (payload.startsWith('/') && !payload.startsWith('/micro-app')) {   
                    payload = '/micro-app' + payload     
                }   
            } else { 
                // 绝对路径,且不以/micro-app开头  
                if (payload && payload.path?.startsWith('/') && !payload.path.startsWith('/micro-app')){          
                    payload.path = '/micro-app' + payload.path       
                }   
            }  
        }    
        return fn.call(this, payload, onComplete, onAbort)  
    }
} 
router.push = judgeRoutePath(router.push)
router.replace = judgeRoutePath(router.replace) 

//......

5.2.4、history微应用

import { renderWithQiankun, qiankunWindow, type QiankunProps } from 'vite-plugin-qiankun/dist/helper';
import { createRouter, createWebHistory } from 'vue-router'; 

let app: App;
const render = (props: QiankunProps = {}) => { 
    const { container, modifyRoutes, baseUrl, routerAdapter } = props;  
    const el: string | Element = container?.querySelector('#app') || '#app'; 
    // 避免 id 重复导致微应用挂载失败  
    app = createApp(root);   
    let router;  
    if (baseUrl && modifyRoutes) {  
        router = createRouter({     
            history: createWebHistory(baseUrl),   
            routes: modifyRoutes(routes),   
        }); 
    } else {  
        router = createRouter({    
            history: createWebHistory(import.meta.env.VITE_BASE_URL),   
            routes: routes,  
        }); 
    } 
    routerAdapter?.(router);  
    app.use(router);   
    app.mount(el);
}; 
const initQianKun = () => { 
    renderWithQiankun({    
        bootstrap() {      
            console.log('微应用:bootstrap');  
        },   
        mount(props) {   
            // 获取主应用传入数据  
            console.log('微应用:mount', props);   
            render(props);   
        },  
        unmount(props) {  
            console.log('微应用:unmount', props);  
            app?.unmount();  
        },    
        update(props) {   
            console.log('微应用:update', props);   
        }, 
    });
};  
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render(); // 判断是否使用 qiankun ,保证项目可以独立运行

5.2.5、服务器代理

前端界面自动识别微应用路径,然后调用。例如:"https://domain/xxx"、"https://domain/yyy"

但是微应用与主应用很可能不在一个域名下,所以需要后台代理支持

5.3、局部嵌入某一页面

局部嵌入微应用某一页面,微应用配置与上述相同

主要问题是,使主应用当前路径,触发微应用对应页面,并且提供对应页面需要的参数

此处案例是主应用hash,微应用history,所以找到目标页面,并把它路径改成当前location的#之前一样就行

<template>  
    <div :id="id" class="micro-app-once-vue" />
</template> 
<script> 
import { loadMicroApp } from 'qiankun'
let _microApp 
export default {  
    props: {     
        entry: {   
            type: String,  
            required: true   
        },   
        path: { 
            type: String, 
            required: true   
        },    
        query: {  
            type: Object  
        }   
    },  
    data: () => ({ id: 'once-app-' + Math.floor(10000 * Math.random()) }),
    mounted() {   
        this.displayMicroApp() 
    },  
    beforeDestroy() {  
        _microApp?.unmount()  
        _microApp = null  
    }, 
    methods: {   
        async displayMicroApp() {   
            const { entry, path } = this 
            if (!path) return     
            // 将微应用页面需要参数放上去 
            const query = Object.assign({}, this.query, this.$route.query);  
            this.$router.replace({ path: this.$route.path, query })  
            await new Promise((r) => setTimeout(r, 500));  
            const pathname = location.pathname    
            // 主应用向微应用传递参数  
            const microAppProps = {    
                baseUrl: '/',        
                /**
                 * 修改子应用routes,修改为加载指定路由文件
                 * @param routes 
                 */      
                 modifyRoutes(routes) {    
                     // 只保留指定的路由,并改为根路径  
                     const recursion = (rs, pre) => {     
                         for (const rou of rs || []) {     
                             if (path === (pre + rou.path).replace('//', '/')) {   
                                 return Object.assign({}, rou, { path: pathname.replace('/', '') })     
                             } else if (rou.children?.length) {       
                                 const re = recursion(rou.children, pre + rou.path)    
                                 if (re) {             
                                     return Object.assign({}, rou, { children: [re] })  
                                 }          
                             }       
                         }       
                         return false    
                     }        
                     const result = recursion(routes, '') 
                     if (result) {      
                         console.log(result)     
                         return [result]        
                     } else {          
                         return []        
                     }   
                 }      
             }   
             console.log('display micro app[once]') 
             const appConfig = {      
                 name: 'flowApp', // 'micro-app-once', 子应用已指定名称,此处需与之保持一致     
                 entry: `${entry}?_=${Math.random()}`,  
                 container: '#' + this.id,    
                 props: microAppProps     
             }  
             // let conf = { sandbox: { experimentalStyleIsolation: true }, singular: true }; 
             const conf = { sandbox: false, singular: true }   
             const lifeCycles = {   
                 // 生命周期钩子函数    
                 // beforeLoad: (app) => {   
                 //   console.log('beforeLoad', app)   
                 // },      
                 async beforeMount(app) {  
                     console.log('beforeMount ', app)  
                 },     
                 async afterMount(app) {    
                     console.log('afterMount', app) 
                 },       
                 async beforeUnmount(app, global) {    
                     console.log('beforeUnmount ', app)   
                 }    
                 // afterUnmount: (app) => {   
                 //   console.log('afterUnmount', app)  
                 // }   
             }     
             _microApp = loadMicroApp(appConfig, conf, lifeCycles) 
         }  
     }
 }
 </script> 

主应用history,微应用history

<template>  
    <div id="micro-app-once"></div> 
</template>
<script lang="ts" setup> 
import { LoadableApp, loadMicroApp, MicroApp } from 'qiankun' 
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { MicroAppProps } from '@/typings/ImportTypes'
import { wait, changeElementTheme } from '@/utils/util'
import { useThemeStore } from '@/store/modules/theme'
import { ROUTE_BASE } from '@/config/setting'

const route = useRoute()
const props = defineProps({ 
    entry: {     type: String,     required: true   }, 
    path: {     type: String,     required: true   } 
})
const themeStore = useThemeStore()
let _microApp: MicroApp 

const displayMicroApp = async () => { 
    let { entry, path } = props  
    if (!path) return   
    await wait(100)   
    
    const changeTheme = function () { 
        // let doc = document.querySelector('#micro-app-once [data-qiankun]')  
        let doc = document.querySelector('html')  
        if (!doc) return    
        let { color, darkMode } = themeStore.$state   
        changeElementTheme(color, doc, darkMode) 
    } 
    watch(() => themeStore.$state.color, changeTheme) 
    watch(() => themeStore.$state.darkMode, changeTheme)  
    
    // 主应用向微应用传递参数 
    const microAppProps: MicroAppProps = {  
        baseUrl: ROUTE_BASE,  
        /**      
        * 修改子应用routes,修改为加载指定路由文件   
        * @param routes  
        */   
        modifyRoutes(routes) {     
            // 只保留指定的路由,并改为根路径    
            return routes
                .filter((r) => r.path.includes(path))
                .map((rt) => Object.assign({}, rt, { path: route.path })) 
        } 
    }   
    console.log('display micro app[once]') 
    let appConfig: LoadableApp<MicroAppProps> = {   
        name: 'flowApp', //'micro-app-once', 子应用已指定名称,此处需与之保持一致  
        entry: `${entry}?_=${Math.random()}`, 
        container: '#micro-app-once',   
        props: microAppProps
    } 
    // let conf = { sandbox: { experimentalStyleIsolation: true }, singular: true }; 
    let conf = { sandbox: false, singular: true }  
    let lifeCycles = {   
        // 生命周期钩子函数   
        // beforeLoad: (app) => {    
        //   console.log('beforeLoad', app) 
        // },   
        async beforeMount(app: any) {  
            console.log('beforeMount ', app) 
        },   
        async afterMount(app: any) {  
            console.log('afterMount', app) 
            changeTheme()    
        },    
        async beforeUnmount(app: any, global: Window) {  
            console.log('beforeUnmount ', app)  
        }  
        // afterUnmount: (app) => {  
        //   console.log('afterUnmount', app)    
        // }  
    }  
    _microApp = loadMicroApp(appConfig, conf, lifeCycles)
} 
onMounted(displayMicroApp)
onBeforeUnmount(() => _microApp?.unmount())
</script>

5.4、原生html微应用

原生html作为微应用,面临的问题是qiankun微应用需要入口文件抛出对应的生命周期函数

如果微应用可以通过源码修改,则没有问题;当微应用不能修改时,可以fetch自定义修改

<template>
  <div :id="id" class="micro-app" />
</template>
<script>
import { loadMicroApp } from 'qiankun';
import URL from 'url'

let contextPath = '/workflow-app'
if (location.href.includes('/portal-')) {
  contextPath = '/flow-app'
}

let entryJs = `<script>(function (global) { if (!Promise) return;global['purehtml'] = {bootstrap: function () {console.log('purehtml bootstrap'); return Promise.resolve(); }, mount: function (props) { console.log('purehtml mount'); if (layer) { layer.msg = props.message.info }props.window.downWorkFlowAttFile=function(id){downWorkFlowAttFile(id)}; return Promise.resolve(); }, unmount: function () { console.log('purehtml unmount'); return Promise.resolve(); }, }; })(window);</`
entryJs += 'script>'

function modifyUrl(url) {
  url = URL.parse(url)
  url.host = location.host
  url.pathname = contextPath + url.pathname
  return URL.format(url)
}
async function fetch(url) {
  if (url.includes('element-theme')) url = 'favicon.ico';
  if (url.includes('//workflow.')) {
    url = modifyUrl(url)
  }
  // include, same-origin, omit
  const options = { credentials: 'same-origin' }
  if (url.includes('//member.')) {
    // 该域名对静态资源做了跨越限制,去除same-origin改为no-cors
    options.mode = 'no-cors'
  }
  let res = await window.fetch(url, options);
  if (url.includes('RenderWorkFlowStepProgressBody')) {
    // 入口entry.js特殊处理

    let content = await res.text()
    // 增加entry
    content = content.replace('</html>', entryJs + '</html>')
    // 替换其中跨域链接
    content = content.replaceAll(/\/\/workflow\.(uk|u|you)zhicai.com/g, contextPath)
    // 去掉jsonp
    content = content.replaceAll('dataType: "jsonp",', '')
    // 去除jsonp之后处理返回数据
    content = content.replaceAll('success: function (d) {', 'success: function (d) {d=eval(d);')
    // 适配onclick时找不到该方法
    content = content.replace('function downWorkFlowAttFile', 'window.downWorkFlowAttFile=function')

    content = new Blob([content], { type: 'text/plain' });
    res = new Response(content, { headers: res.headers })
  }
  return res
}
export default {
  props: {
    workFlowType: {
      type: [String, Number],
      required: true
    },
    projectId: {
      type: String,
    },
    businessId: {
      type: String,
    },
    url: {
      type: String,
      required: true
    }
  },
  data() {
    const id = 'workflow-app' + Math.floor(10000 * Math.random());
    return {
      id,
    };
  },
  mounted() {
    this.displayFlowApp();
  },
  beforeDestroy() {
    this._microApp?.unmount();
  },
  methods: {
    async displayFlowApp() {
      if (!this.url) return;

      const dom = document.querySelector('#' + this.id);
      if (!dom) return;

      await new Promise((r) => setTimeout(r, 300));

      console.log('display flow app');
      const appConfig = {
        name: this.id,
        entry: this.url,
        container: '#' + this.id,
        props: { message: this.$message, window },
      };
      const conf = {
        // singular: true,
        fetch,
        sandbox: { experimentalStyleIsolation: true },
        excludeAssetFilter(assetUrl) {
          return !assetUrl.includes('//workflow.');
        },
      };

      this._microApp = loadMicroApp(appConfig, conf);
      setTimeout(() => {
        this.$el.querySelectorAll('button').forEach((button) => {
          button.addEventListener('click', function(e) {
            e.stopPropagation();
          });
        });
      }, 10000);
    },
  },
};
</script>

5.5、element-plus微应用

如果主微应用是使用新版对应vue3的ui库,其实不需要考虑这个问题;

但是如果是主应用element-ui,微应用element-plus,就会存在样式污染的问题,而element-plus推荐按需加载,一旦使用按需加载样式文件分散,无法使用上述的通过剔除主题文件达到效果。

建议自定义命名空间。

首先删除工程中所有主动引用element-plus组件和样式的地方,全部通过按需加载(message、messagebox、loading等服务方式调用的不算)

参考element-plus官网

// styles/element/index.scss
// we can add this to custom namespace, default is 'el' 
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with (  
    $namespace: 'ewf' // 此处指定命名空间,样式文件中已“ewf”替换“el”
);
// @use "element-plus/theme-chalk/src/index.scss" as *;
// 由于element-plus样式按需引入,js引入组件的样式需要独立加载
@import 'element-plus/theme-chalk/src/loading.scss'; 
@import 'element-plus/theme-chalk/src/message.scss'; 
@import 'element-plus/theme-chalk/src/message-box.scss';
<template>  
    <el-config-provider namespace="ewf"> // 同上  
        <el-container>       
            <div id="app">       
                <router-view v-slot="{ Component }">   
                    <transition>            
                        <component :is="Component" />          
                    </transition>     
                </router-view>     
            </div>   
        </el-container> 
    </el-config-provider> 
</template>
//elementui 按需导入 
import AutoImport from 'unplugin-auto-import/vite';
import Components from 'unplugin-vue-components/vite'; 
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';  

    plugins: [      
        AutoImport({ resolvers: [ElementPlusResolver()] }),   
        // Components({ resolvers: [ElementPlusResolver()] }),  
        // use unplugin-vue-components   
        Components({       
            resolvers: [     
                ElementPlusResolver({    
                    importStyle: "sass",     
                    // directives: true,     
                    // version: "2.1.5",     
                }),      
            ],    
        }),   
    ],     
    css: {  
        preprocessorOptions: {    
            scss: {          
                additionalData: `@use "@/assets/style/element-index.scss" as *;`,     
            },     
        },  
    },

6、存在问题

使用qiankun微前端解决方案中,我发现一个无法调和的问题

1、vue-router,不论在那个版本中都没有提供主动卸载的接口;

2、且vue官方的指导方案中router/index.js导出的是一个Router实例,工程中很多独立的js文件都是通过引用这个实例来router.push

3、但是在上述很多功能,是通过主应用自定义微应用的路由实现,则微应用main中可能重复实例化Router

4、而且在多个微应用切换时,会存在微应用vue-router未销毁问题,影响页面功能

基于上面几点,微应用应

  1. 尽量不要设置base或设置“/”
  2. 尽量不要直接引用router/index.js
  3. 如果可以,router/index.js仅抛出routes配置,实例化在main.js的render方法中做

或者,微应用工程中做是否在微应用情况的判断,特殊处理

7、应用案例