上一篇 重点讲述了微前端的核心概念、价值(「分而治之」)和应用场景。同时,也提及了微前端架构需要具备的核心能力。
这篇文章,将深入微前端的核心能力,拆解「路由分发」与「应用加载」的全流程。
一、微前端核心架构组成
微前端架构的两个核心角色——主应用(基座应用)和子应用。
主应用负责“统筹调度”,子应用负责“业务实现”,两者通过约定的规则协同工作。而连接两者的核心桥梁,就是「路由分发机制」。
| 角色 | 职责 |
|---|---|
| 主应用 | ① 管理全局布局(Header/Sidebar) ② 监听并分发路由 ③ 注册/加载/卸载子应用 ④ 提供全局通信机制 |
| 子应用 | ① 独立实现业务逻辑 ② 暴露标准生命周期(mount/unmount) |
1. 主应用的具体职责
主应用是整个微前端系统的“调度中心”,不负责具体的业务逻辑,核心作用是:
- 子应用注册管理:维护所有子应用的基本信息(如应用名称、激活路由、资源地址等);
- 路由分发:监听 URL 变化,拦截路由请求,根据预设规则匹配对应的子应用;
- 子应用加载与渲染:根据匹配结果,加载子应用的静态资源(JS/CSS/HTML),并将子应用挂载到指定容器;
- 全局状态与通信:提供跨子应用的通信机制和全局状态管理(后续文章详细讲解);
- 全局资源共享:提供公共组件、工具库、样式等,避免子应用重复打包。
2. 子应用的具体职责
子应用是独立的业务应用,核心作用是:
- 实现具体业务逻辑:如商品管理、订单管理等独立业务模块;
- 适配主应用协议:暴露主应用所需的生命周期钩子(如挂载、卸载、更新),供主应用调用;
- 路由独立管理:维护自身的路由系统,确保在主应用中加载后能正常跳转;
- 资源独立打包:可独立构建、部署,不依赖主应用的构建流程。
二、路由分发的底层逻辑
微前端的核心任务之一:根据 URL 决定激活哪个子应用。比如:
- 访问 `/ops` 时,加载营销的子应用; - 访问 `/assets` 时,加载 资产的子应用; - 访问 `/order` 时,加载订单的子应用。实现上述的核心技术是:监听路由变化&拦截跳转—— 主应用需要监听 URL 的变化,在浏览器发起页面跳转请求之前,先判断该 URL 对应的是哪个子应用,再执行子应用的加载逻辑,而不是让浏览器直接跳转页面。
前端路由的实现方式主要有两种:hash 模式和 history 模式。
1. hash 模式
基于 hashchange 事件拦截路由跳转
window.addEventListener('hashchange',()=>{consthash=location.hash||'#/';/** * 匹配子应用 * microApps = [{ * name: 'ops', * prefix: '#/ops', * js: ['./sub-apps/ops/app.js'], * css: ['./sub-apps/ops/style.css'], * globalKey: '__OPS_APP__' * }] */constmatchedApp=microApps.find(a=>hash.startsWith(a.prefix));if(matchedApp){// 加载子应用(详见下述:加载子应用资源)awaitloadApp(matchedApp);}else{// 未命中子应用,走主应用自己的逻辑// ...}});► 主应用在初始化时,监听window.hashchange事件;
► 用户点击子应用链接或手动修改 hash 时,触发hashchange事件;
► 主应用在事件回调中,解析当前 hash 值,匹配对应的子应用(如 hash 为#/ops匹配营销子应用);
► 如果匹配到子应用,则加载该子应用的资源并挂载;如果未匹配,则执行主应用自身的路由逻辑。
✔️ 优势:兼容性好(支持 IE8 及以上),实现简单,无需后端配置;
2. history 模式 「推荐」
基于 popstate 事件+重写 history API
// 重写pushState方法constoriginalPushState=history.pushState;history.pushState=function(...args){// 执行原生pushStateoriginalPushState.apply(this,args);// 手动触发路由匹配逻辑matchSubApp();};// 重写replaceState方法constoriginalReplaceState=history.replaceState;history.replaceState=function(...args){originalReplaceState.apply(this,args);matchSubApp();};// 监听popstate事件window.addEventListener("popstate",matchSubApp);// 路由匹配逻辑:解析URL,匹配子应用functionmatchSubApp(){constcurrentUrl=window.location.pathname;/** * 匹配子应用 * apps = [{ * name: 'ops', * activeRule: '/ops', * js: ['./sub-apps/ops/app.js'], * css: ['./sub-apps/ops/style.css'], * globalKey: '__OPS_APP__' * }] */constmatchedApp=microApps.find((app)=>currentUrl.startsWith(app.activeRule));if(matchedApp){// 加载子应用(详见下述:加载子应用资源)awaitloadApp(matchedApp);}else{// 未命中子应用,走主应用自己的逻辑// ...}}但这里有个关键问题:pushState和replaceState方法不会触发popstate事件,只有点击浏览器前进/后退按钮时才会触发。因此,微前端基于 history 模式的路由拦截,需要做两件事:
调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发,比如点击后退按钮(或者在 JavaScript 中调用 history.back() 方法)。即,在同一文档的两个历史记录条目之间导航会触发该事件。
► 监听popstate事件:处理浏览器前进/后退导致的 URL 变化;
► 重写history.pushState和history.replaceState方法:当代码中调用这两个方法修改 URL 时,手动触发路由匹配逻辑。
✔️ 优势:URL 美观,符合直觉
❗️ 关注:支持 IE10 及以上,需要后端配置(所有子应用路由都指向主应用入口 HTML,否则刷新会 404)
3. 两种模式对比与选择
| 对比维度 | hash 模式 | history 模式【主流】 |
|---|---|---|
| URL 美观度 | 差(带#) | 好(无#) |
| 兼容性 | 好(IE8+) | 一般(IE10+) |
| 后端配置 | 无需配置 | 需要配置(路由转发) |
| 实现难度 | 简单 | 稍复杂(重写 API) |
三、子应用注册与加载全流程
通过路由拦截,主应用找到了匹配的子应用,接下来核心就是「子应用加载与激活」流程。
1. 注册子应用
主应用需要维护一个“子应用注册表”,记录所有子应用的关键信息。子应用在接入微前端时,需要先向主应用“注册”这些信息。
注册表的核心字段(以 Qiankun 为例):
// 主应用中的子应用注册表constmicroApps=[{name:"ops",// 子应用唯一标识entry:"http://localhost:8081",// 子应用的入口地址(加载资源的基础路径)activeRule:"/ops",// 子应用的激活路由(URL匹配规则)container:"#appContainer",// 子应用挂载的DOM容器},{name:"order",entry:"http://localhost:8082",activeRule:"/order",container:"#appContainer",},];► 注册方式:可以是主应用静态配置(适合子应用数量固定的场景),也可以是动态注册(通过接口从服务端获取子应用列表,适合子应用数量动态变化的场景)。
2. 匹配激活子应用
这里主要指上述「路由拦截」逻辑:主应用监听 URL 变化,解析当前 URL 路径,与子应用注册表中的activeRule进行匹配,找到对应的子应用。
functiongetActiveApp(pathname){returnmicroApps.find((app)=>pathname.startsWith(app.activeRule));}如:当前 URL 为http://localhost:8080/ops/home,主应用遍历microApps,发现ops的activeRule是/ops,当前 URL 以该规则开头,因此匹配到ops子应用。
3. 加载子应用资源
这里主要讲述 HTML 入口加载(主流方案,如 Qiankun)的加载流程。
匹配到子应用后,主应用通过子应用的entry地址(如http://localhost:8081)请求子应用的入口 HTML 文件,然后解析该 HTML 中的script和link标签,加载对应的 JS 和 CSS 资源。
► 主应用请求子应用入口 HTML:fetch(entry + '/index.html')
► 解析 HTML 中的资源标签:提取<script>和<link>标签的src和href属性
► 动态加载资源:根据解析到的资源路径,动态创建<script>和<link>标签,加载对应的 JS 和 CSS 文件
核心逻辑:
// 加载子应用入口HTMLasyncfunctionloadSubAppHtml(entry){constresponse=awaitfetch(entry);consthtml=awaitresponse.text();// 解析HTML中的script和link标签constscripts=parseScriptsFromHtml(html);// 自定义方法:提取script标签的srcconststyles=parseStylesFromHtml(html);// 自定义方法:提取link标签的href// 加载CSS资源awaitPromise.all(styles.map((style)=>loadStyle(style)));// 加载JS资源(这里需要注意JS的加载顺序)awaitPromise.all(scripts.map((script)=>loadScript(script)));returnhtml;}// 加载CSS资源functionloadStyle(href){returnnewPromise((resolve,reject)=>{constlink=document.createElement("link");link.rel="stylesheet";link.href=href;link.onload=resolve;link.onerror=reject;document.head.appendChild(link);});}// 加载JS资源functionloadScript(src){returnnewPromise((resolve,reject)=>{constscript=document.createElement("script");script.src=src;script.onload=resolve;script.onerror=reject;document.head.appendChild(script);});}4. 挂载子应用
子应用资源加载完成后,主应用会调用子应用暴露的mount生命周期钩子,将子应用挂载到指定的container容器中。
示例(子应用导出生命周期):
// Vue子应用的main.jsexportasyncfunctionmount(props){// props是主应用传递给子应用的参数(如容器、全局状态等)const{container}=props;// 初始化Vue实例,挂载到主应用指定的容器newVue({render:(h)=>h(App),}).$mount(container?container.querySelector("#app"):"#app");}exportasyncfunctionunmount(){// 卸载Vue实例,清理资源(避免内存泄漏)vm.$destroy();vm.$el.innerHTML="";}主应用调用挂载逻辑:
// 假设已加载子应用的JS资源,获取到子应用导出的生命周期constsubAppExports=window[subAppName];// 子应用导出的对象// 调用mount方法,传递参数awaitsubAppExports.mount({container:document.querySelector("#subapp-container"),// 可传递其他参数,如全局状态、工具函数等globalState:window.globalState,});5. 卸载子应用
当 URL 变化,匹配到其他子应用时,主应用需要先卸载当前运行的子应用,避免资源占用和冲突。卸载逻辑就是调用子应用暴露的unmount生命周期钩子,清理子应用的 DOM、事件监听、全局变量等资源。
四、动手实践:用原生 JS 实现极简版微前端
核心实现:主应用路由拦截、子应用注册、子应用加载与挂载
1. 项目结构
simple-micro-frontend/ ├── main-app/ # 主应用 │ └── index.html # 主应用入口 ├── ops/ # 营销子应用(简化版,仅用HTML模拟) │ └── index.html # 子应用入口 └── order/ # 订单子应用(简化版,仅用HTML模拟) └── index.html # 子应用入口2. 主应用实现(main-app/index.html)
<divclass="nav"><ahref="/ops">营销子应用</a><ahref="/order">订单子应用</a></div><divid="subapp-container"></div><scriptsrc="micro-core.js"></script>// micro-core.jsletactiveApp=null;// 1. 子应用注册表constmicroApps=[{name:"ops",activeRule:"/ops",entry:"./ops/index.html",container:"#subapp-container",},{name:"order",activeRule:"/order",entry:"./order/index.html",container:"#subapp-container",},];// 2. 重写history API,实现路由拦截(history模式)// 重新实现 history.replaceState 方法,见上述constoriginalPushState=history.pushState;history.pushState=function(...args){originalPushState.apply(this,args);reroute();};// 3. 拦截a标签点击,避免页面刷新document.addEventListener("click",(e)=>{if(e.target.tagName==="A"){e.preventDefault();// 阻止默认跳转consthref=e.target.getAttribute("href");history.pushState(null,"",href);// 手动修改URL}});// 监听popstate事件(浏览器前进/后退按钮)window.addEventListener("popstate",reroute);// 4. 路由分发核心functionreroute(){constpathname=location.pathname;consttargetApp=microApps.find((app)=>pathname.startsWith(app.activeRule));if(activeApp){activeApp.unmount&&activeApp.unmount();// 卸载旧应用activeApp=null;}if(targetApp){loadApp(targetApp);// 加载新应用}}// 5. 加载子应用asyncfunctionloadApp(app){constresponse=awaitfetch(app.entry);constcontent=awaitresponse.text();// 解析子应用入口文件中的 script及styleconst[scripts,styles]=parseScriptsCssFromHtml(content);// 加载对应脚本// 如:document.head.appendChild(link);styles.forEach((style)=>loadStyles(style));// 如:document.body.appendChild(script)scripts.forEach((script)=>loadScript(script));// 执行子应用 mountwindow[app.name].mount({container:document.getElementById("subapp-container")});}// 首次加载reroute();3. 子应用实现(以 ops/index.html 为例)
<script>// 子应用的简单逻辑(模拟业务代码)window.ops={mount({container}){container.innerHTML="<h1>ops 内容</h1>";console.log("ops 挂载");},unmount(){document.getElementById("subapp-container").innerHTML="";console.log("ops 卸载");},};</script>❗️ 实际项目中,需要补充沙箱隔离、子应用生命周期管理、性能优化等功能。
上述 DEMO 示例,只实现了核心流程。实际项目中,还需要考虑更多细节,如:主应用和子应用之间如何高效通信、子应用如何与主应用共享状态、避免 JS 全局变量污染和 CSS 样式冲突(JS 沙箱、样式隔离)等。