基于微前端的前后端结合分层设计(设计篇)

本文还有许多细节未触及、未考究,将持续更新

前文介绍了微前端的原理、现状以及提出了前后端结合的微前端分层设计。本篇将针对这种架构,尝试更深入细节,设计一个微前端框架。

前后端分层

前后端分层其实是有多层的:

  1. 后台
  2. 前端(根元素改变,类似路由
  3. 前端(根元素未改变,页面中某个子元素改变了

这就造成了在服务注册,沙盒上,根据不同层次,有些许不同

运行时隔离

运行时隔离有两个方向可以走:1 是帮助代码进行编译期隔离,自然而然运行时隔离,

  • 使用变量混淆&加前缀 or 类似模块化的闭包:
  • 在有打包器(webpack 等)的前提下简单易行,webpack 加个 loader,根据业务子应用的标示加前缀/混淆

2 是沙盒,在运行期切换上下文来做到运行时隔离: 存储上下文,切换上下文,详细可参考[2],有一点需要注意,不切换页面(第 2 层),与页面相关的沙盒(dom, css)是没有意义的

当然事情不是非黑即白的,我们可以在这两者间进行取舍。

首先确定会发生冲突的有哪些:

  • js 变量
  • css
  • dom
  • 存储
  • 依赖
  • polyfill

如果应用运行时切换,上述场景皆可以在运行时拥有自己的上下文,但是

  1. 如果需要获取另一个上下文中的内容,如需要获取 dom,如何获取?
  2. 但是否会太重了?要去支持某种切换是否也可以作为插件进行?

对于 1 问题,仔细想想,除 dom 之外似乎并没有必要性。恰好 dom 的沙盒实现我也没想清楚(延迟函数,切换页面后执行?),等后续想明白的再做补充。

对于 2 问题,要分场景讨论:

js 变量

  • (optional)对于一般变量,如果不以 var 在全局声明,或者已经处于某个模块内,无须处理
  • 对于全局变量,或者现有的 BOM 等对象,运行时切换很必要,然而这里有 location 的坑[1](我并未深入研究
  • 沙盒:任意两个子应用皆需要沙盒

css

  • (optional)是否有运行时切换完全看是否需要,可以以 loader 帮助前缀的形式解决

dom

  • (optional)事件:加前缀完全可以解决
  • (optional)元素:除标签选择器之外,皆可使用直接加前缀来解决;标签选择器可以约束当前微前端父 dom 元素来解决

存储

(optional)存储的 key 也可以加前缀

依赖

(optional)虽然各个业务子应用有依赖的控制权,但依赖一定是可以全局管控的,无非是想不想/方不方便/应不应该管控,详见依赖管理

polyfill

polyfill 情况比较复杂,一方面,去修改了现有对象的原型,与 js 变量第二条相似,需要运行时切换。另一方面,又与依赖相似。可以管控。

如果内部存在规范并且能执行规范,则不需要运行时甚至 loader 也可以;不过头条的艾石光大佬在视频中说到:

规范本身没有什么意义,只有能够在工程上 enforce,才有意义

在我有限的开发管理经验中,我认为这句话是正确的。(@xiaoluoHe 说:也有意义,不会写的人看下该怎么写,哈哈)。

综上,可选项皆可以插件化,必须处理的只有全局变量,全局对象足以。

服务注册与发现

服务发现头条的文章讲得也很详细了。ref
前后结合,向中心注册时,即注册到后台的微服务,这样的一个好处是,后台承担服务发现的工作,前端只需要 load 服务即可。
而单纯前端不需要注册,只需要在用的时候 load 服务即可,load 过程会向后台查询所需的微服务及其 entry。

路由

前一篇提到,路由不是必须的,有后台存在的情况下大可以把路由工作交给后台,
实现路由的方参考面试官: 你了解前端路由吗?

将路由与前后端分层-第二层相关联可能是一个帮助进行 dom,css 沙盒隔离的好办法

如果把路由设计为插件,需要在主应用声明路由依赖及声明路由

应用销毁

除了使用路由进行沙盒隔离,我们还可以采用销毁业务子应用的方法,如果当前页面已经被其他子应用占据,子应用可以自行保存上下文(如存储 or 后台),并直接销毁当前应用。这种方式与生命周期钩子有关。

通信

这里主要讨论下以下 2 点:

CustomEvent vs EventBus

蚂蚁的 kuitos 大佬在微前端方案正确的架构姿势提到

比如在通信机制的设计与选择上,尽量基于浏览器原生的 CustomEvent api,而不是自己搞的 pub/sub。

这里有些不同想法,之前遇到过一个坑,即CustomEvent传参会经历序列化和反序列化,导致某些对象(比如 window)/方法是无法传过去的。

EventBus通信作为插件貌似比较简单,只需要在不同的微服务中引入同一个EventBus实例,将其挂载在全局即可,但多少都有少许不雅观或者见依赖注入

EventBus 调用栈跟踪

TODO·

在看这篇 v8 异步博文的时候想起来,用异步 eventbus 会导致调用栈问题,思考了一下,同步会造成阻塞,还是建议使用异步。

应用版本

有三处与应用版本相关的之处:

  1. 在服务注册时,应注册应用的版本
  2. 在子应用 load 时,查找对应的 entry 地址并返回
  3. 可选,在通信时,确定究竟是哪个应用注册的事件

如果将应用版本设计为插件,对于

  1. 后台服务&服务注册时可提供可选字段及默认选项
  2. 可选,通信提供可选字段及默认选项

其他插件

  • 对于某种框架(react, vue, angular, jquery 等)的 load 插件
  • 后台服务注册,服务发现的插件

依赖控制

依赖的控制分为两部分,一部分是外部依赖的管理,一部分是上述插件的注入

依赖管理

各应用分布自治,且框架允许不同版本,不同依赖运行时隔离,必然会带来依赖的管理问题。

需要人去管控依赖,各团队将所需依赖统一上报,集中讨论依赖及版本。将各个依赖皆提供为依赖子应用的形式,业务子应用在其入口声明依赖该依赖子应用即可。声明后的依赖将会挂载在对应根 dom 的 head 中。

需要退化机制,即子应用在加载时,先确认全局环境下是否有此依赖,如果没有则加载,如果有则无视之。这个步骤也可以在框架内实现:子应用启动时声明依赖,如果依赖存在,切换上下文时进行保留;如果依赖不存在,业务子应用自行挂载,并且通知框架依赖该依赖的元素。

这种方式不失为一个不错的解决方案,但有两个问题,一是,对开发不友好,业务子应用无法脱离全局环境独立开发、运行、测试。二是在团队内部这种方式可行,但在跨团队时可以说必然会出现问题。

框架只能提供允许团队这样操作的能力,但无法处理此类问题。

依赖注入

拿通信举例,在主应用声明通信依赖,加载 bus 子应用,内含 bus 实例;业务在闭包外子应用声明,在 mount 时,框架在闭包的参数中为其注入 bus 实例。

结束语

暂时能想到的就这么多,里面可能存在一些问题,也有很多未提及的地方,后续慢慢补充。各位看官见谅。

参考文档

  1. 前端微服务在字节跳动的打磨与应用
  2. 字节跳动的微前端沙盒实践