基于微前端的前后端结合分层设计(设计篇)
本文还有许多细节未触及、未考究,将持续更新
前文介绍了微前端的原理、现状以及提出了前后端结合的微前端分层设计。本篇将针对这种架构,尝试更深入细节,设计一个微前端框架。
前后端分层
前后端分层其实是有多层的:
- 后台
- 前端(根元素改变,类似路由
- 前端(根元素未改变,页面中某个子元素改变了
这就造成了在服务注册,沙盒上,根据不同层次,有些许不同
运行时隔离
运行时隔离有两个方向可以走:1 是帮助代码进行编译期隔离,自然而然运行时隔离,
- 使用变量混淆&加前缀 or 类似模块化的闭包:
- 在有打包器(webpack 等)的前提下简单易行,webpack 加个 loader,根据业务子应用的标示加前缀/混淆
2 是沙盒,在运行期切换上下文来做到运行时隔离: 存储上下文,切换上下文,详细可参考[2],有一点需要注意,不切换页面(第 2 层),与页面相关的沙盒(dom, css)是没有意义的
当然事情不是非黑即白的,我们可以在这两者间进行取舍。
首先确定会发生冲突的有哪些:
- js 变量
- css
- dom
- 存储
- 依赖
- polyfill
如果应用运行时切换,上述场景皆可以在运行时拥有自己的上下文,但是
- 如果需要获取另一个上下文中的内容,如需要获取 dom,如何获取?
- 但是否会太重了?要去支持某种切换是否也可以作为插件进行?
对于 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 会导致调用栈问题,思考了一下,同步会造成阻塞,还是建议使用异步。
应用版本
有三处与应用版本相关的之处:
- 在服务注册时,应注册应用的版本
- 在子应用 load 时,查找对应的 entry 地址并返回
- 可选,在通信时,确定究竟是哪个应用注册的事件
如果将应用版本设计为插件,对于
- 后台服务&服务注册时可提供可选字段及默认选项
- 可选,通信提供可选字段及默认选项
其他插件
- 对于某种框架(react, vue, angular, jquery 等)的 load 插件
- 后台服务注册,服务发现的插件
- …
依赖控制
依赖的控制分为两部分,一部分是外部依赖的管理,一部分是上述插件的注入
依赖管理
各应用分布自治,且框架允许不同版本,不同依赖运行时隔离,必然会带来依赖的管理问题。
需要人去管控依赖,各团队将所需依赖统一上报,集中讨论依赖及版本。将各个依赖皆提供为依赖子应用的形式,业务子应用在其入口声明依赖该依赖子应用即可。声明后的依赖将会挂载在对应根 dom 的 head 中。
需要退化机制,即子应用在加载时,先确认全局环境下是否有此依赖,如果没有则加载,如果有则无视之。这个步骤也可以在框架内实现:子应用启动时声明依赖,如果依赖存在,切换上下文时进行保留;如果依赖不存在,业务子应用自行挂载,并且通知框架依赖该依赖的元素。
这种方式不失为一个不错的解决方案,但有两个问题,一是,对开发不友好,业务子应用无法脱离全局环境独立开发、运行、测试。二是在团队内部这种方式可行,但在跨团队时可以说必然会出现问题。
框架只能提供允许团队这样操作的能力,但无法处理此类问题。
依赖注入
拿通信举例,在主应用声明通信依赖,加载 bus 子应用,内含 bus 实例;业务在闭包外子应用声明,在 mount 时,框架在闭包的参数中为其注入 bus 实例。
结束语
暂时能想到的就这么多,里面可能存在一些问题,也有很多未提及的地方,后续慢慢补充。各位看官见谅。