Skip to content

微前端应用(二) ​

本页对应 微前端应用,共 5 段视频,约 56 分钟。

学习目标 ​

  1. 能够为主应用添加“打开大屏”按钮,并通过唯一路由名称执行 router.push 动态加载子应用,验证其不与主应用现有路由冲突。
  2. 能够准确描述实际上线流程及前端职责边界:提交自测通过的分支给测试人员,由 CI/CD 平台完成拉取、检查、打包和部署。
  3. 能够使用环境变量文件定义开发/生产环境变量,正确使用规定前缀与 process.env.NODE_ENV 判断当前环境,并验证变量值随环境切换。
  4. 能够独立完成主应用打包与模拟部署,将 dist 放入静态服务器并访问验证,同时说明 History 路由刷新白屏的中间件解决方式。
  5. 能够根据环境切换主应用加载子应用的入口地址,配置子应用线上 base 为绝对地址,清空子应用路由 baseURL,并重新打包部署验证大屏正常加载。
  6. 能够排查主应用与子应用同时上线后的静态资源路径、接口地址不一致及 Token 不共享导致的 401 问题,并通过统一接口环境完成验证。

知识点 ​

本节要解决的问题 ​

本节笔记原文只有一句:

作为子应用的挂载节点,也就是 container 容器要准备好,否则没有挂载的位置。

这句话看似简单,但它决定了微前端子应用能不能“出现”在主应用页面里。子应用不是凭空渲染的,它必须有一个 DOM 落点;这个落点就是挂载容器。转写稿里反复出现的“挂在节点”,应纠正为“挂载节点”;“EL 杠but”应理解为 el-button;“doll route pushush”应理解为 $router.push。本节笔记未附图片,以下不虚构图片;转写稿中与入口按钮、挂载位置相关的代码按原意整理。

为什么需要: 主应用必须先有一个稳定位置,子应用才能被加载并渲染进去。
怎么想: 先把“入口”与“落点”分开:按钮负责跳转,路由页面负责提供容器。
怎么做: 在主应用里准备专门的路由和挂载节点,再让子应用注册时使用这个节点。
易错点: 只配置了子应用,却没有准备容器;或者容器选择器与注册配置不一致,页面就会没有挂载位置。

从按钮跳转理解“先有落点” ​

转写稿视频1中,在管理员位置加了一个按钮,按钮文案是“打开大屏”,点击时执行路由跳转,目标指向 big。这里有一个小细节:这个路由名称在整个主应用里必须是没有的,也就是不能和主应用已有路径冲突。原因很直接:这个地址是专门用来加载子应用的,如果主应用里本来就有同名路由,主应用就不知道你到底要去哪里。

转写稿中的关键写法可整理为如下示意:

html
<el-button @click="$router.push({ name: 'big' })">打开大屏</el-button>

调试时还处理了按钮太挤的问题:给这个 name 加左边 margin,让它和旁边内容拉开距离。这个细节虽然小,但说明“入口按钮”和“目标路由”要一起检查:按钮能点、路由能跳、跳过去之后有容器可挂载。

为什么需要: 用户需要一个进入子应用页面的入口。
怎么想: 把它当成普通路由跳转,但目标路由要专用于子应用。
怎么做: 加 el-button,绑定 $router.push,路由名称保持主应用内唯一。
易错点: 路由名与主应用冲突;只写了按钮却忘了准备目标路由和容器;样式挤在一起影响点击体验。

挂载容器怎么准备 ​

主应用跳转到目标路由后,对应页面里要准备好容器节点。这个节点就是子应用的“插座”。转写稿在讲无界方案时说得很直白:写一个组件,把这个组件写到刚才主应用准备挂载节点的位置,再指定一下子应用的 URI 地址,配置就完事了。也就是说,容器不是抽象概念,它最终要落到主应用模板中的一个具体位置。

为什么需要: 没有容器,子应用即使被加载,也没有地方渲染。
怎么想: 先确认主应用页面里有一个稳定、唯一的挂载位置,再让子应用注册配置指向它。
怎么做: 在目标路由组件中准备容器;无界方案中把无界组件放到这个挂载节点位置,并配置子应用地址。
易错点: 容器不存在或选择器写错;把组件放到无关位置;子应用地址写错,导致容器有了但内容加载不出来。

用无界方案对照理解 ​

转写稿提到,无界方案更简单:子项目什么都不用动,只改主项目;如果主应用是 Vue 框架,可以直接使用无界 Vue 的封装,装好包、引入组件,把组件放到主应用准备挂载节点的位置,再指定子应用 URI 地址即可。这个对照能帮助我们理解“挂载容器”的本质:不管用乾坤还是无界,主应用都要先给出一个位置。

为什么需要: 理解不同方案下容器位置的表现形式,避免只会照抄配置。
怎么想: 乾坤里关注注册时的容器选择器,无界里关注组件放在哪个挂载位置。
怎么做: 先准备主应用节点,再按方案放置组件或配置容器。
易错点: 把“加载地址”当成“挂载位置”;地址和容器是两个问题,不能混为一谈。

检查清单与易错点 ​

完成本节后,至少检查:点击“打开大屏”能否跳转;目标路由名称是否在主应用中唯一;目标页面是否真的渲染了挂载容器;子应用注册或无界组件是否指向这个位置;控制台是否出现找不到容器、地址错误等提示。转写稿后续部署部分还提醒,入口地址在开发环境和上线环境可能不同,上线时要按环境切换,不能把开发地址写死;但这属于后续部署内容,本节先确保“有容器可挂”。

为什么需要: 挂载容器是最基础的一环,前面错一步,后面加载和通信都无从谈起。
怎么想: 按“按钮入口 → 唯一路由 → 容器节点 → 子应用加载”的顺序排查。
怎么做: 逐项验证,先保证页面能跳到目标路由,再确认容器存在。
易错点: 路由冲突、容器缺失、选择器不一致、地址写错,都是常见问题。

先看问题:开发阶段为什么 assets 会找错地方 ​

子应用被主应用加载后,浏览器解析静态资源时,并不总是按我们以为的“子应用目录”去找。开发阶段子应用通常跑在自己的 Vite 开发服务器上,例如 localhost:5173;主应用则跑在另一个地址。主应用把子应用挂载进来后,如果子应用生成的资源 URL 没有带上自己的开发服务器 origin,浏览器就可能以主应用的地址为基准去请求 assets,最后变成去主应用下面找子应用的资源,出现 404 或资源加载失败。

转写稿里提到过一个很关键的思路:子应用被主应用加载过去后,“走的是主应用那个链接”,所以需要强制让它走子应用自己的地址。本节要解决的正是开发阶段的这一类静态资源路径问题,只不过落点不是业务代码,而是子应用 Vite 开发服务器的 server.origin 配置。

怎么想:让子应用开发服务器声明自己的 origin ​

这里不要先去改路由,也不要先怀疑图片路径写错。先判断:请求到底发到了哪个域名和端口。如果 Network 面板里 assets 请求落到了主应用端口,而不是子应用的 5173,就说明资源 URL 的 origin 不对。

Vite 的 server.origin 用来指定开发服务器生成资源 URL 时使用的 origin。配置后,开发阶段相关静态资源会以子应用自己的开发地址为准,而不是被主应用地址“带走”。所以思路是:在子应用的 Vite 配置里,把开发服务器 origin 明确写成子应用实际运行地址。

怎么做:在子应用里补 server.origin ​

在子应用的 Vite 配置文件中,找到 server 选项,加入笔记中的配置:

json
server: {
    // 防止开发阶段的assets 静态资源加载问题
    origin: '//localhost:5173'
}

这段配置的关键点有三个:

  1. 它放在子应用的 Vite 配置里,不是主应用里。因为要声明的是“子应用开发服务器自己的 origin”。
  2. 5173 必须和子应用实际启动端口一致。如果端口被占用后 Vite 自动换了端口,这里也要同步改,否则配置看起来写了但不生效。
  3. 改完后要重启子应用开发服务器,再启动或刷新主应用验证。只在编辑器里保存但不重启,开发服务器配置不一定立即按新值工作。

验证时打开浏览器 Network,重新加载主应用并进入子应用页面,观察 assets 下资源的请求地址。修复前如果请求落在主应用端口,修复后应看到请求回到子应用的 localhost:5173。如果仍然不对,优先检查:是不是改错了项目、端口是否一致、是否重启、主应用注册子应用时填的入口地址是否仍指向子应用开发地址。

易错点与边界 ​

第一,server.origin 是开发服务器配置,不是路由 base,也不是生产部署配置。转写稿后面讲上线时提到,子应用上线还要根据环境切换加载地址、配置 base 为线上绝对地址,那是生产部署阶段的事,不能拿开发阶段的 //localhost:5173 直接当线上配置。

第二,不要只改主应用。这个问题的资源来源在子应用,配置应落在子应用。

第三,注意端口一致性。主应用加载子应用的入口地址、子应用实际启动端口、server.origin 三者要对得上,否则会出现“入口能打开、资源却 404”的错觉。

第四,origin 写成协议相对形式 //localhost:5173 时,要结合当前开发协议理解;如果本地存在 http/https 混用,仍要检查浏览器是否拦截混合内容。最稳妥的调试方式始终是看 Network 里真实请求 URL,而不是凭感觉猜路径。

监听路由变化 ​

为什么需要:微前端主应用本身是一套路由系统,子应用也有自己的路由。用户点击“打开大屏”后,主应用必须知道地址变了,才能判断“现在该不该加载子应用”。如果没人监听路由变化,页面只会停在主应用里,子应用永远不会被触发。

怎么想:把路由变化看成一次“信号”。主应用不需要自己造一套复杂机制,以乾坤这类方案为例,框架启动后会监听地址变化;业务侧要做的是保证这次跳转能发生、地址能被识别。转写稿里是在管理员页面加按钮,点击后执行一次普通路由跳转,这个跳转就是信号。

怎么做:在按钮上绑定点击事件,事件里调用 router.push。代码示意如下,路由名以项目实际配置为准:

js
// 主应用按钮:进入子应用路由
openBigScreen() {
  this.$router.push({ name: 'big' })
}
html
<el-button @click="openBigScreen">打开大屏</el-button>

易错点:转写稿反复提醒,这个路由名称在主应用路由表里必须是“独一无二”的,不能和主应用已有地址冲突;否则框架无法判断你到底要去主应用页面还是子应用。按钮样式太挤时,转写稿里是给按钮加左侧 margin 调整间距,这属于交互细节,不要和路由逻辑混在一起。

匹配子应用 ​

为什么需要:地址变化后,主应用要知道“这个地址归哪个子应用管”。微前端里可能不止一个子应用,不能所有路由都加载同一个子应用。

怎么想:匹配的核心是“路由规则和子应用注册信息对上”。主应用注册子应用时会提供入口、激活规则等信息;业务侧要保证点击后进入的地址落在子应用的匹配范围内。转写稿里说“这个名称在整个主应用里边是不是没有”,本质就是让这个地址成为专门加载子应用的地址。

怎么做:注册子应用时,把子应用的激活路由和入口配置好;主应用跳转时使用该路由。示意配置如下:

js
// 主应用注册子应用时,entry 不能写死开发地址
const isDev = process.env.NODE_ENV === 'development'

entry: isDev ? 'http://localhost:5173' : 'http://localhost:8089'

易错点:不要把子应用路由写成主应用已有路由,否则会冲突;也不要把开发地址写死。转写稿在部署阶段就遇到这个问题:开发时子应用跑在 5173,上线后要切到服务器地址 8089,所以用 process.env.NODE_ENV 判断环境。这里纠正一个同音字:是“乾坤”方案,不是“ Qiankun ”随意拼写;是“无界”方案,不是“五件方案”。

加载子应用 ​

为什么需要:匹配到子应用后,主应用还要把子应用的 HTML、JS、CSS 等资源取回来。没有加载,页面上只有空容器。

怎么想:加载地址来自子应用注册时的 entry。开发环境指向本地子应用服务,生产环境指向部署后的子应用服务器。转写稿里主应用加载子应用时有一个 entry 配置项,部署时把它从开发地址切换成线上地址。

怎么做:用环境变量区分两套地址;子应用侧还要配置资源基础路径,避免 JS、CSS、图片仍从主应用地址下找。转写稿中子应用 vite.config 增加 base,并把它设为线上子应用的绝对地址:

js
// 子应用 vite.config
base: 'http://localhost:8089'

易错点:只改主应用 entry 不够。子应用打包后的静态资源如果还走相对路径,可能被主应用地址接管,导致资源 404。转写稿还提到子应用路由里的 baseURL 要置空,让路由以主应用运行地址为主:

js
// 子应用 router,去掉自己的 base
createWebHistory('')

渲染子应用 ​

为什么需要:资源加载完成后,子应用要挂载到主应用指定的容器里,用户才能看到大屏页面。渲染阶段还要处理样式隔离、JS 隔离等问题,否则主应用和子应用会互相污染。

怎么想:把渲染理解成“把子应用塞进主应用留好的坑位”。主应用提供挂载节点,子应用启动并挂载;框架负责沙箱隔离。转写稿提到无界方案时解释过 iframe 沙箱:相当于内嵌一个独立浏览器空间,JS 和 CSS 互相不影响。

怎么做:主应用准备容器节点,注册子应用时指定容器;子应用正常启动。部署调试时,转写稿点击“打开大屏”后出现 401,原因是后台和大屏调用的接口服务不同,一个走 API,一个走 V1,token 不共享。后来把后台环境变量也改成 V1,两边 token 才能共用。这个调试过程说明:渲染出来不等于功能正常,接口域名、token 作用域也要核对。

易错点:刷新白屏是 history 路由常见问题。转写稿用 Node 服务加 history API fallback 中间件解决:后端不处理时,刷新某个前端路由会被当成资源请求,返回 not found;中间件把路径回退到 index.html 后才正常。另一个易错点是主应用和子应用接口域名不一致导致 token 不共享,不要误以为“有 token 就一定能访问”。

目标:把“加载子应用”拆成三件事 ​

为什么需要:主应用不能把子应用代码直接打进自己的 bundle,否则子应用独立开发、独立部署就失去意义。运行时按路由匹配子应用配置,再动态拉取子应用 HTML 和脚本,才能做到“点按钮进入大屏”这类按需加载。
怎么想:一次加载可以拆成“路由变化 → 匹配 activeRule → 拉取 entry HTML → 塞进容器 → 执行脚本 → 调子应用挂载”;离开时则清空容器并卸载。
怎么做:用 import-html-entry 拉 HTML 并执行脚本,用 rewriteRouter 监听路由变化,用 loadMicroApp 统一处理挂载/卸载。
易错点:激活路由必须与主应用路由区分开。转写稿里点击按钮执行 router.push 跳到 /big,这个路径在主应用中“压根没有”,是专门给子应用用的;若与主应用路径冲突,主应用自己就会先匹配走。

路由重写:为什么要拦 pushState 和 popstate ​

为什么需要:SPA 路由跳转通常走 history.pushState,不会刷新页面;只监听 popstate 只能覆盖前进/后退,覆盖不了代码里的 push。
怎么想:保留原始 pushState,包装后先执行原逻辑,再触发子应用加载;popstate 则直接触发加载。
怎么做:

javascript
function rewriteRouter () {
  window.addEventListener('popstate', () => {
    loadMicroApp()
  })
  const rawPushState = window.history.pushState
  window.history.pushState = async (...rest) => {
    rawPushState.apply(window.history, rest)
    console.log('监听到 pushState 方法')
    loadMicroApp()
  }
}

易错点:包装里要用 apply(window.history, rest) 保住 this;先调用原始方法再加载,保证 location.pathname 已更新;loadMicroApp 是异步过程,调试时不要假设它立刻完成。

loadMicroApp:匹配、加载、挂载与卸载 ​

为什么需要:路由变化后要有一个统一入口决定“加载谁、放哪里、何时卸载”。
怎么想:用 window.location.pathname 找配置;有匹配就 importHTML(entry),拿 template 塞容器,再 execScripts(),最后调子应用暴露的挂载方法;无匹配就清空旧容器并卸载。
怎么做:

javascript
// 用来加载子应用html并解析
import importHTML from 'import-html-entry'

// 重新路由
function rewriteRouter () {
  // 针对 go/back
  window.addEventListener('popstate', () => {
    loadMicroApp()
  })
  // 针对pushState
  const rawPushState = window.history.pushState
  window.history.pushState = async (...rest) => {
    rawPushState.apply(window.history, rest)
    console.log('监听到 pushState 方法')
    // 加载子应用
    loadMicroApp()
  }
}

// 加载子应用资源
let _activeMicroApp = null
function loadMicroApp () {
  // 1. 匹配激活路由 并加载子应用html
  const path = window.location.pathname
  const microApps = getMicroApp()
  const microAppConfig = microApps.find(app => app.activeRule === path)
  _activeMicroApp = _activeMicroApp || microAppConfig
  console.log('当前配置', microAppConfig)
  if (microAppConfig) {
    // 挂载
    importHTML(microAppConfig.entry).then(async res => {
      // 2. 获取模版
      const template = res.template
      const container = document.querySelector(microAppConfig.container)
      container.innerHTML = template
      // 2. 获取脚本并执行
      await res.execScripts()
      window.__qiankunMount()
    })
  } else {
    // 卸载
    const container = document.querySelector(_activeMicroApp.container)
    container.innerHTML = ''
    _activeMicroApp = null
    window.__qiankunUnMount()
  }
}

// 注册子应用配置
let microApps = []
function registerMicroApp (configArr) {
  microApps = configArr
}
function getMicroApp () {
  return microApps
}

// 启动方法
function start () {
  rewriteRouter()
}

// 导出通用方法
export {
  registerMicroApp,
  start
}

易错点:_activeMicroApp = _activeMicroApp || microAppConfig 只适合“首次记录”,如果从一个子应用切到另一个,它不会更新;卸载分支里若 _activeMicroApp 为空,querySelector 会报错,实际代码应判空。window.__qiankunMount / __qiankunUnMount 需要子应用按约定暴露,否则执行脚本后不会挂载。

注册与启动:把配置和入口收口 ​

为什么需要:主应用不应该到处写死子应用地址,注册配置后统一由 start 接管路由监听。
怎么想:registerMicroApp(configArr) 存数组,getMicroApp() 取配置;start() 只做路由重写。
怎么做:配置里至少包含 activeRule、entry、container;启动时调用 start()。
易错点:start() 只重写路由,首次直接进入子应用路径时不会自动加载,通常还要在启动后主动执行一次 loadMicroApp()。转写稿里还强调:开发时 entry 可能是 localhost:5173,上线后要换成子应用服务器地址(如 8089),可用 process.env.NODE_ENV 判断,不要把开发地址写死。

调试与上线补充:转写稿里的几个坑 ​

为什么需要:核心代码能跑起来,不等于上线能跑通。转写稿里的调试过程很典型:点击“打开大屏”后出现 401,排查发现后台接口走 /api,子应用接口走 /v1,两个服务的 token 不共享;把后台接口也统一到 /v1 后,token 才能共用。
怎么想:先看网络请求前缀,再看环境变量和部署地址;主应用 history 模式刷新白屏,则用 connect-history-api-fallback 这类中间件把路径回退到 index.html。
怎么做:子应用 vite.config 里配置线上 base,保证 JS、CSS、图片走子应用自己的绝对地址;主应用加载地址按环境切换。
易错点:不要把“接口都能调”误认为“token 通用”;不要忽略静态资源 base,否则子应用被主应用加载后资源会走错域名。

本节要验证什么 ​

这一节的目标不是继续堆功能,而是把前面写好的主应用、子应用真正联调起来:主应用能按路由加载子应用,子应用能挂载、能卸载,页面里的按钮交互也正常。转写稿里把这一步称为“最后一步”:在主应用里加一个“打开大屏”的按钮,点击后做普通路由跳转,让 activeRule 命中,从而动态加载子应用。这里要特别注意:这个跳转路径必须是主应用路由系统里没有的、独一无二的地址,专门用来加载子应用;否则主应用自己也不知道该去哪儿。

为什么需要:只有通过真实路由触发,才能验证 activeRule 是否生效。
怎么想:先保证子应用能独立跑,再让主应用按规则加载它。
怎么做:先保留下面的注册配置,再在主应用加按钮跳转。
易错点:路径和主应用已有路由冲突;只写了按钮但没触发路由;start() 没调用。

主应用注册配置:让主应用知道“加载谁、放哪里、何时加载” ​

按笔记原样保留主应用配置:

javascript
// 注册微应用配置
registerMicroApp([
  {
    name: 'big-screen',
    entry: '//127.0.0.1:5500/',
    container: '#container',
    activeRule: '/big-screen'
  }
])

start()

为什么需要:主应用必须知道子应用的名字、入口地址、挂载容器和激活规则,否则无法加载。
怎么想:entry 指向子应用静态服务地址;container 是主应用页面里的挂载点;activeRule 是路由前缀,命中后才加载。
怎么做:确认子应用在 127.0.0.1:5500 可访问,主应用模板里存在 #container,然后调用 start()。
易错点:entry 的协议、端口、末尾斜杠要和实际服务一致;container 选择器写错会挂不上去;activeRule 不要和主应用已有路由重复。转写稿特别强调,这个路径是“独一无二”的,不能跟主应用里的地址发生冲突。

主应用入口按钮:用普通路由跳转触发加载 ​

转写稿里的做法是:在主应用里加一个“打开大屏”按钮,点击时执行 router.push('/big-screen')。这不是特殊 API,就是普普通通的路由跳转;跳过去以后,activeRule: '/big-screen' 命中,子应用才被加载。

为什么需要:需要一个明确的用户操作来触发路由变化。
怎么想:activeRule 是开关,路由跳转是拨动开关的动作。
怎么做:按钮绑定点击事件,事件里 router.push('/big-screen')。
易错点:路径必须主应用里原本没有;不要写成主应用已有页面路由;按钮样式、间距只是展示问题,不影响加载逻辑。

子应用入口:静态页面与模拟生命周期 ​

按笔记原样保留子应用代码:

html
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>我是子应用</title>
  <style>
    #root {
      width: 500px;
      height: 400px;
      background-color: royalblue;
      color: #fff;
    }
  </style>

</head>

<body>
  <div id="root">
    <button id="btn">toggleColor</button>
  </div>
  <script>
    const btn = document.querySelector('#btn')
    const root = document.querySelector('#root')
    btn.addEventListener('click', () => {
      root.style.backgroundColor = 'red'
    })

    // 模拟生命周期函数
    function mount () {
      console.log('mount by 乾坤')
    }

    function unMount () {
      console.log('unMount by 乾坤')
    }

    window.__qiankunMount = mount
    window.__qiankunUnMount = unMount
  </script>
</body>

</html>

为什么需要:先独立验证子应用自身没有报错,再谈主应用加载。
怎么想:页面里有 toggleColor 按钮,点一下应能变色;生命周期先用全局函数模拟,方便在控制台观察挂载和卸载。
怎么做:先把子应用跑在 127.0.0.1:5500,浏览器直接访问,点按钮看颜色变化;再回主应用点击“打开大屏”,看控制台是否打印 mount by 乾坤,切走路由看是否打印 unMount by 乾坤。转写稿里的“乾坤”即 qiankun。
易错点:子应用端口必须和 entry 一致;全局函数名要和模拟时保持一致;如果直接打开子应用都报错,先别查主应用。

联调与调试顺序 ​

为什么需要:微前端联调问题多,顺序错了很难定位。
怎么想:先子应用、再主应用、最后看路由和容器。
怎么做:启动子应用静态服务 → 启动主应用 → 点击“打开大屏”跳 /big-screen → 观察 #container 是否出现子应用 → 控制台看 mount / unMount → 点 toggleColor 验证事件。
易错点:页面空白先看 entry 请求是否 200、#container 是否存在、activeRule 是否命中;控制台没有 mount,先检查 window.__qiankunMount 是否挂上;按钮无效,检查脚本是否执行、选择器是否写错。

测试通过的标准是:主应用能跳转并加载子应用,子应用能正常挂载、卸载,toggleColor 按钮能变色,控制台能观察到模拟生命周期输出。

代码示例 ​

示例一:微前端核心调度器(src/micro/index.js) ​

js
// src/micro/index.js
import importHTML from 'import-html-entry'

// 注册进来的子应用配置列表
let microApps = []
// 记录当前已激活的子应用,用于卸载
let _activeMicroApp = null

// 注册子应用配置
export function registerMicroApp (configArr) {
  microApps = configArr
}

function getMicroApp () {
  return microApps
}

// 重新路由:劫持 pushState / popstate
function rewriteRouter () {
  // 针对 go / back
  window.addEventListener('popstate', () => {
    loadMicroApp()
  })

  // 针对 pushState
  const rawPushState = window.history.pushState
  window.history.pushState = async (...rest) => {
    rawPushState.apply(window.history, rest)
    console.log('监听到 pushState 方法')
    // 加载子应用
    loadMicroApp()
  }
}

// 加载子应用资源
async function loadMicroApp () {
  // 1. 匹配激活路由
  const path = window.location.pathname
  const microApps = getMicroApp()
  const microAppConfig = microApps.find(app => app.activeRule === path)
  _activeMicroApp = _activeMicroApp || microAppConfig
  console.log('当前配置', microAppConfig)

  if (microAppConfig) {
    // 2. 加载子应用 html 并解析
    const res = await importHTML(microAppConfig.entry)
    const template = res.template
    const container = document.querySelector(microAppConfig.container)
    // 挂载
    container.innerHTML = template
    // 3. 获取脚本并执行
    await res.execScripts()
    // 4. 渲染:调用子应用暴露的生命周期
    window.__qiankunMount && window.__qiankunMount()
  } else {
    // 卸载
    if (!_activeMicroApp) return
    const container = document.querySelector(_activeMicroApp.container)
    container.innerHTML = ''
    _activeMicroApp = null
    window.__qiankunUnMount && window.__qiankunUnMount()
  }
}

// 启动方法
export function start () {
  rewriteRouter()
}

关键行与易错点

  • rawPushState.apply(window.history, rest) 必须先执行原方法再调 loadMicroApp(),否则 window.location.pathname 还是旧值,会匹配到上一个路由。
  • 只劫持了 pushState 和 popstate,replaceState 未处理;如果项目里用了 router.replace,需要同样补一段劫持,否则路由变了但子应用不加载。
  • _activeMicroApp = _activeMicroApp || microAppConfig:这行保证“已经激活过就不再覆盖”,否则从子应用路由返回时拿不到旧容器去清空。但首次进入且不匹配任何子应用时 _activeMicroApp 为 null,笔记原码直接 _activeMicroApp.container 会报错,因此加了 if (!_activeMicroApp) return 保护。
  • container.innerHTML = template 依赖容器先存在,见示例二的 <div id="container">;容器不存在会直接抛 Cannot set properties of null。
  • execScripts() 是异步的,必须 await 之后再调用 __qiankunMount,否则生命周期函数还没挂到 window 上。

示例二:主应用——挂载容器 + 注册子应用 + 按钮跳转 ​

html
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>主应用</title>
</head>
<body>
  <div id="app"></div>
  <!-- 子应用的挂载节点,必须提前准备好 -->
  <div id="container"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>
js
// src/main.js
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { registerMicroApp, start } from './micro'

// 适配子应用在「开发」和「上线」两套环境下的加载地址
const isDev = process.env.NODE_ENV === 'development'

// 注册微应用配置
registerMicroApp([
  {
    name: 'big-screen',
    entry: isDev ? '//127.0.0.1:5500/' : '//localhost:8089/',
    container: '#container',
    activeRule: '/big-screen'
  }
])

start()

createApp(App).use(router).use(ElementPlus).mount('#app')
vue
<!-- 管理员页面中的按钮:点击动态加载子应用 -->
<template>
  <el-button type="primary" class="open-screen" @click="openBigScreen">
    打开大屏
  </el-button>
</template>

<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()

const openBigScreen = () => {
  router.push('/big-screen')
}
</script>

<style scoped>
.open-screen {
  margin-left: 20px;
}
</style>

关键行与易错点

  • <div id="container"></div> 就是笔记里说的“挂载容器”,registerMicroApp 里的 container: '#container' 必须和它一模一样;写在 #app 内部容易被 Vue 重渲染清掉,建议放在 #app 外面。
  • activeRule: '/big-screen' 必须是主应用路由表里不存在的路径,独一无二,专门用来加载子应用;和主应用路由冲突就永远匹配不到。
  • entry 走环境变量切换:开发指向子应用 dev server(5173/5500),上线指向子应用部署服务器(8089),这是转写稿里强调的“适配子应用在开发和上线两套环境下的加载地址”。
  • process.env.NODE_ENV 是固定前缀 + 内置变量名;Vite 下它默认会被替换成 'development' / 'production',别写成 process.env.NODE_ENV === 'dev'。
  • 按钮里 router.push('/big-screen') 只是一次普普通通的路由跳转,真正的加载由示例一劫持的 pushState 触发,不要在按钮里手动调 loadMicroApp。

示例三:子应用——HTML 模板、生命周期与 vite 配置 ​

html
<!-- 子应用 index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>我是子应用</title>
  <style>
    #root {
      width: 500px;
      height: 400px;
      background-color: royalblue;
      color: #fff;
    }
  </style>
</head>
<body>
  <div id="root">
    <button id="btn">toggleColor</button>
  </div>
  <script>
    const btn = document.querySelector('#btn')
    const root = document.querySelector('#root')
    btn.addEventListener('click', () => {
      root.style.backgroundColor = 'red'
    })

    // 模拟生命周期函数
    function mount () {
      console.log('mount by 乾坤')
    }

    function unMount () {
      console.log('unMount by 乾坤')
    }

    window.__qiankunMount = mount
    window.__qiankunUnMount = unMount
  </script>
</body>
</html>
js
// 子应用 vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  // 上线后以子应用自己的服务器地址为基准加载 js/css/图片
  base: '//localhost:8089/',
  server: {
    // 防止开发阶段的 assets 静态资源加载问题
    origin: '//localhost:5173'
  },
  plugins: [vue()]
})
js
// 子应用 src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  // 清空 base,以主应用实际运行的地址为主
  history: createWebHistory(''),
  routes: []
})

export default router

关键行与易错点

  • server.origin: '//localhost:5173' 只影响开发阶段:不配的话,子应用被主应用加载后,assets 里的资源会默认去主应用的 assets 目录找,控制台一片 404。
  • base: '//localhost:8089/' 是上线阶段的对应处理:子应用被主应用“包”进来后,相对路径会以主应用地址为准,不写死绝对 base,js/css/图片就全部加载不到。
  • createWebHistory('') 把子应用路由原来的 base 清空,否则子应用自己带一段 base 前缀,和主应用 activeRule 拼起来路径对不上。
  • 生命周期必须挂成 window.__qiankunMount / window.__qiankunUnMount,名字和示例一里调用的一致;execScripts() 执行完这些脚本后才会存在,所以顺序不能反。
  • 子应用里 #root 是自己模板中的节点,示例一里 container.innerHTML = template 正是把它整段塞进主应用的 #container,两边的 id 不要混用。

常见错误 ​

  1. 点击“打开大屏”后进入主应用 404 或仍显示主应用页面
    现象:按钮跳转后子应用不加载,或落到主应用已有页面。
    原因:activeRule 与主应用路由冲突,或跳转路径与注册的 activeRule 不完全一致。
    修法:给子应用设置主应用中不存在的独立路径(如 /big-screen),router.push 与 activeRule 保持完全一致,并检查尾斜杠/大小写。

  2. 子应用加载时报容器相关错误或不显示
    现象:querySelector 拿到 null,或页面无子应用内容。
    原因:挂载容器未提前准备,或注册的 container 选择器与实际 DOM 不一致。
    修法:主应用模板先保留对应容器节点,注册配置中的 container 与 DOM 选择器保持一致,确保加载前节点已存在。

  3. 开发阶段子应用 assets 资源 404,请求跑到主应用目录下
    现象:图片、字体等静态资源加载失败,路径指向主应用地址。
    原因:子应用 Vite 开发服务未配置资源 origin。
    修法:在子应用 server.origin 中配置自身开发地址,如 //localhost:5173。

  4. 生产部署后子应用 JS/CSS/图片 404
    现象:主应用能打开,子应用页面空白或样式丢失,控制台资源路径错误。
    原因:子应用未配置线上 base,或 router 中仍使用自身环境 baseURL。
    修法:子应用配置线上绝对 base;将 router 的 baseURL 置空或改为主应用运行地址为主;重新打包部署。

  5. 生产环境仍去加载开发地址 5173
    现象:上线后主应用请求子应用开发端口,加载失败。
    原因:注册子应用的 entry 写死为开发地址,未按环境切换。
    修法:用 process.env.NODE_ENV 判断开发/生产,开发用 5173,生产改为子应用线上服务器地址,并重新打包主应用。

  6. 环境变量打印为 undefined 或不生效
    现象:代码中读取环境变量拿不到值。
    原因:变量名未按要求加前缀,或定义文件与执行命令不匹配。
    修法:在 .env.development / .env.production 中定义,业务变量使用 VITE_APP_ 前缀,按 run dev / build 对应环境检查读取值。

  7. 登录后打开大屏接口 401
    现象:主应用有 token,但子应用接口无权访问。
    原因:主应用与子应用接口前缀不一致(如一个 API、一个 V1),token 不属于同一服务。
    修法:统一接口前缀到同一服务,重新登录获取对应 token,再验证子应用接口。

  8. history 模式刷新页面 404 或白屏
    现象:页面内跳转正常,刷新后找不到资源。
    原因:后端/静态服务器未把前端路由回退到 index.html。
    修法:Node 服务增加 history fallback 中间件;NGINX 使用 try_files 回退到 index.html。

练习 ​

  1. 在主应用管理员页面增加“打开大屏”按钮,配置跳转路径并注册子应用,要求 activeRule 不与主应用路由冲突;写出你检查路径一致性的 3 个点。
  2. 模拟子应用开发环境 assets 404:定位请求地址,修改 Vite 配置修复,并说明修复后请求应落到哪个端口。
  3. 完成一次生产部署改造:列出主应用 entry、子应用 base、子应用 router baseURL 三处应如何随环境调整,并说明漏改任一处会出现什么现象。
  4. 构造“主应用登录后子应用 401”的排查过程:从 Network、环境变量、token 所属服务三个方向定位并修复。
  5. 给本地 Node 静态服务器或 NGINX 增加 history fallback,验证刷新 /big-screen 不再 404/白屏。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1微前端主应用点击按钮跳转06:57
2实际上线流程说明05:26
3环境变量基础使用14:12
4模拟上线-主应用模拟上线08:52
5模拟上线-主应用和子应用同时上线20:53

小结 ​

  1. 手工微前端主链路是:路由监听 → 匹配子应用 → 加载 HTML/脚本 → 挂载/卸载生命周期。
  2. activeRule 必须唯一且避开主应用已有路由,跳转路径要与注册配置严格一致。
  3. 挂载容器必须先存在,container 选择器要与实际 DOM 对齐。
  4. 开发阶段优先检查子应用 server.origin,避免静态资源错误落到主应用。
  5. 生产部署重点检查三件事:entry 是否指向线上子应用、子应用 base 是否正确、router baseURL 是否仍带自身前缀。
  6. 环境变量要按文件、命令、前缀三处同时核对,联调时先看实际请求地址和值。
  7. 主/子应用独立打包部署时,接口前缀与 token 共享范围必须统一,否则容易出现 401。
  8. 上线排查顺序建议:路由匹配 → 容器 → 静态资源路径 → 接口前缀/token → history fallback/转发配置。