从场景倒推看字节微前端

「微物料」这一块画虚线是因为它比较偏体系建设后期(也离我们(Aeolus)目前需求比较远),在前期微前端运作刚起步阶段,是不需要实现的;

并且「微物料」的出现本质上是对微前端形态的一种转换,把微前端从 「不同页面级别应用组合,no-bundle」 转换到 「根据接口协议,可以直接加载远程的组件、函数,no-bundle」

直接模糊了 App / Page / Component (widget) / Function / Plugin 的边界;在写法上形如原生浏览器 esm import 以及 deno import (调用方式上也颇有一种后端 「远程过程调用」(RPC) 的感觉),直接拉低组件复用的门槛、远程加载的门槛,于是物料市场这种东西也顺势就会有;

// 伪代码示例,加载函数级别的微组件并执行

import foo from “https://cdn/…/foo.js”;

const foo = import(“https://cdn/…/foo.js”);

const foo = load(“https://cdn/…/foo.js”);

foo(xxx);

复制代码;

在这个场景下,简单区分下目前这几个称呼的边界

  • App - 一整个微前端应用,内部也能有很多模块、多个页面 (Page)

  • Page - 一个稍大一点有路由的微前端组件可以称为页面,如一个数据查询页面

  • Widget - 没有路由的小组件(挂件),如一个样式很独特的按钮

  • Function - 被远程加载执行的一个功能函数,如试想一下用 UMD 加载 lodash 一个 func (接口格式定义在应用外)

  • Plugin - 接口格式、执行上下文定义的比较严的函数 (接口格式由应用定义)

运行时容器

这部分就是通常狭义上的「微前端框架」做的事;

它主要是要干什么呢,大概这些事:

  • 应用加载 - 根据注册的子应用,通过给定的 url,加载约定格式的子应用入口,并挂载到给定位置

    • 部分框架是根据类似 manifest 的数据,来获取子应用注册情况以及入口地址
  • 部分框架支持和管理平台配合,运行时接受平台动态注入的入口地址 (也有框架宣称运行时注入和管理平台解耦,但实际是如果不用,就得自己实现注入逻辑)

  • JS 做入口更纯粹,用 HTML 做入口更易于旧项目改造

  • 业界目前常用两种入口格式, HTML 和 JS

  • 父子入口组合(即确定依赖关系)也有两种模式,构建时组合 和 运行时组合

  • 生命周期 - 加载 / 挂载 / 更新 / 卸载 等

    • 加载 / 挂载时做的初始化、权限守卫、i18n 语言等
  • 卸载时做清理,如卸载 script 标签、style 标签、子应用 dom 等

  • 以及路由、父子通信时做双向更新的桥梁

  • 路由同步 - 子应用的路由切换时,同步更新 url;url 跳转 / 更新时,同步更新子应用

    • 也就是对子应用做到路由等同于 url
  • 应用通信 - 是说支持父子应用之间便捷地相互通信,不像 postMessage 那样难用 (指字符串)

    • 什么,你问兄弟应用相互通信?当然大家都是用父应用作 EventHub
  • 沙箱隔离 - 为了各个应用「互补干扰」,需要把各个应用在“隔离”的环境中执行

    • 缺少隔离的话,CSS 全局样式可能 冲突混乱,JS 全局变量可能被 污染 / 篡改 / 替换
  • 这一部分业界的方案和演进比较多,下一章会展开讲讲

  • 异常处理 - 以上所有东西在报错时的统一处理,比如加载失败、或者路由匹配失败

沙箱隔离


通常在多个应用间,需要做隔离的就两个部分, JS & CSS;

JS 隔离

Snapshot

子应用挂载时,先对全局 window 变量打个快照放闭包里,再把全局 window 丢给子应用,并在子应用卸载时通过快照恢复全局 window 变量;

这是早期部分框架的做法,实际上这也并没有形成“隔离”,只是防止多个子应用互相“污染”;限制也非常多:

  • 父子子应用不能共存,一个 url 路由下整个页面都是某一子应用

  • 多个子应用之间不能共存,因为全局 window 只有一个,快照只有一个

  • 快照的方式安全性不够严格,深拷贝遇到 document / history 等都会有问题

现在已经没有这么干的了,都是用沙箱的思路。

Sandbox

Wasm VM

重新编译一个 Wasm 的 JS 解释器放在浏览器中,把子应用直接放进这个 VM 中执行;

隔离非常严格,看到过很多技术文章讲解,但目前没有调研到有实际微前端框架这么干的,

原因和大家不用 Web Worker / iframe 一样,隔离太严格了,通信非常麻烦,通信开销非常大;

(但在除微前端之外有一些用,比如 StackBlitz[4] )

with() + new Function(code) + Proxy

with 语法用于改变作用域链,这里用来拦截写访问全局变量时对 window 的查找,如直接访问 Array.from 而不是 window.Array.from 写法时;

new Function 执行 code 作用等同于 eval,但 eval 能访问到当前局部作用域变量,new Function 返回函数不管哪里执行,都只能访问全局作用域,正是我们想要的。

而 Proxy 提供的是 with 和 new Function 闭包中用到的充当 window 作用域的对象,通过白名单属性限制能访问真正 window 上的部分元素,通过 Proxy 让删除 / 添加全局变量 / api 时不会对真正全局 window 产生影响;

同时对 document / history / localtion 上各类操作做劫持,比如把 document.body 上插入元素乾坤大挪移、把 history.push 改写再同步到 url、把 localtion path 拦截让子应用只获取内部路由, 等等,这些种种限制组成沙箱环境;

// 简化伪代码示例

window = new Proxy(pick(window, whiteListProperties), { … })

document = new Proxy(document, { … })

sandbox = new Function(`

return function ({ window, location, history, document }, code){

with(window) {

${code}

}

}`)

sandbox().call(window, { window, location, history, document }, code)

但这里对 window 拦截的程度是有限的,甚至可以简单理解为「浅拷贝」而非「深拷贝」,通过全局通用 API 很容易做到逃逸而实现污染,比如直接改掉 Array.prototype.push 的行为;

with() + new Function(code) + Proxy + iframe contex

为了更安全的解决上面的 Proxy window 全局 API 逃逸问题,可以取一个 iframe 的 window 作为沙箱环境上下文的 window;

这里的 iframe 并不是直接作为沙箱来执行子应用代码,子应用依然执行在 with + new Function 中,这个 iframe 只是个创建出来的空的 same-origin iframe,唯一用途是取它的 iframe.contentWindow 对象传给子应用做 window;

因为 iframe 的严格隔离性,一切全局对象跟外层均没有任何关系(除了 parent),因此内外两个 Array Array.prototype 都不相同,等同于把上一个方案的 window 拦截做到了 「深拷贝」,是一种目前比较完善优雅的沙箱方案;

(对 document / history / localtion 的代理拦截与上一个方案无异)

// 简化伪代码示例

frame = document.body.appendChild(document.createElement(‘iframe’,{

src: ‘about:blank’,

sandbox: “allow-scripts allow-same-origin allow-popups allow-presentation allow-top-navigation”,

style: ‘display: none;’,

}))

window = new Proxy(frame.contentWindow, { … })

document = new Proxy(document, { … })

sandbox = new Function(`

return function ({ window, location, history, document }, code){

with(window) {

${code}

}

}`)

sandbox().call(window, { window, location, history, document }, code)

Realms

tc39 还在提案中的新规范 Realms[5],stage 2,可以创建完全独立的全局对象和全局作用域,用来实现沙箱正合适 (也有部分关于逃逸的讨论[6]),

目前没有调研到任何微前端框架在用,仅 Figma 提到用于自身插件方案(上文提到「微物料」化之后,插件也能纳入「微前端」范畴);

CSS 隔离

不同于 JS 隔离的相对成熟, CSS 隔离在业界完全不成熟,目前对于大部分微前端框架都是有点问题;

大部分都会告诉你,用工程化的方式加前缀来防止冲突,比如 BEM / css modules / css in js / 自定义前缀 等;

但这没法解决不同应用依赖了同一个 UI 库不同版本的情况;

并且大部分历史项目里面也有很多硬编码的 className 很难彻底改造;

切换应用时卸载

与上文提到的 JS 隔离用的 Snapshot 在应用切换时的[挂载 / 卸载原理]相同,问题也相同,不再赘述;

Shadow DOM

Shadow DOM[7] 听起来才是真正有效用于 CSS 隔离的沙箱,有着和 iframe 一样严格的 DOM 隔离,Shadow DOM 内部的元素始终不会影响到它外部的元素;

并且不管是 <style> 或 <link rel="stylesheet"> 产生的 css 在内外之间都是互不影响;

(图源 MDN)

听起来很美好,只需要给每个子应用外面套一个 Shadow DOM 就万事大吉,子应用往 head 里插入的 style / css link 都拦截到这个 Shadow DOM 内;

▶︎ …

▼ 

▼ 

▼ 

▼ #shadow-root (open)

▶︎ 

▶︎ …

▼ 

但实际上,除了兼容性、浏览器 Shadow DOM 有一堆 BUG、react-dom 低版本对 Shadow DOM 事件不支持外,还有一个问题:

弹窗遮罩

准确的说是:子应用那些通过 JS 往 document.body 上插的元素,如 Tooltip / Popover / Modal 怎么办?

他们要是真插入到 document.body 上了,就跳过了 Shadow DOM,也就没有了子应用的 CSS,样式就没了啊;

要是被 JS 沙箱的 document 劫持到了插入操作,那这些 Tooltip / Popover / Modal 元素应该插入到哪里?

如果是插入到子应用 Shadow DOM 内跟挂载 DOM 同级的位置,可能因为 DOM 结构(顺序)改变导致子应用某些样式出问题,也可能因为子应用所在区域的 位置、大小、margin/padding 跟 body 不一致,导致这个插入的元素(如 Tooltip)的定位出现偏差,毕竟不是所有插入元素都用 fixed 定位;

一种 hack 的解决办法是,在 document.body 末尾给每个子应用对应再放一个 Shadow DOM 的 div,这个 div 和 document.body 的定位、大小、margin/padding 属性都完全一样,等同于覆盖在 body 之上,并且内部完全同步了对应子应用插入的 style / css link 标签,

这个 Shadow DOM 的 div 用来承载子应用插入到 document.body 上的元素(需要 JS 沙箱配合),这样,不管是 Tooltip / Popover / Modal 还是没有 fixed 定位的元素,获取到的 css 都和子应用内部一致,并且所在位置又和 body 对齐,基本解决问题;

▶︎ …

▼ 

▼ 

▼ 

▼ #shadow-root (open)

▶︎ 

▶︎ …

▼ 

▼ 

▼ #shadow-root (open)

▶︎ 

▶︎ …

▼ 

但 hack 并不是完美的,但这里基于同步 css 的做法可能会有无法同步、遗漏,等问题;

  • 比如对 <style> 标签内部的同步需要一直监听、两个 Shadow DOM 之间需要来回同步,因为任何一个内都可能插入新的 <style> 标签,也能在原有的某个 <style>标签内修改;

  • 再比如 css in js 方案为了性能通常会使用 CSSStyleSheet.insertRule\(\) API[8] 来创建样式,这样元素虽然能受到 css 样式影响,但对应 <style> 标签内容是完全空的,基于标签内容的手动无法同步,需要 JS 沙箱配合劫持 insertRule API 来做同步;

  • 以及如果子应用通过 JS 插入 dom 的位置不是 document.body ,而是其他任何一个一有 dom 的位置,这里也很难做劫持;

技术债 !!


下面列出的是典型的技术债,不过它们清理的过程,也可以看做微前端改造过程的一部分。

  • 模块之间组件的交叉耦合

模块内引入了其他模块的内部组件 / 方法,

这些被引用项应该拆分出去成公共组件 / 方法;

(如数据准备用到标签的表达式树组件、可视化筛选器组件等)

  • 公共依赖组件/方法还没完整拆分打包

Common 公共组件 / Service 公共方法等,需要后续重构拆分发包

  • URL 路由模式改造还没做 (hash history => browser history)

之前 Aeolus 一直有需求和计划要从 hash history 改到 browser history,但还没做,

如果计划要改造,但在此之前做了微前端改造,那么之后路由改造的兼容可能更难做;

不同微前端容器对路由模式的支持程度不一样,并且对父子应用能否使用不同模式的支持也不一样;

  • React v17 升级以修复 Shadow DOM 问题

主流框架 CSS 隔离都有带 Shadow DOM 支持,而 React 需要升级到 v17 才有对 Shadow DOM 各种问题的修复;

又因为目前用的 umi, react 运行版本是由 umi 内部包控制的,所以实际上这个升级是连带着 umi 一起升级的,新版 umi 的路由懒编译等特性也需要处理。

  • 硬编码的写在代码中的 jsx className

大部分这类 className 都没有前缀,命名也很简单 (right, left, first, last …),极易造成冲突,这部分也需要改造或重构;代码里这部分有不少,还很难统计;

  • Dev / CI / CD 流程还在改造中 (monorepo / dev preformance / ci tasks)

而本身微前端的改造也对 Dev / CI / CD 流程有影响,因此两者最好不要同时做;

公共依赖复用

微前端不解决复用问题,「依赖复用」本身不是微前端框架该做的事 ,有些依赖是不能也不应该复用的(如代码的执行会对依赖本身的内部变量/context 产生副作用)(部分框架做了 npm lib 级别的复用抽取,但也会导致 bundle chunk 有问题);

用哪个框架?

技术上,用什么框架都可以,各个框架的设计基本都是宣称几行轻量级无侵入的接入方式,因此接入成本和替换成本都很小;重点是风神自己需要做完模块拆分;

实际上,我们只能选有配套治理体系服务的、研发者离我们近的;否则我们需要自己根据框架造配套治理体系以及自行排查接入源码级问题。

因此我们同时需要在改造时做好不耦合、甚至能随时替换微前端框架的准备。

要改造到什么程度?

  • 内部公共依赖该拆分的拆分,该发包的发包

  • 对应模块完整移动到其他仓库(或 monorepo 目录,如 apps/ ),并能独立启动开发 (因为能开发就能做部署了)

  • 剩下就是按照对应框架文档做打包接入

  • 具体实际改造过程和过渡阶段工程化有什么指导性方案?


Refs


把整个过程中,看的比较有关联有价值的文章列出来了

起源

Techniques, strategies and recipes for building a modern web app with multiple teams using different[9]

综述

【推荐】是对微前端的一些总体概览,包括设计演进、技术演进等

  • [Live Record][10]

  • 微前端如何设计落地 - InfoQ | Phodal[11]

  • Micro-frontend Architecture in Action-微前端的那些事儿 | Phodal[12]

  • 如何设计实现一个微前端框 QianKun - 方涣[13]

  • 微前端到底是什么?- 前端向后[14]

  • 你必须知道的 11 个微前端框架-InfoQ[15]

Webpack5 Module Federation

  • Module Federation | webpack[16]

  • Webpack 5 Federation. A Game-changer to Javascript architecture. - inDepthDev[17]

  • 精读《Webpack5 新特性 - 模块联邦》[18]

  • 三大应用场景调研,Webpack 新功能 Module Federation 深入解析-阿里云开发者社区[19]

  • webpack 打包的代码怎么在浏览器跑起来的?看不懂算我输[20]

没有沙箱,只有代码打包复用

Qiankun

  • qiankun 文档官网[21] / umijs/qiankun Github[22]

  • @umijs/plugin-qiankun[23]

  • umijs/umi-plugin-qiankun examples[24]

  • 目标是最完善的微前端解决方案 - qiankun 2.0[25]

  • 微前端的核心价值 · 语雀[26]

  • 如何设计实现一个微前端框 QianKun - 方涣[27]

  • 飞猪微前端实践:统一运营工作台的解决方案[28]

  • 阿里云开放平台微前端方案的沙箱实现[29]

  • Click event not firing when React Component in a Shadow DOM[30]

qiankun 是运行时容器、加载器,但没有解答的工程与平台问题

Magic microservices

  • github.com/bytedance/m…[31]

理念是 web components 做隔离,是纯框架容器的一层,不包含管理平台

Puzzle
  • github.com/puzzle-js/p…[32]

Bit.dev

  • Bit: The platform for the modular web[33]

  • teambit/bit[34]

  • Installing Bit | Documentation[35]

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
是 web components 做隔离,是纯框架容器的一层,不包含管理平台

Puzzle
  • github.com/puzzle-js/p…[32]

Bit.dev

  • Bit: The platform for the modular web[33]

  • teambit/bit[34]

  • Installing Bit | Documentation[35]

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-JbtuQPyt-1715813525096)]

[外链图片转存中…(img-WCK1LfHY-1715813525097)]

[外链图片转存中…(img-Mp8P9HPQ-1715813525097)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值