micro-app微前端使用,接入vite应用以及发布完整项目体验

5,177 阅读10分钟

项目选型

微前端有qiankun,micro-app等,为什么选中了micro-app?

两个框架都使用了微前端概念,但是micro-app对原有的项目入侵最少,极大减少修改原项目;qiankun是需要修改原项目的,毕竟作为子应用的项目都运行很久了;为了减少开发成本,减少代码选择京东开源的micro-app

micro-app介绍

官网的介绍可以自己去看,这里不就不做重复了 直接上教程

image.png

项目中使用

你可以使用任何一项构建工具,当做基座使用

目录如下

image.png

基座版本如下:

"vue":"^3.2.37"

"@micro-zoe/micro-app": "^0.8.10"

"vite": "^3.0.7"

"vue-router": "^4.1.5"

基座配置

1 第一步在main.js中引入micro-app,并且执行micro.start函数;开启微前端模式
import microApp from '@micro-zoe/micro-app'

microApp.start()

image.png

2 第二步 在相应的文件中使用micro-app的标签 (这里以vue3的子应用举例)
<micro-app baseroute='/vue3' name='vue3' url='http://localhost:3003/'></micro-app>

image.png

micro-app标签中

name(必传且唯一):应用名称,类似key的作用

url(必传且唯一):应用地址,会被自动补全为http://localhost:3000/index.html ,子应用的地址

baseroute(可选):基座应用分配给子应用的基础路由,就是上面的 /vu3,用于区分子应用的path,也可以不选

如果关闭了沙箱机制 baseroute失效,且样式隔离失效

子应用配置

3 第三步在vue3子应用中设置

vue3子应用为vue-cli 创建的项目,如是vite或者react创建的项目需要单独配置

  • 3.1 在vue.config.js中添加云讯跨域

    devServer: { headers: { 'Access-Control-Allow-Origin': '*', } }
    
  • 3.2 如果基座是histroy路由.子应用是hash路由此步骤跳过

    const index = createRouter({
       // 👇 __MICRO_APP_BASE_ROUTE__ 为micro-app传入的基础路由
       history: createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL),
       routes
    })
    
  • 3.3 设置 publicPath 如果子应用不是webpack构建的,这一步可以省略。

步骤1: 在子应用src目录下创建名称为public-path.js的文件,并添加如下内

     // __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__ } 
     

步骤2: 在子应用入口文件的最顶部引入public-path.js

         import './public-path'
  • 3.4 在main.js中监听卸载

       //1 监听卸载操作
       window.addEventListener('unmount', function () {
          createApp(App).unmount()
       })
      // 2 如果子应用频繁卸载可以使用umdm模式  两者取其一
       // main.js 
       import { createApp } from 'vue' 
       import * as VueRouter from 'vue-router'
       import routes from './router' 
       import App from './App.vue'
       let app = null
       let router = null 
       let history = null
       // 👇 将渲染操作放入 mount 函数 -- 必填 
       function mount () { 
           history = VueRouter.createWebHistory(window.__MICRO_APP_BASE_ROUTE__ || '/')
           router = VueRouter.createRouter({
                history,
                routes, 
           })
           app = createApp(App)
           app.use(router) 
           app.mount('#app')
       }
       // 👇 将卸载操作放入 unmount 函数 -- 必填 
       function unmount () { 
           app.unmount() 
           history.destroy() 
           app = null 
           router = null 
           history = null 
       }
       // 微前端环境下,注册mount和unmount方法 
       if (window.__MICRO_APP_ENVIRONMENT__) {
           window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } 
       } else {
           // 非微前端环境直接渲染
           mount() 
       }
       
    

以上就是vue3作为子应用的配置

分别启动基座项目与子应用的项目后点击vue3应用

image.png

image.png

vite子应用使用需要单独处理 官网地址

由于vite作为子应用的构建方式与webpack不同需要单独去适配,下面是使用方法

基座处理
   <micro-app name='child-name' 
   url='http://localhost:3001/basename/' 
   inline // 使用内联script模式 
   disableSandbox // 关闭沙箱
   />

这里关闭沙箱机制,所以baseUrl失效,样式隔离也失效了,基座的数据传输需要单独去处理;如果项目中接入了vite子应用需要考虑怎么处理样式隔离;考虑没有了沙箱机制出现的问题该如何去解决(我也不知道会出现什么问题,官网的建议是不接入等,1.0版本发布;) 我做的项目是vite基座+vite子应用,下面我会详细的介绍我是都碰到了什么问题,以及怎么解决的;ps:我做的项目以上线.

image.png

基座是vite应用的话需要再处理main.js 如果不是 忽略此步骤;
   此处是处理vite引用开发环境中
import microApp from '@micro-zoe/micro-app'

microApp.start({
plugins: {
 modules: {
   // appName即应用的name值
   appName: [{
     loader(code) {
       if (process.env.NODE_ENV === 'development') {
         // 这里 basename 需要和子应用vite.config.js中base的配置保持一致 
         // 这里处理的是开发环境下匹配到from 或者import 引入的文件以子应用域名/basename去替换 
         code = code.replace(/(from|import)(\s*['"])(/basename/)/g, all => {
           return all.replace('/basename/', '子应用域名/basename/')
         })
       }

       return code
     }
   }]
 }
}
})

image.png

image.png

vite子应用处理 官网地址

按照官网的配置处理,下面是我的配置

  1. 修改容器id

image.png 2. 修改route为hash路由

image.png

  1. 图片处理

image.png 4. vite.config.js处理(这里后面会讲到处理了什么,具体在部署那块)

image.png

到此都已经处理完成. 因为项目中没用到Angular,next.js 这里就不讲了,具体可以看官网;

image.png

路由处理 官网地址

注意以下几点:

路由配置:

基座是hash路由,子应用也必须是hash路由

基座是history路由,子应用可以是hash或history路由

baserouter 是基座分发给子应用的路径,子应用可以从window.__MICRO_APP_BASE_ROUTE__上获取baseroute的值,用于设置基础路由(此处是用于标识子应用的路由)

注意此处如果基座是history路由,且子应用也是history路由,并且设置了baseroute, 那么当前子应用页面的路由path需要与baseroute 一致; 因为子应用是根据游览器上的地址渲染对应的页面

image.png

子应用不会根据micro-app的url属性渲染对应的页面,而是根据浏览器地址渲染对应的页面

此句话可以理解为 子应用只会加载当前index.html的地址 例: 子应用地址为:http://localhost:3003

如果我们想渲染的地址为http://localhost:3003/page

那么 我们正确的写法应该是 而不是url='http://localhost:3003/page'

<micro-app name='vue3' url='http://localhost:3003/' baseroute='/vue3'></micro-app>

那么 我们如何跳转到这个页面呢? 基座中跳转的参数为:

  router.push('/page')
跳转处理

场景一:基座 → 子应用

基座直接使用 router.push 即可,路径会自动匹配到对应子应用的路由:

// 基座中跳转到子应用的 /page 路由
// 前提:子应用的 baseroute 为 '/vue3',且子应用内部定义了 /page 路由
router.push('/vue3/page')

场景二:子应用内部跳转

子应用内部直接用自身的 router 跳转即可,和在独立应用中一样:

// 子应用内部
this.$router.push('/page')
// 或
router.push('/page')

场景三:子应用 → 基座(跨应用跳转)

子应用需要跳转到另一个子应用或基座页面时,有两种方式:

// 方式1:通过 window 直接操作浏览器地址
window.location.href = '/other-app/page'

// 方式2:通过 microApp.router(推荐,支持 history/hash 模式统一处理)
// 需要在子应用中通过 window.microApp 访问
// 注意:关闭沙箱时 window.microApp 不可用,见下文"关闭沙箱的通信"

场景四:基座通过 microApp.router 控制子应用跳转

micro-app 提供了虚拟路由 API,基座可以编程式控制子应用的跳转:

import microApp from '@micro-zoe/micro-app'

// 基座中控制指定子应用跳转
microApp.router.push({
  name: 'vue3',           // 子应用 name
  path: '/page',          // 目标路径
})

// 也支持 replace
microApp.router.replace({
  name: 'vue3',
  path: '/detail',
})

场景五:keep-alive 模式下的路由缓存

如果子应用开启了 keep-alive,需要注意页面切换时缓存的清理:

<micro-app name='vue3' url='http://localhost:3003/' keep-alive></micro-app>

启用 keep-alive 后,子应用卸载时不会销毁,再次切换回来时会恢复之前的状态。适合列表→详情→返回列表保留滚动位置的场景。

1.0版本修改了什么

micro-app 1.0 是一次大版本升级,主要变化:

1. 虚拟路由系统重构

1.0 版本重新设计了虚拟路由,解决了以下历史问题:

  • 子应用 history 模式路由的兼容性大幅提升
  • 不再强制要求基座 history 对应子应用 hash 的限制
  • 路由跳转的匹配逻辑更加准确

2. Vite 子应用沙箱支持(重大更新)

这是 1.0 最受期待的功能 — Vite 子应用不再需要关闭沙箱

  • 支持 Vite 子应用在沙箱内运行
  • 样式隔离(scoped css)对 Vite 子应用生效
  • baseroute 在 Vite 子应用中恢复正常功能

如果你是 0.x 版本且 Vite 子应用关了沙箱,升级 1.0 后可以重新开启:

<!-- 升级前(0.x):被迫关闭沙箱 -->
<micro-app name='child' url='http://localhost:3001/' inline disableSandbox />

<!-- 升级后(1.0+):可以开启沙箱,样式隔离与 baseroute 均正常 -->
<micro-app name='child' url='http://localhost:3001/' baseroute='/child' />

3. iframe 沙箱模式

新增 iframe 属性,可以用 iframe 作为沙箱载体,替代默认的 with 沙箱,隔离性更强:

<micro-app name='child' url='http://localhost:3001/' iframe />

4. fiber 模式(实验性)

引入了 fiber 渲染模式,用于优化大型子应用的首次加载体验,按需注入资源。

5. 数据通信 API 增强

  • forceSetData / forceDispatch 支持强制推送相同数据
  • clearData / clearDataListener 更细粒度的清理控制

升级建议: 如果你的项目是 0.8.x 且已经上线稳定运行,不需要立刻升级。新项目可以直接上 1.0。老项目如果要升,重点关注 Vite 子应用沙箱这块的回归测试。

数据传输

子应用获取基座数据

基座通过 microApp.setData 向子应用推送数据:

// 基座
import microApp from '@micro-zoe/micro-app'

// 向名为 'vue3' 的子应用发送数据
microApp.setData('vue3', {
  token: 'xxx',
  userInfo: { name: '张三', role: 'admin' },
  theme: 'dark'
})

子应用监听并接收:

// 子应用
if (window.__MICRO_APP_ENVIRONMENT__) {
  // autoTrigger: true — 监听时立即用基座上次发送的数据触发一次回调
  window.microApp.addDataListener((data) => {
    console.log('收到基座数据:', data)
    // data => { token: 'xxx', userInfo: {...}, theme: 'dark' }
    // 在这里把数据存储到 store 或做后续处理
  }, true)
}

注意: 子应用挂载后才监听,所以要设置 autoTrigger: true 确保拿到基座提前发送的数据。

子应用向基座发送数据通信
// 子应用
window.microApp.dispatch({
  type: 'NAV_CHANGE',
  path: '/vue3/detail',
  params: { id: 123 }
})

// 如果数据对象引用没变但需要强制推送
window.microApp.forceDispatch({
  timestamp: Date.now()
})

基座接收:

// 基座
microApp.addDataListener('vue3', (data) => {
  console.log('收到子应用数据:', data)
  // data => { type: 'NAV_CHANGE', path: '/vue3/detail', params: { id: 123 } }
}, true)
基座获取子应用数据

有两种方式:

// 方式1:被动监听(推荐)
microApp.addDataListener('vue3', (data) => {
  // 子应用每次 dispatch 都会触发
  console.log('实时数据:', data)
}, true)

// 方式2:主动获取缓存数据
const cachedData = microApp.getData('vue3')
console.log('缓存数据:', cachedData)

getData 拿的是最后一次子应用 dispatch 上来或基座 setData 下去的数据,不是实时拉取,所以推荐用监听方式。

基座向子应用发送数据

同理,基座也可以用 setData + addDataListener 的组合方式:

// 基座发送
microApp.setData('vue3', { locale: 'zh-CN' })

// 基座也能主动读取自己发过的数据(第二个参数 true 表示读取基座→子应用的数据)
const sent = microApp.getData('vue3', true)
全局通信

全局通信用于基座和所有子应用之间广播消息,适合主题切换、语言切换、登出通知等场景:

// 基座 — 全局广播
microApp.setGlobalData({ theme: 'dark', lang: 'en' })

// 基座 — 监听全局数据
microApp.addGlobalDataListener((data) => {
  console.log('全局数据变更:', data)
})

// 子应用 — 监听全局数据
window.microApp.addGlobalDataListener((data) => {
  if (data.theme === 'dark') {
    document.body.classList.add('dark')
  }
}, true)

// 子应用 — 也可以发送全局数据
window.microApp.setGlobalData({ userLogout: true })

// 获取当前全局数据快照
microApp.getGlobalData()

// 清理
microApp.clearGlobalData()
microApp.clearGlobalDataListener()
关闭沙箱的通信

关闭沙箱时 window.microApp 不可用,因为 window.microApp 是注入在沙箱中的对象。此时需要换一种方式通信:

方案一:用 CustomEvent(推荐)

// 子应用 — 发送
window.dispatchEvent(new CustomEvent('child-to-base', {
  detail: { type: 'ready', data: '子应用已挂载' }
}))

// 基座 — 接收
window.addEventListener('child-to-base', (e) => {
  console.log('收到子应用数据:', e.detail)
})

// 基座 — 发送给子应用
window.dispatchEvent(new CustomEvent('base-to-child', {
  detail: { token: 'xxx' }
}))

// 子应用 — 接收
window.addEventListener('base-to-child', (e) => {
  console.log('收到基座数据:', e.detail)
})

方案二:用基座全局变量

// 基座 main.js
window.__BASE_STORE__ = reactive({
  token: '',
  userInfo: null,
  theme: 'light'
})

// 子应用中直接读写
const store = window.__BASE_STORE__
console.log(store.token)
store.theme = 'dark'

方案三:micro-app 1.0 开启沙箱后直接用标准 API

升级 1.0 后 Vite 子应用也能用沙箱了,所以 window.microApp 恢复正常,直接用标准通信 API 即可,不需要上面两种 workaround。

打包处理

基座打包

基座和普通 Vite 项目一样,执行 npm run build 就行。注意几点:

  1. 路由模式:如果用 history 模式,Nginx 要配 try_files
  2. 子应用地址:打包后 <micro-app> 的 url 要指向子应用线上地址,开发环境用环境变量区分:
<micro-app 
  name='vue3' 
  :url='subAppUrl' 
  baseroute='/vue3'
/>
// 基座中动态设置子应用地址
const isDev = import.meta.env.DEV
const subAppUrl = isDev 
  ? 'http://localhost:3003/' 
  : 'https://子应用线上域名/'
子应用打包

webpack 子应用打包无特殊处理。

Vite 子应用打包重点:

  1. vite.config.jsbase 配置需要支持微前端环境:
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  base: '/',
  // 如果在微前端环境中,需要动态 base,这里生产环境写死线上路径
  // 或者用环境变量控制
  build: {
    // 确保资源路径正确
    assetsDir: 'assets',
  },
  server: {
    port: 3003,
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
  },
})
  1. 图片等静态资源路径问题

Vite 子应用中,new URL('./img.png', import.meta.url) 这种写法在微前端环境中可能 404。建议:

// 用绝对路径引用 public 下的资源
'./img.png''/img.png'

// 或者动态拼接
const imgUrl = window.__MICRO_APP_ENVIRONMENT__ 
  ? window.__MICRO_APP_PUBLIC_PATH__ + 'img.png'
  : '/img.png'

关闭沙箱机制遇到的问题

由于项目是 Vite 基座 + Vite 子应用 的架构,micro-app 0.8.x 必须关沙箱(disableSandbox + inline)。关沙箱后踩了以下几个坑:

1. 全局变量污染

没沙箱隔离,子应用的 window.xxx 会和基座以及其他子应用共享。比如:

// 子应用 A 中定义
window.version = 'app-a-v1'

// 子应用 B 中覆盖
window.version = 'app-b-v2'

// 基座读到的是后加载的那个值

解决: 给全局变量加命名空间前缀:

// 子应用 A
window.__APP_A_VERSION__ = 'v1'

// 子应用 B  
window.__APP_B_VERSION__ = 'v2'
2. CSS 样式冲突

关闭沙箱后 disable-scopecss 自动生效,子应用的 CSS 会直接注入基座 DOM,产生样式覆盖。

解决:

子应用各自使用 CSS 命名空间或 CSS Modules:

// vite.config.js 中配置 CSS Modules 前缀
export default defineConfig({
  css: {
    modules: {
      // 给每个子应用不同的前缀
      generateScopedName: '[name]__[local]___[hash:base64:5]',
    },
  },
})

或者统一用 Tailwind CSS prefix 选项:

// tailwind.config.js
module.exports = {
  prefix: 'app-b-',  // 子应用 B 的所有类名前加 app-b-
}
3. 事件监听未清理

关沙箱后 unmont 时 removeEventListener 不会自动执行,导致内存泄漏。

解决: 子应用中所有 window.addEventListener 必须在 unmont 时手动清理:

function mount() {
  window.addEventListener('resize', handleResize)
  window.addEventListener('custom-event', handleCustom)
}

function unmount() {
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('custom-event', handleCustom)
  app.unmount()
  app = null
}

if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  mount()
}
4. baseroute 失效

关沙箱后 baseroute 不生效,子应用路由需要自己处理基准路径:

// 子应用 router
const router = createRouter({
  // 关沙箱时 __MICRO_APP_BASE_ROUTE__ 为 undefined
  // 手动指定 '/vue3' 或用 hash 路由规避
  history: createWebHashHistory(),
  routes,
})

建议:关闭沙箱时用 hash 路由,这样基座 history + 子应用 hash 不会冲突。

部署上线问题

1. Nginx 配置

基座和所有子应用都需要配置跨域:

# 基座
server {
  listen 80;
  server_name base.your-domain.com;

  location / {
    root /app/base/dist;
    # history 模式需要 try_files
    try_files $uri $uri/ /index.html;

    # 允许子应用请求
    add_header Access-Control-Allow-Origin *;
  }
}

# 子应用
server {
  listen 80;
  server_name child.your-domain.com;

  location / {
    root /app/child/dist;
    try_files $uri $uri/ /index.html;

    # 跨域 — 必须配置
    add_header Access-Control-Allow-Origin *;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
  }

  # 处理 OPTIONS 预检请求
  if ($request_method = 'OPTIONS') {
    return 204;
  }
}
2. 资源 404 问题

部署后 Vite 子应用的 chunk 文件读取失败常见原因:

  • base 路径不对:子应用 vite.config.js 的 base 如果写死了 /,但实际部署在子路径下,资源会 404。
// vite.config.js 修正
export default defineConfig(({ command }) => ({
  // 线上路径根据实际部署位置填写
  base: command === 'build' ? 'https://child.your-domain.com/' : '/',
}))
  • publicPath 注入:micro-app 会自动注入 __MICRO_APP_PUBLIC_PATH__,但 Vite 开发模式下不走这个变量,所以要区分环境。
3. 子应用独立运行 + 嵌入运行双模式

上线后子应用可能需要独立访问(调试、灰度),也要被基座嵌入。需求——同一份构建产物支持两种模式:

// 子应用 main.js
let app = null

function mount() {
  app = createApp(App)
  app.mount('#app')
  // ...初始化
}

function unmount() {
  app?.unmount()
  app = null
}

if (window.__MICRO_APP_ENVIRONMENT__) {
  // 微前端模式:挂载/卸载由基座控制
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  // 独立运行模式:直接挂载
  mount()
}
4. 环境变量区分

部署时子应用的 API 地址需要正确指向:

// 子应用中判断环境
const getBaseURL = () => {
  // 微前端环境 — 和基座共享域名,用相对路径
  if (window.__MICRO_APP_ENVIRONMENT__) {
    return '/api'
  }
  // 独立部署 — 用子应用自己的 API 域名
  return 'https://api.child-domain.com'
}

以上就是 Vite 基座 + Vite 子应用使用 micro-app 从配置到部署的完整实践。核心痛点就两个:0.x 版本关沙箱后的样式隔离和通信1.0 版本升级后这两块基本解决。如果项目还没启动,直接上 1.0;如果已经在跑且稳定,保持现有版本即可。