基础扫盲
什么是微前端
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用可以独立运行、独立开发、独立部署
微服务与微前端原理和软件工程,面向对象设计中的原理同样相通,都是遵循单一职责、关注分离、模块化与分而治之等基本的原则
微前端的优缺点
- 优点
- 可以与时俱进,不断引入新技术/新技术
- 局部/增量升级
- 代码简洁、解耦、易维护
- 独立部署
- 组织更具扩展能力,其团队更加独立自治
- 缺点
- 重复依赖
- 团队之间更加分裂
微前端应用场景
- 兼容遗留系统
- 应用聚合
- 团队间共享:其低内聚高耦合特性,使得高质量的共享成为可能
- 局部/增量升级
微前端要克服的几个障碍
- 资源的隔离:由于存在不同应用各自定义CSS和全局变量的情况,应用聚合时需要考虑彼此之间的影响。应用JS 沙箱和 CSS 隔离等相关技术,使各应用之间互不影响
- 应用的注册
- 对性能的影响:按需加载、预加载、公共依赖加载
- 应用间通信
- 应用嵌套/并行
微前端实现的几种方式
- 路由分发式微前端:即通过路由将不同的业务分发到不同的、独立前端应用上。其通常可以通过 HTTP 服务器的反向代理来实现,又或者是应用框架自带的路由来解决
- 使用 iFrame 创建容器,使用前提:不需要 SEO、拥有相应的应用管理机制
- 自制框架兼容应用
- 组合式集成:将应用微件化
- 纯 Web Components 技术构建:Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 Web 应用中使用它们
- 结合Web Components构建
- 在Web Components中集成现有框架
- 集成在现有框架中的 Web Components
如何落地微前端一体化运营工作台
qiankun 的使用
运行在主应用的子应用发起的请求配置其经过主应用的网关层,通过服务层处理其跨域、登录、转发等逻辑
微前端体系下的 plus
- 性能优化:公共资源只加载一遍;即把子应用公共依赖的大版本对齐,将所有公共依赖抽离出来,只在进入主应用时候加载这一份,由于 CDN 地址一样,那么子应用不会重复加载
- 体验的度量
- 反馈跟踪
交互设计统一模式:同类统一;舒服对齐;不常用的收起;简单不阻碍
如何分三步来探索微前端架构落地
为什么需要微前端
- 业务价值
- 内部应用太多:域名繁多,记忆成本高
- UI 风格不一致:多应用各自团队维护,风格不一致
- 多应用操作断层:例如公共头改造,需要在各应用中全量修改
- 工程价值
- 统一管理:流程机械不连贯,沟通成本高
- 模块拆分多人合作
- 发布提速
微前端可能遇到的问题
iFrame:可解决全局样式冲突、JS 污染
- 存在的问题:区块中加载子应用,子应用导航之后,用户刷新区块页面,子应用导航会回到初始状态
应用集成
- Luigi:基于 iFrame 的,不可取
- Single-Spa:SystemJs or other,是一个裸库/加载框架,样式隔离、JS执行没有任何包装,模块加载还需要借助 SystemJs 这样的库
- Qiankun:single-spa + sandbox + import-html-entry,基于 Single-Spa 进行封装,路由已经做过处理,利用 import-html-entry 实现了样式隔离
实际遇到的问题:重复复配置、DLL、AntD Modal、父子通讯、多Store 共存
应用集成模式
- 简单模式:整页覆盖渲染 + 导航器浮层
- 精细模式:提供 Content 渲染区域给子应用
如何在字节设计与实践微前端沙盒
沙盒应该做什么
iframe-困难重重
- 特点
- 站点页面被拆分为 N 个 iframe,每个iframe 单独一个域名
- 独立上下线、独立运行时
- 问题
- 难以 deeplinking
- 共用数据困难:登录身份、站内信、跨模块通信
- 存在公用代码、加载优化、运行优化等问题
沙盒像什么
像 Docker
- 开发者必须体会不到环境差别
- 运行时没有环境差异
- 服务端微服务的基石
沙盒怎么做
- 参考单核、操作系统进程模拟进程切换策略
- JavaScript 是单线程的
- 通过对路由切换的封装,模拟单进程
- 通过对事件循环封装,模拟单核多进程
- 用 Context 切换模拟线程安全
- 新沙盒即将激活时,查找当前激活中的沙盒
- 保存现场,存储 context、恢复之前的 context
- Context 切换的笛卡尔积
- 比较并切换
- 沙盒数量 N 的笛卡尔平方
- 退回“初始” context
- 恢复之前 diff 的 context
沙盒到底作什么
- CSS 干扰
- 独立开发、混合加载:HTML 标准的 CSS 作用域;Scoped CSS
- CSS module CSS in JS:DOM header
- 单核多进程的情况
- CSSStyleSheet.cssRules
- 全局变量的干扰
- Polyfill 等差异巨大:例如 generatorRuntime;组件模块化;全局的外部环境
- Identifier:let;const;class
- Configurable
- window.location
- 所有需要进程安全的对象:DOM 沙盒;cookie;localstorage
如何设计微前端的主子路由调度
微前端内核部分功能模块展开
主子应用路由调度问题
符合主子应用路由正常调度至少同时满足三个场景用例
- 由一个具体的 URL 直达 目标子应用的对应页面
- 点击子应用中的链接,跳转对应页面,且主应用路由同步
- 点击浏览器后退,正确返回上一个页面,且主应用路由同步
MVC 时代
页面状态管理发展史
**
Backbone.js 的 Router 巧妙的利用了 hash 本身作为 currentState 这个变量,封装了 hashChange 事件
官方推出的 History API 也基本覆盖了 Controller 的能力,其更强大体验更好,但其并不能完全等同于前端路由,因为它管的是页面状态,额外的可以让我们在页面状态改变时更改 url,但其实本身也支持在不改变 url 的同时改变页面状态
前端路由不是必须!
但我们希望把页面状态暴露给用户,那么就需通过 url 体现,两者形成强关联,通过 url 反馈页面状态
主应用完全不处理路由
- 主应用不根据路由调度子应用,子应用内部的状态变化按照原样反映到主应用路由
- 问题:
- 初始化无法识别路由:主应用没有处理路由,初始化进入不能确定要调用哪个路由,可能只显示主应用
- 路由冲突:如果两个子应用路由一样,就会发生冲突
子应用和主应用共享路由**
- 主应用根据路由调度子应用,子应用内部的状态变化根据特定规则反映到主应用的路由
- 切片分割,要求子应用按照一定格式定义路由,例如路由前面添加 app_name 进行分割,避免路由冲突 ```vue // 使用子应用名称来对共享资源切片 const APP_NAME = ‘cow’;
- 新的问题:需要将约定侵入子应用的逻辑(限定了子应用的路由处理方式)
- 想到的解决方式:沙箱隔离
子应用维护隔离的路由
- 隔离对全局上下文产生的副作用:Closure;Node.js Module
- 特别地,隔离 DOM / BOM 对象:
- 主应用根据路由调度子应用,子应用内部的状态变化反映到各自的沙箱中,互不干扰,但不会反映到主应用的路由中
- 新的问题:由于无法感知子应用路由的变化,主应用不能体现路由逻辑
子应用路由同步回主应用
- 子应用监听路由变化然后通知主应用:Proxy history;postMessage
- 主应用侧监听此消息:replaceState
- 主应用根据路由调度子应用,子应用内部的状态变化反映到各自的沙箱中,互不干扰,并且最终会反映到主应用的路由中
- 新的问题:
- 主应用和子应用都改变了 state,这会引起 popstate 无限循环吗
- 不会,replacestate 和 pushstate 不会触发 popstate
- 为何用 replaceState 而非 pushState
- 主应用和子应用都改变了 state,这会引起 popstate 无限循环吗
当一个页面中的 iframe 跳转后,parent 页面的地址栏、history 会如何变化
如何在大型应用中架构设计微前端方案
业务背景
业务困境
技术选型
- 巨石应用:随着页面增多,开发效率与稳定性无法满足要求
- iframe:除了用户体验问题,其实是个很不错的方案
- 框架组件:不满足一个系统的心智,同时长期维护成本高
- 微前端:在大型系统的业务场景下,体验和效率的平衡点
微前端方案
架构设计理念
- 技术栈无关:单一体系技术栈统一
- 开发体验一致:不用学习新概念新流程
- 中心化路由:路由管理、应用分发
- 独立开发部署:独立开发、独立部署
核心概念
- 框架应用:负责 Layout 以及微应用配置与注册渲染
- 微应用:按业务功能纬度拆分应用,通常是一个 SPA 应用,至少包含一个页面/路由
核心流程
路由劫持
微应用加载渲染:关注微应用生命周期的挂载与卸载,避免不卸载而产生副作用
应用通信:只建议主应用(框架应用)和子应用(微应用)之间的轻量级通信
- 数据通信
- 事件通信
样式隔离:最好基于约定低成本隔离
- 业务代码样式隔离:CSS Modules
- 基础组件样式隔离:基础组件 namespace
- shadow DOM:业务改造成本高
- 处理逃逸容器节点:未在指定 DOM 上就会无样式
- 事件机制兼容:例如 react 事件代理到 document,基于shadow DOM 会阻断事件到 host 层
- Shadow DOM 是为了允许在Web平台上本地封装和组件化,而不必依赖像
- 您可以将 shadow DOM 视为“DOM 中的 DOM”。它是自己独立的 DOM 树,具有自己的元素和样式,与原始 DOM 完全隔离
- Shadow DOM 暴露给主机HTML的元素记录了简单的 ,但在其下面有与组件相关的元素和样式,它们不构成 DOM 全局范围的一部分
- 正如其名称所示,shadow DOM始终附加到常规DOM中的元素。没有DOM,shadow DOM就不存在了
JS 隔离:沙箱隔离
微模块:从功能角度拆分,微应用是 SPA 应用接入到框架应用中;那么,微模块是什么
- 使用场景
- 多模块共存
- 模块组合搭建
- 动态渲染模块
- 微模块:多模块共存/不耦合路由/独立完成模块功能
- 微模块标准:在标准 UMD 模块规范基础上,定义 mount/unmount 生命周期
如何设计实现微前端框架-qiankun
qiankun 特点
- 简单:任意 js 框架均可使用。微应用接入像使用接入一个 iframe 系统一样简单,但实际不是 iframe
- 完备:几乎包含所有构建微前端系统时所需要的基本能力,如 样式隔离、js 沙箱、预加载等
- 生产可用:已在蚂蚁内外经受过足够大量的线上系统的考验及打磨,健壮性值得信赖
“友好”的微前端方案
- 技术栈无关:不限制接入应用的技术栈;应用之间没有隐性依赖
- 有时 我们没有限定技术栈,但应用之间依然存在耦合,消灭耦合才能做到技术栈无关
- 接入简单:最好就像 iframe 一样简单;尽可能避免旧应用改造
qiankun 认为,“技术栈无关” 是微前端的核心价值
微前端的前提,还是得有主体应用,然后才有微组件或微应用,解决的是可控体系下的前端协同开发问题 (含 空间分离带来的协作 和 时间延续带来的升级维护 ) @玉伯 · 蚂蚁金服
应用加载与切换
应用路由 与 Future State
加载主应用根据 /b 确定子应用,然子应用路由系统尚未加载,则会报错,这是懒加载会遇到的问题。Angular 社区称这个问题为 Futuer State。
- 可用解决方案:劫持路由系统做改造,React Router 等路由库可以使用其动态路由方案
- qiankun 采用方案: 社区成熟方案 Single-SPA,其已经劫持了路由并做好加载及路由切换,同时解决了应用接入问题
协议接入:只要应用实现了 bootstrap、mount、unmount 三个生命周期钩子并导出,qiankun 框架就能挂载
- bootstrap:子应用第一次挂载会执行 bootstrap 初始化
- mount:挂载,渲染应用
- unmount:应用切换走的时候执行
组合时机和应用入口
隔离和通信
应用隔离:JS 沙箱
两个沙箱环境:Global Env 外部全局环境 和 Render Env 内部沙箱环境
沙箱实现的两种方案:
Snapshot 快照沙箱
- 在应用沙箱挂载/卸载时记录快照
- 在应用切换时依据快照恢复环境
快照怎么打?
- windows diff:通过两个循环把当前环境和原有环境进行比较,全量恢复为原有环境
- ES6 Proxy:代理 windows,劫持子应用对全局环境的修改,记录子应用对全局环境的挂载、修改、删除,恢复原有环境只需反向执行这些步骤
- 这是 qiankun 1.0 的思路,缺点 是无法支持多实例(不能同时激活两个应用,沙箱之间会打架)
Proxy
- 代理沙箱内部操作,不影响 Global Env
- 将副作用局限在 Render Env 内部
样式隔离:主、子应用样式冲突 和 子应用之间样式冲突
- Dynamic Stylesheet 动态样式表
- 方案:当从子应用 A 切换到子应用 B时,删除 A 样式,挂载 B 样式
- 问题:近保证单应用模式下样式不冲突,未解决主应用与子应用的样式冲突
- 工程化手段,可靠简单最优选
- 严格样式隔离:Shadow DOM
- 存在一些问题,可能需要改造子应用解决
- 例1:弹窗组件默认是添加到 document.body 上的,那么就跳过了阴影边界跑到了主应用中,会造成样式丢失
- 例2:子应用使用的是 React 技术栈,而 React 的事件代理其实是在 document 上的,也存在问题
- 存在一些问题,可能需要改造子应用解决
- RFC:runtime css transformer,动态运行时改变 CSS
应用通信
基于 url:URL:/b/func-log?data=aaa,翻译:调用 B 应用的 log(“aaa”)
- 优点:完全解耦
- 缺点:比较弱
url 中心设计,url 决定页面展现
发布/订阅模型:应用之间不直接交互,都去事件总线注册和订阅事件,天然的事件总线就是 CustomEvent 浏览器自带的事件总线
- 基于 props:主应用传递 state、onGlobalStateChange、setGlobalState 给子应用,子应用之间也通过与主应用通信实现间接的相互通信
书单 《知行-技术人的管理之路》 《领域驱动设计DDD》 《哲学问题》 《守望者(watchmen)》 《金字塔原理》(说/想/做/表演) 《松本行弘的程序世界》