利刃出击-MicroApp一探究竟

本文比较了Qiankun与京东自研的MicroApp微前端框架,MicroApp以其低接入成本、更好的性能和组件化设计脱颖而出,介绍了MicroApp的架构、项目构建和数据通信机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在使用过程中我发现 qiankun 还是有一些不爽的地方的:

1.项目的侵入性依然很强

2.qiankun 在沙箱方面依然有不少坑。

3.对vite的支持很不友好

4.路由及子应用加载问题很多。(配置复杂不易理解,加载慢......)

可能就是使用 qiankun的这些难受,迫使京东的小伙伴在去年 7 月推出了自己微前端解决方案 —— MicroApp。今天我们就来看看咱们MicroApp是如何实现微前端方案的。

在讲解之前我们先来做个对比:

对比single-spaqiankunMicro App
框架体积20kb94kb30kb
渲染原理基于路由监听路由监听+手动渲染CustomElement
数据通信机制x基于props属性传递基于发布订阅+CustomEvent
接入成本
多框架兼容x
js沙箱x
样式隔离x
shadowDomx
预加载x
静态资源地址补全xx
元素隔离xx
插件系统xx

相比之下,MicroApp显然更高一筹。

MicroApp 一上来就表明了自己的立场:

micro-app 并没有沿袭 single-spa 的思路 。

MicroApp是一款基于类WebComponent进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。它是目前市面上接入微前端成本最低的框架,并且提供了JS沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。MicroApp与技术栈无关,也不和业务绑定,可以用于任何前端框架和业务。

有下面的优势:

  • 使用简单。 将功能封装到 WebComponent 中

  • 零依赖。 无依赖、更高的扩展性

  • 兼容所有框架 技术栈无关

MicroApp 的核心功能在CustomElement基础上进行构建,CustomElement用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

我们来简单看一下MicroApp的架构:

 

我们一起来看看咱们的项目构建过程:

基座应用主要基于森钛的xxv脚手架

安装依赖

npm i @micro-zoe/micro-app --save

路由router.js:

{

        // 因为主应用为history路由,appname-vite子应用是hash路由,这里配置略微不同

        // 已解决带参数时页面丢失的问题

        path: '/app-vite:page*',

        name: 'vite',

        component: () => import('@/views/vite.vue')

    },

vite路由页面:

<template>
    <div>
        <micro-app
            name="appname-vite"
            :url="url"
            inline
            disablesandbox
            :data="microAppData"
            @created="handleCreate"
            @beforemount="handleBeforeMount"
            @mounted="handleMount"
            @unmount="handleUnmount"
            @error="handleError"
            @datachange="handleDataChange"
        ></micro-app>
    </div>
</template>

<script lang="ts">
import { EventCenterForMicroApp } from '@micro-zoe/micro-app';
import config from '../config';

// @ts-ignore 因为vite子应用关闭了沙箱,我们需要为子应用appname-vite创建EventCenterForMicroApp对象来实现数据通信
window.eventCenterForAppNameVite = new EventCenterForMicroApp('appname-vite');

export default {
    name: 'vite',
    data () {
        return {
            url: `${config.vite}/child/vite/`,
            microAppData: { msg: '来自基座的数据' }
        };
    },
    methods: {
        handleCreate (): void {
            console.log('child-vite 创建了');
        },

        handleBeforeMount (): void {
            console.log('child-vite 即将被渲染');
        },

        handleMount (): void {
            console.log('child-vite 已经渲染完成');

            setTimeout(() => {
                // @ts-ignore
                this.microAppData = { msg: '来自基座的新数据' };
            }, 2000);
        },

        handleUnmount (): void {
            console.log('child-vite 卸载了');
        },

        handleError (): void {
            console.log('child-vite 加载出错了');
        },

        handleDataChange (e: CustomEvent): void {
            console.log('来自子应用 child-vite 的数据:', e.detail.data);
        }
    }
};
</script>

main.ts中:

import microApp from '@micro-zoe/micro-app';
 microApp.start({
        plugins: {
            modules: {
             //咱们的子应用是vite项目,在这配置
                'appname-vite': [
                    {
                        loader(code: string) {
                            if (process.env.NODE_ENV === 'development') {
                                // 这里 /basename/ 需要和子应用vite.config.js中base的配置保持一致
                                code = code.replace(/(from|import)(\s*['"])(\/child\/vite\/)/g, all => {
                                    return all.replace('/child/vite/', 'http://localhost:4007/child/vite/');
                                });
                            }

                            return code;
                        }
                    }
                ]
            }
        }
    });

vite.config中:

import { defineConfig,searchForWorkspaceRoot } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path'; // 编辑器提示 path 模块找不到,可以yarn add @types/node --dev
import stylelintPlugin from 'vite-plugin-stylelint';

export default defineConfig({
    plugins: [vue({
        template: {
            compilerOptions: {
                isCustomElement: tag => /^micro-app/.test(tag)
            }
        }
    }),
        stylelintPlugin({
            include: ['src/**/*.css', 'src/**/*.less', 'src/**/*.scss', 'src/**/*.sass', 'src/**/*.vue'],
            cache: false
        })
    ],
    resolve: {
        alias: {
            '@': resolve(__dirname, 'src') // 设置 `@` 指向 `src` 目录
        }
    },
    server: {
        port: 3000, // 设置服务启动端口号
        open: true ,// 设置服务启动时是否自动打开浏览器
        fs: {
            allow: [
                searchForWorkspaceRoot(process.cwd()),
                '/'
            ]
        }
    },
    base: '/main/',
    build: {
        outDir: 'main'
    }
});

以上就是主应用的改造,如果不需要路由,子应用只作为一个组件的话,和引入一个vue组件的方式相似。

 

接下来我们看看子应用如何改造:

子应用的main.ts中

 import { createApp, App as AppInstance } from 'vue'
import { createRouter, createWebHashHistory, RouterHistory, Router } from 'vue-router'
import App from './App.vue'
import routes from './router'

declare global {
  interface Window {
    eventCenterForAppNameVite: any
    __MICRO_APP_NAME__: string
    __MICRO_APP_ENVIRONMENT__: string
    __MICRO_APP_BASE_APPLICATION__: string
  }
}

// 与基座进行数据交互
function handleMicroData (router: Router) {
  // eventCenterForAppNameVite 是基座添加到window的数据通信对象
  if (window.eventCenterForAppNameVite) {
    // 主动获取基座下发的数据
    console.log('child-vite getData:', window.eventCenterForAppNameVite.getData())

    // 监听基座下发的数据变化
    window.eventCenterForAppNameVite.addDataListener((data: Record<string, unknown>) => {
      console.log('child-vite addDataListener:', data)

      if (data.path && typeof data.path === 'string') {
        data.path = data.path.replace(/^#/, '')
        // 当基座下发path时进行跳转
        if (data.path && data.path !== router.currentRoute.value.path) {
          router.push(data.path as string)
        }
      }
    })

    // 向基座发送数据
    setTimeout(() => {
      window.eventCenterForAppNameVite.dispatch({ myname: 'child-vite' })
    }, 3000)
  }
}

/**
 * 用于解决主应用和子应用都是vue-router4时相互冲突,导致点击浏览器返回按钮,路由错误的问题。
 * 相关issue:https://github.com/micro-zoe/micro-app/issues/155
 * 当前vue-router版本:4.0.12
 */
 function fixBugForVueRouter4 (router: Router) {
  // 判断主应用是main-vue3或main-vite,因为这这两个主应用是 vue-router4
  if (window.location.href.includes('/main-vue3') || window.location.href.includes('/main-vite')) {
    /**
     * 重要说明:
     * 1、这里主应用下发的基础路由为:`/main-xxx/app-vite`,其中 `/main-xxx` 是主应用的基础路由,需要去掉,我们只取`/app-vite`,不同项目根据实际情况调整
     *
     * 2、因为vite关闭了沙箱,又是hash路由,我们这里写死realBaseRoute为:/app-vite#
     */
     const realBaseRoute = '/app-vite#'

     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 || '')
       }
     })
  }
}


// ----------分割线---umd模式------两种模式任选其一-------------- //
let app: AppInstance | null = null
let router: Router | null = null
let history: RouterHistory | null = null
// 将渲染操作放入 mount 函数
function mount () {
  history = createWebHashHistory()
  router = createRouter({
    history,
    routes,
  })

  app = createApp(App)
  app.use(router)
  app.mount('#vite-app')

  console.log('微应用child-vite渲染了')

  handleMicroData(router)

  // fixBugForVueRouter4(router)
}

// 将卸载操作放入 unmount 函数
function unmount () {
  app?.unmount()
  history?.destroy()
  // 卸载所有数据监听函数
  window.eventCenterForAppNameVite?.clearDataListener()
  app = null
  router = null
  history = null
  console.log('微应用child-vite卸载了')
}

// 微前端环境下,注册mount和unmount方法
if (window.__MICRO_APP_BASE_APPLICATION__) {
  // @ts-ignore
  window['micro-app-appname-vite'] = { mount, unmount }
} else {
  // 非微前端环境直接渲染
  mount()
}

vite.config.ts 

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { join } from 'path'
import { writeFileSync } from 'fs'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    (function () {
      let basePath = ''
      return {
        name: "vite:micro-app",
        apply: 'build',
        configResolved(config) {
          basePath = `${config.base}${config.build.assetsDir}/`
        },
        // writeBundle 钩子可以拿到完整处理后的文件,但已经无法修改
        writeBundle (options, bundle) {
          for (const chunkName in bundle) {
            if (Object.prototype.hasOwnProperty.call(bundle, chunkName)) {
              const chunk = bundle[chunkName]
              if (chunk.fileName && chunk.fileName.endsWith('.js')) {
                chunk.code = chunk.code.replace(/(from|import\()(\s*['"])(\.\.?\/)/g, (all, $1, $2, $3) => {
                  return all.replace($3, new URL($3, basePath))
                })
                const fullPath = join(options.dir, chunk.fileName)
                writeFileSync(fullPath, chunk.code)
              }
            }
          }
        },
      }
    })() as any,
  ],
  server: {
    port: 4007,
  },
  base: `${process.env.NODE_ENV === 'production' ? 'http://www.micro-zoe.com' : ''}/child/vite/`,
  build: {
    outDir: 'vite',
  },
})
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋名山大前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值