前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版

微前端实战总结,从 single-spa 到 qiankun 的原理拆解

首页2021-07-31 20:35:08Front-End
微前端qiankunsingle-spaicestarkJS 沙箱样式隔离

一个 Vue 2 写的后台管理系统跑了两年,产品说新模块要用 React 写,同时老的那十几个页面一个都不能动。这种需求听着像抬杠,但在稍微有点年头的公司里是常态。当时我第一反应是套个 iframe 得了,结果试了半天,弹层居中、刷新丢路由、父子通信,每一样都要单独写补丁。

后来才发现,这类问题社区早就有一整套答案,就是微前端。这篇把我自己啃 single-spa 和 qiankun 时理清的四件事写下来,路由怎么劫持、子应用资源怎么加载、JS 全局环境怎么隔离、样式怎么不互相污染。每一块都有能跑通的代码,也标了我踩过的坑。

微前端架构总览

在本篇文章中,我们将从浅入深,和大家一起学习以下知识:

  • 微前端到底在拆什么、合什么,什么情况下不值得上
  • single-spa 做了哪些事、又故意不做哪些事
  • qiankun 在 single-spa 之上补的那两块拼图
  • 飞冰 icestark 的另一种接入姿势
  • CSS 隔离的四条路子,每条在什么场景会失效
  • 快照沙箱与 Proxy 沙箱的完整实现,以及各自的边界
  • 应用间通信和公共依赖的几种处理方式

# 一、微前端在拆什么,又在合什么

微前端现有的落地方案大致能分三类,自组织模式、基座模式,以及模块加载模式。国内绝大多数团队用的是基座模式,也就是有一个主应用负责路由和调度,若干子应用挂在它下面。

微前端的拆与合示意

一句话概括,微前端就是把不同功能按不同维度拆成多个子应用,再由一个主应用把它们加载回来。

微前端的核心在于拆,拆完后再合。

拆的动机通常是这三个里的某一个。第一个是技术栈打架,不同团队开发同一个应用,有人要用 React 有人守着 Vue 2,谁也说服不了谁。第二个是发版被绑死,一个仓库十几个模块,任何一个改动都要全量回归、全量发布。第三个是老代码不敢碰,项目里还留着几年前的页面,业务方不给重写预算,但新功能得用新技术写。

那把一个应用划成若干子应用,各自打包成 lib,路径切换时按需加载,是不是就都解决了?子应用独立构建、独立部署,技术栈也不再受主应用限制,协同开发这件事就顺了。

代价也得先说清楚。多一层运行时调度就多一层故障面,样式串了、子应用白屏、路由回退两次,这些排查链路都比普通 SPA 长。如果你的团队对整个系统有完全的话语权,也有人力做统一治理,那老老实实做模块化和 monorepo 可能更划算。上不上微前端,先算这笔账。

关于各家方案的横向对比和选型路径,我在 微前端落地方案对比,qiankun 无界 Module Federation 怎么选 里写得更细,这篇专注讲机制本身。

# 二、从 single-spa 到 qiankun,各自补了什么

single-spa 与 qiankun 的演进关系

时间线其实很短。2018 年 single-spa 出现,它把自己定位成「一个用于前端微服务化的 JavaScript 前端解决方案」,做的事情非常克制,只实现了路由劫持和应用加载,样式隔离和 JS 执行隔离一概不管。

2019 年蚂蚁的 qiankun 基于 single-spa 做二次封装,公式可以写成 single-spa + sandbox + import-html-entry。它补上了资源加载和运行时隔离这两块,接入方式做到了接近 iframe 那种开箱即用的程度。

所以子应用能独立构建、运行时动态加载、主子应用完全解耦并且技术栈无关,靠的不是什么黑魔法,是一份协议。这里提前强调一下,子应用必须导出 bootstrap、mount、unmount 三个方法,这就是协议的全部内容。

顺着上面聊几个大家一定会问的问题。

**这不就是 iframe 吗?**并不是。iframe 的隔离确实最彻底,但它有几个硬伤绕不过去。子应用切了三层路由,用户一刷新就回到首页;iframe 内部弹一个 Modal 想相对整个浏览器窗口居中,你得靠 postMessage 通知父页面画遮罩;每个 iframe 都要重新建一整套浏览器环境,白屏时间省不掉。这几条我都踩过,弹层一多代码就很难维护了。

**应用间怎么通信?**常见的有四种路子,按耦合度从低到高排:基于 URL 传参,能力最弱但最解耦;基于 CustomEvent 做事件广播;基于 props 由主应用直接注入到子应用的 mount;再就是全局变量或者 Redux 这类共享 store。中后台项目我一般只用前三种,共享 store 一旦铺开,子应用就没法独立跑了。

**公共依赖怎么处理?**两条路,一条是 CDN 加 webpack 的 externals,把 React、Vue、UI 库从各自的包里剔出去;另一条是 webpack 5 的联邦模块(Module Federation),构建时就把依赖共享关系约定好。

# 三、路由劫持,single-spa 只干这一件事

要理解 qiankun,先得把地基那层看明白。这一节用 single-spa 从零搭一遍,你会很清楚地看到它缺了什么。

# 3.1 子应用导出生命周期

先建一个 Vue 子应用,用官方适配包 single-spa-vue 生成三个生命周期函数:

vue create spa-vue  
npm install single-spa-vue  
@前端进阶之旅: 代码已经复制到剪贴板

single-spa-vue 帮你做的事情是把 Vue 实例的创建和销毁包装成 bootstrap、mount、unmount,你不用自己手写这套样板:

// main.js

import singleSpaVue from 'single-spa-vue';

const appOptions = {
   el: '#vue',
   router,
   render: h => h(App)
}

// 在非子应用中正常挂载应用
if(!window.singleSpaNavigate){
 delete appOptions.el;
 new Vue(appOptions).$mount('#app');
}

const vueLifeCycle = singleSpaVue({
   Vue,
   appOptions
});


// 子应用必须导出以下生命周期:bootstrap、mount、unmount
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
export default vueLifeCycle;
@前端进阶之旅: 代码已经复制到剪贴板

中间那个 window.singleSpaNavigate 判断是全篇最实用的一行。single-spa 在接管页面时会往 window 上挂这个标记,有它说明当前跑在基座里,就把 el 删掉交给基座决定挂载点;没有它说明是自己 npm run dev 独立跑,直接挂到 #app。日常改子应用的样式和交互,不用每次都把基座起起来。

子应用的路由也得跟着改,base 要和主应用注册时的激活路径对上,否则跳转过去会 404:

// router.js
const router = new VueRouter({
  mode: 'history',
  base: '/vue',   //改变路径配置
  routes
})
@前端进阶之旅: 代码已经复制到剪贴板

配好之后子应用独立访问是这个样子:

子应用配置 base 后的运行效果

# 3.2 把子应用打包成库

子应用要被别人加载,就不能只输出一个自己用的 bundle,得输出成 UMD 格式的库,并且挂一个能从 window 上取到的名字:

//vue.config.js
module.exports = {
    configureWebpack: {
    // 把属性挂载到window上方便父应用调用 window.singleVue.bootstrap/mount/unmount
        output: {
            library: 'singleVue',
            libraryTarget: 'umd'
        },
        devServer:{
            port:10000
        }
    }
}
@前端进阶之旅: 代码已经复制到剪贴板

打完包之后,浏览器里 window.singleVue 上就能看到那三个生命周期方法:

打包成 UMD 后 window 上暴露的生命周期

libraryTarget: 'umd' 这一条不能省。主应用是通过 script 标签把子应用的 JS 拉进来的,只有 UMD 会在浏览器环境下往 window 上挂全局变量,换成 esm 或者 commonjs 主应用什么都拿不到。

# 3.3 主应用注册与激活

主应用先准备好挂载节点:

<div id="nav">
    <router-link to="/vue">vue项目router-link> 
    <div id="vue">div>
div>
@前端进阶之旅: 代码已经复制到剪贴板

然后手动写一个加载 script 的函数,把子应用的产物一个个拉下来:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa'

Vue.config.productionTip = false

async function loadScript(url) {
  return new Promise((resolve,reject)=>{
    let script = document.createElement('script')
    script.src = url 
    script.onload = resolve
    script.onerror = reject
    document.head.appendChild(script)
  })
}

// 注册应用
registerApplication('myVueApp',
  async ()=>{
    console.info('load')
    // singlespa问题 
    // 加载文件需要自己构建script标签 但是不知道应用有多少个文件
    // 样式不隔离
    // 全局对象没有js沙箱的机制 比如加载不同的应用 每个应用都用同一个环境
    // 先加载公共的
    await loadScript('http://localhost:10000/js/chunk-vendors.js')
    await loadScript('http://localhost:10000/js/app.js')

    return window.singleVue // bootstrap mount unmount
  },
  // 用户切换到/vue下 我们需要加载刚才定义的子应用
  location=>location.pathname.startsWith('/vue'),
)

start()

new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
@前端进阶之旅: 代码已经复制到剪贴板

跑起来是这样,点击导航时子应用被挂到 #vue 里:

single-spa 主应用加载子应用的效果

代码注释里那几行就是 single-spa 的全部短板,写在这儿是为了下一节做对照。加载文件要你自己拼 script 标签,可你根本不知道子应用打出来多少个 chunk,文件名带 hash 之后更是没法写死;样式不隔离;全局对象也没有任何沙箱机制,几个子应用轮流往同一个 window 上写东西。

还有一个必须补的动作,动态设置子应用的 publicPath:

if(window.singleSpaNavigate){
  __webpack_public_path__ = 'http://localhost:10000/'
}
@前端进阶之旅: 代码已经复制到剪贴板

这行很多人会漏掉。子应用被基座加载时,页面地址是基座的域名,如果不改 publicPath,子应用去请求自己的异步 chunk、图片、字体时全会打到基座域名上,控制台里一片 404。

fe
  • 一、微前端在拆什么,又在合什么
  • 二、从 single-spa 到 qiankun,各自补了什么
  • 三、路由劫持,single-spa 只干这一件事
    • 3.1 子应用导出生命周期
    • 3.2 把子应用打包成库
    • 3.3 主应用注册与激活
  • 四、qiankun,把缺的两块补上
    • 4.1 主应用注册
    • 4.2 Vue 子应用接入
    • 4.3 React 子应用接入
  • 五、飞冰 icestark 的另一种接法
  • 六、CSS 隔离的四条路子
    • 6.1 子应用之间
    • 6.2 主应用和子应用之间
  • 七、JS 沙箱机制
    • 7.1 快照沙箱
    • 7.2 Proxy 代理沙箱
  • 八、通信与公共依赖,落地时最容易返工的两件事
  • 总结
  • 参考

← Serverless 架构与微信小程序云开发实践总结前端监控系统从 0 到 1 搭建全流程总结 →