微前端说明书

为何写

互联网公司技术选型三定律javascript

  1. 流行即正义
  2. 新鲜即正义
  3. 复杂即正义 —— 我

由于最近被问起当前公司的前端产品有没有聚合为微前端的可能性,因此又从新开始审视“微前端”这个话题。差很少一年前写过一篇反驳美团微前端方案的文章。那篇文章更多的是关于“没有必要这么作”,可是“应该如何作”我也并无给出更好的方案。最近在参考了不少资料以后,对这个的问题的答案有了轮廓html

本文分为两个部分:“战略”和“战术”。前者关于为何以及在什么场景下使用微前端,后者关于采用什么的技术实施微前端。这篇文章里我会反对某些方案,同意某些方案,仅表明我的意见前端

开头的“互联网公司技术选型三定律”是我我的总结的,也是我在这篇文章里极力反对的。这三条定律的产生有行业的缘由也有程序员职业的缘由。三定律的存在致使了某些技术的被曲解和滥用,其中就有微前端。在本文中也会引用这三定律作说明java

战略篇

实现微前端一点都不难,我相信你也看过无数种微前端实施方案。但问题不在于咱们能不能作,而是咱们为何要作。react

Dan Abramov(你应该知道他是谁) 在 Twitter 上提出过一个问题,他认为微前端解决的问题经过好的组件模式就能解决,为何须要微前端?git

有时候甚至不用经过组件,经过一个门户网站将不一样功能的站点收集在一个页面上某种意义上也算微前端。因此咱们谈论的微前端到底是什么?程序员

微前端的概念衍生自微服务。在我看来微服务带来的改进是是架构上的解耦,好比灵活替换和独立部署发布。注意这样的解耦是架构上的而不是功能上的,在实际的的工做中,经常一个功能的迭代会带来多个微服务的链式修改。在一个恶劣设计的极端状况下,你划分了十个微服务,可是每次功能修改都须要对十个微服务同时修改,那这和一个单体应用有什么区别?在单体应用中若是你设计的足够优秀,单体内部也能够存在好的功能解耦。因此在当今微服务做为标配的状况下,微服务也并非绝对正义github

微前端和微服务类似,它带来的也是仅仅是架构上的解耦。关于功能解耦我在战术部分详述npm

组件化的确是目前前端广泛的开发模式,但并非全部的前端功能都须要走组件化这一条路。好比文档性质的站点能够经过 static site generator 生成;绚烂的活动页面更适合利用动画特效类库进行编程。我想表达的是:微前端不是跨世代的通用解决方案,它也不是用于代替先用的组件模式。它只是给了咱们一个让不一样技术栈不一样团队开发同一个产品的机会。这个定义来自于 Luca Mezzalira 对 Dan Abramov 质疑的回复,我很是赞同:编程

Let’s start with this, Micro-frontends are not trying to replace components, it’s a possibility we have that doesn’t fit in all the projects like components are not the answer for everything.

微前端适用于不一样技术栈不一样团队须要对同一产品进行修改的开发模式,好比 Google Cloud:

从菜单栏咱们能够看出谷歌云提供不一样类型的服务,可是这些服务之间都相互独立,有的是通用性质的,有的是云计算相关的,即便是在云计算一栏下又划分了不一样类型的计算服务。(我猜想)不一样的服务来自不一样的团队进行开发,虽然它们不相互干扰,可是又须要同一个产品予以体现。那么使用微前端是最好的方式

注意这里“同一产品”的定义仅仅是从视觉形态和用户体验方面考虑。若是 A 网站只是要用到 B 网站的数据,那么经过接口提供就行了。

你可能会注意到腾讯旗下的(全部)站点的登录框都是使用 iframe 集成。这也算是一种微前端:其余的团队只负责本身业务相关的页面,而“登录框”团队负责维护统一登录框供你们调用。他们之间不须要关心对方的技术栈,迭代周期(甚至甩锅也变得方便了)。若是有一天 iframe 变成了统一的 Web Component,这种微前端关系仍然成立

美团的的微前端方案里,咱们看看他们作微前端的诉求:

美团已是一家拥有几万人规模的大型互联网公司,提高总体效率相当重要,这须要不少内部和外部的管理系统来支撑。因为这些系统之间存在大量的连通和交互诉求,所以咱们但愿可以按照用户和使用场景将这些系统汇总成一个或者几个综合的系统。

由于美团的HR系统所涉及项目比较多,目前由三个团队来负责。其中:OA团队负责考勤、合同、流程等功能,HR团队负责入职、转正、调岗、离职等功能,上海团队负责绩效、招聘等功能

这种团队和功能的划分模式,使得每一个系统都是相对独立的,拥有独立的域名、独立的UI设计、独立的技术栈。可是,这样会带来开发团队之间职责划分不清、用户体验效果差等问题

这里我对他们要作微前端的动机感到有一些疑惑:

  • “系统汇总”的方式有不少,除了门户之外,咱们曾经尝试过经过给每个系统添加一个公共共享的导航栏组件来让系统之间的导航和跳转更方便,效果也不错
  • “独立的域名、独立的UI设计、独立的技术栈”——这不就是相互独立的站点吗?若是这么多年用户都能正常使用,为何如今要把它们聚合在一块儿?
  • “这样会带来开发团队之间职责划分不清、用户体验效果差”,我不认为微前端可以解决团队之间职责划分的问题;用户体验效果不是更应该从交互体验和统一设计规范入手吗?

我不是针对美团,但就微前端而言,就事论事我认为这是一个好的反面例子,可以让咱们从不一样的角度进行反思。在后面的内容里我也会再引用其中的内容。固然他们也不是这篇文章中惟一的反面教材。

我在阅读 Martin Fowler 的 《Patterns Of Enterprise Application Atchitecture》时,最大的一点感触是他历来不排斥任何的技术方案:若是你想作业务相关的数据存储,你固然能够选择 ORM 来实现 Domain Model 模式;你一样能够选择简单至极的 Transaction Script 模式 (A Transaction Script organizes all this logic primarily as a single procedure):

However much of an object bigot you become, don’t rule out Transaction Script. There are a lot of simple problems out there, and a simple solution will get you up and running much faster.

不少人(曾经包括我本身在内)在技术选型方面喜欢追求一种“宏大叙事感”:若是技术不够复杂,不够新,开发周期不够长,动员团队不够多,怎么在公司内彰显个人影响力?咱们之因此敢这么放肆是由于环境鼓励咱们这么作,每一个团队都在这么作。咱们一直在被暗示,项目的风险和可维护性不重要,反正三年以后我也不必定在这个公司,三年以后我可能成了管理人员,三年以后接手维护系统的人不是我。不管如何三年以后项目必定会推倒重来。咱们的简历上老是强调咱们作过多少系统,而不是把它们作的多好

从职业素养的要求上说做为开发人员咱们应该关心风险和可维护性。减小项目风险和增长可维护性的措施之一就是让代码变得简单。微前端从本质上说只是给了咱们一个解决选项而非标准答案。若是你有留意的微服务的发展趋势的话,微服务生态已经很是的庞大,几乎每个环节都能找到对应的第三方组件来完成工做。微前端也同样,若是你愿意你能够找到无数种方案让项目看上去高精尖,可是为何明明一个 React 就能解决的问题必定要上 React 全家桶才甘心。

准确且谨慎的使用微前端,这是个人建议

Dan Abramov 提出的另外一个质疑是多种技术栈混合的产物实际上是一种互不妥协的结果:

首先我不认为应该禁止游戏混用引擎,好比某个 3D 游戏的某个回忆复古关卡须要横版射击的表现形式,那么就应该使用横版射击引擎,引擎应该忠于游戏的展示。其次,前端框架与游戏引擎不一样,游戏引擎不只决定了物理特效,还影响了画面展示,可是前端框架只是决定了运行机制,真正的用户体验一致性取决因而否统一 UI 设计与用户体验

战术篇

我没法告诉你某个方案是最完美的。评价是一件多维的事情,这其中甚至还与你的团队规模有关,因此我只是把这些可能的方案的 pros 和 cons 一一列举出来。我也不会陷入到具体的技术细节中去,例如如何在 CSS 中避免 class 污染,像我以前说的,实现历来都不是问题,问题是咱们为何要去实现。

Web App 隔离

让咱们从最简单的 case 开始。

在美团的例子中,微前端是由三个团队独立开发的 Web App 组成,此时 App 是微前端架构里的最小粒度。这样的划分和隔离是最安全的,由于 App 间几乎没有任何的从人到代码的资源共享。

重复代码

但这样的独立策略难免让人担忧代码的重复:假设团队 A 使用 React 技术栈开发了一个 Dialog 组件,团队 B 也使用了 React 技术栈也开发了一个 Dialog 组件,那么貌似这那个 Dialog 可以合并成一个 Dialog 来减小维护成本。

这的确符合常识,咱们耳目濡染的接收了不少关于不要作重复代码的熏陶:好比 DRY(Don't Repeat Yourself) 原则,DIE 原则(Duplication is Evil).在绝大多数状况下它们都是正确,但在微前端中并不是如此。“微 (Micro)” 这个词并不是仅仅是字面上“小”的意思,而是表明独立和自治。以 Dialog 为例,不一样的 App 隶属于不一样业务,不一样的业务对 Dialog 功能有着不一样的需求。每一个小团队对本身的业务才是最熟悉的,若是须要对 Dialog 进行变化的话他们可以对本身维护的 Dialog 准确快速的作出决定。把不一样团队的 Dialog 合并成为一个以后,看似代码量减小了,可是期间的沟通成本和维护成本反而增长了。原本是为了解耦架构的微前端由于组件共享又被耦合在了一块儿。

即便不站在微前端的角度上,我依然不推荐抽象共享组件。抽象最好应该是在项目稳定的后期,看到了确切的功能重叠部分,再考虑把它们共享出来。由于在需求快速变化的前期,不一样业务的需求会致使共享组件变成并集而非交集的结果。

最后抽象并不是是无敌的,前提是你要知道如何抽象,错误的抽象比重复代码维护起来还要难受

编排层

隔离方案中另外一个须要解决的问题是应用的启动和切换,此时咱们须要一个相似于 Orchestration Layer(编排层) 的东西。它负责协调不一样的 App 之间的活动,好比:

  • 管理 App 的生命周期
  • 加载、卸载 App
  • 微前端路由管理
  • 提供公共功能

编排层不是什么新鲜的东西,在 SOA 架构中就已经存在。你能够把编排层和 App 理解为 steam 平台和平台上游戏的关系,也能够把 BFF 看成针对接口的编排层。在美团的方案中,编排层就是他们口中的 Portal 项目。

可是我反感方案美团 Portal 方案的关键缘由是,编排层对 App 代码进行了入侵。好比:

为了避免侵入“子项目”,咱们采用构建过程当中替换的方式来作,“Portal项目”把公共库引入进来,从新定义,而后经过window.app.require的方式引用,在编译“子项目”的时候,把引用公共库的代码从require('react')所有替换为window.app.require('react'),这样就能够将JS公共库的版本都交给“Portal项目”来控制了

这段话自相矛盾:段落的开头说“为了避免入侵子项目”,结尾则说“这样就能够将JS公共库的版本都交给 Portal 项目来控制了”。这样一来,微前端中最宝贵的独立技术栈的优点被削弱了,全部 App 的公共类库都要交给 Portal 控制。

若是它们指的是 Java 的 Portal 概念的话,我以为再这里也不适用,由于 Portal 指的是动态碎片聚合成单个网页:

A portlet is a Web-based component that will process requests and generate dynamic content.

在这里我想特别的强调编排层的职责,编排层不是 manager,它相似 broker、coordinator 甚至 glue。编排层是为 App 服务,而不是 App 为编排层服务。你不会见到 BFF 对上游的后端接口提需求;你也不会见到 Application Layer 对 Domain Layer 指手画脚。

关于编排层另外一点我想强调的是,编排层不局限于在 client 端实现,咱们也能够拥有 server 端的编排层。例如当用户从应用中登出以后,由后端返回一个包含须要登录的页面,而前端则不须要再关心权限控制。这实际上是回到了传统 MVC 的那一套。若是选择 server 端的编排层,一方面咱们能够考虑用上 server rendering;另外一方面咱们也须要担忧 App 间数据共享的问题

以组件为单位

以组件为单位聚合成微前端是目前你能看到的主流理想的实现方式。

  • 为何说是主流?由于你能搜索到的绝大部分关于微前端的实现案例,都是基于组件化的。
  • 为何说是理想?由于这些案例一般是来自某一位开发人员之手,而非是某个团队实践以后的结果。

若是你在 Google 上搜索 Micro Frontends, 排名靠前的是一个名叫 Micro Frontends 的开源项目。项目里举了一个例子,来描述用组件聚合微前端的一个场景,在这个挑选商品的页面中,它须要调度三种框架来编写组件来协同完成工做:

咱们就以这个 case 为例,看看以组件为单位的微前端须要解决什么问题

Communication

通讯是头等大事。在上面的例子中,当用户在产品列表中选择不一样类型的玩具时,须要通知购买按钮的价格进行调整。然而项目做者在父子组件通讯实现方案中选择直接修改购买按钮对应的 DOM:移除旧 DOM、插入新 DOM 或者修改 DOM 属性。在这个开源项目中,做者认为 DOM 就是组件间相互通讯的 API 。

我支持做者后半段叙述的使用 DOM Event 来进行子组件到父组件以及同辈组件之间的消息传递。可是直接修改 DOM 绝对是一个很是糟糕的设计。直接修改 DOM 比如我直接经过 IP 访问网站,比如 React 父组件经过找到子组件的 DOM 来修改子组件。不只耦合性强,之后每增长一处须要感知变化的组件时,都要在父组件中添加代码。可是经过事件,我只须要添加消费方便可。

Synchronization

仅仅是消息机制每每是不够的,有时候咱们将数据状态进行同步。假设如今须要支持用户勾选多个商品并统一进行结算,且支持优惠满减活动。此时购物按钮组件须要存储目前购物总金额,才能计算出优惠以后的金额。

此时我想到三个办法:

  1. 保持原状,用户选择商品时依然发布事件。购物按钮接收到事件后经过 Event Sourcing 计算当前总额
  2. 商品列表追踪当前商品总额,并在用户操做商品时经过事件同步给购物按钮
  3. 用户选择商品时发布事件,不过由全局存储的模块接收事件并计算总金额。购物按钮从全局存储模块得到当前总金额

方案一的缺陷在于,若是有多个组件同时须要知道当前总额时,多个组件须要重复相同的工做,一份相同含义和价值的数据会存储被存储多份

方案二的问题在于,商品列表在业务逻辑上来讲是不须要知道商品总额的,模块的职责划分出现了错误

因此目前看来方案三才是最佳的选择

Package Manage

通讯和数据共享都没法回避一件事情:契约。不管是组件间直接通讯仍是经过 event 进行通讯,它们都须要和对方预约消息格式;须要共享数据的组件之间也须要约定数据的 schema。不管组件如何的迭代,契约始终要和其余组件保证一致。

由于组件之间独立的缘故,不一样的组件迭代节奏不尽相同,天然组件间就会出现版本差别。然而如何保证不一样版本间的契约不会被破坏?文档能够,契约测试也能够。然而更大的问题是,如何保证组件协做产生的功能不被破坏?独立组件或许有测试可以覆盖到本身的功能,但这不意味着合并以后的功能依旧正常,因而在 App 中,咱们彷佛还须要端到端的测试来保证交付功能的正常

若是团队若是真的独立开发组件的话,我建议在组件的发布阶段加上 pipeline,持续集成以免影响其余功能

Responsibility and Team Work

使用组件聚合最(令我)头疼的问题之一,是如何为组件找到对应的团队负责,以及如何在组件聚合的模式下划分团队。

团队划分一般有两类划分模式,这两种模式的叫法有不少,我在这里姑且称之为 Component Team(如下简称 CT) 和 Feature Team(有如下简称 FT)

  • Component Team: 康威定律告诉咱们组织的沟通方式会在系统设计上有所表达。若是你有四个小组开发编译器,那么你会获得一个四步编译器。CT 模式即组织和架构一致。在这个模式下团队的划分是按照分层架构或者说垂直技术栈进行划分的,例如前端、后端和运维。CT 模式的问题首先在于领域知识散落在不一样的技术架构中,产生了耦合;其次在须要协同工做的状况下缺乏 ownership,每一个团队只关心本身的KPI,缺乏知识的共享和传承

  • Feature Team: 这个模式也被成为逆康威模式(Inverse Conway Maneuver),团队按照业务架构而非技术架构进行划分,一个团队负责单一业务上的功能,可是在技术上,它们能够须要同时修改端到端的代码以及多个微服务,你能够理解为全栈。这个模式的问题是,在容许多个团队修改同一个服务的状况,缺乏服务 owner 容易致使服务代码的质量降低

在敏捷开发和 DDD 的影响之下 FT 模式逐渐变得流行。我我的也推荐 FT 模式,由于我曾在某司深受 CT 模式其害,当组织越庞大,垂直的组织壁垒就越多,你能想象我在某司的时候运维部门的最大愿望是但愿咱们不要上线吗

然而在使用组件聚合的状况下,咱们应该如何划分组件和团队?

首先我不同意上面例子中如此细粒度的划分技术栈的划分组件和团队。这样的会致使每一个如此之小的功能的修改都要涉及好几个(未知)团队的协做开发,CT 模式下的壁垒又从新显现。我更加反对将 componets 在公司内部做为独立的组件库由独立的团队进行开发,这会致使业务团队与组件团队没法对齐。

那 FT 模式呢?当我在设想以 FT 模式进行组件划分时,我又陷入了一种粒度的纠结当中。以上面选择商品而且结算为例,该团队负责的范围就此为止了吗?若是下方还有商品评论和商品推荐的相关内容,我是否应该继续交给这个团队继续负责?

我认为 DDD 是多是一个解药。DDD 理论可以帮助咱们划分出不一样的领域模型,帮助咱们界定上下文。好比商品的购买属于核心域,可是商品的评论属于支撑子域。这样咱们就有理由不将它们交给一个 FT 团队负责。这样前端团队和后端也方便对齐。

注意在 DDD 的模式下请避免组件的跨域复用,这会致使上下文和领域的重叠。

另外若是以 DDD 划分的话,说不定由于范围够大而致使组件聚合升级成了 App 聚合

以组件为聚合的解决方案

很多微前端解决方案基本都是以组件为划分的,不过它们定义的组件和咱们理解的组件并不相同。最终的解决思路又十分类似

IKEA

你没看错,宜家

Experiences Using Micro Frontends at IKEA 一文中,宜家架构师 Kotte 介绍它们采用了一种相似于 transclusion mechanism 的形式。客户端 transclusion 的例子即是图片标签。标签拥有 src 属性用于指向一个 URL。浏览器会在渲染时将改标签替换为一个真实的图片。

在服务端他们的 Edge Side Includes(ESI) 便对应图片标签,不过指向的不是图片而是 HTML。他们拥有页面 (Page) 和碎片 (Fragment) 的概念,一个团队同时须要负责碎片和页面的开发,页面经过 ESI 引用那些碎片。碎片的引用是跨越团队边界的。好比一个产品团队拥有产品缩略图的的碎片,其余的团队就能够引用这个缩略图碎片而不用本身再重写相同的功能。

由于页面由不一样团队的的碎片组成,可能使用的不一样技术,为了可以使它们组件时相互兼容,团队采用了一种自包含(self-contained)技术,即碎片自己就包含了它本身须要的全部资源,好比 CSS 和 Javascript,可以独立运行,而不须要思考碎片的依赖。

OpenComponents

OpenComponents(如下简称 OC) 是一种端到端的解决方案。在关于 OC 的Architecture overview中,项目开宗明义的指出:

OpenComponents' heart is a REST API. It is used for consuming and publishing components.

你能够把它理解为一个进阶版的 npm 系统。除了是独立的组件包以外,它还封装了业务请求,甚至已经渲染完毕,加载即用,不须要再二次开发。若是你须要在页面上引用使用一个缩略图功能的 OC, 只须要在页面引用

<oc-component href="http://localhost/thumbnails">
复制代码

目前解决微前端的另外一个思路是将前端是站在消费者的角度考虑聚合:可能模块 A 是由 React 编写,模块 B 是由 Vue 编写,不要紧在服务端统一编译成浏览器须要 html 与 es5 碎片返回,最终将它们组合再一块儿,对于编排层来讲一视同仁。OC 是这个思路,宜家也是这个思路,

Project Mosaic

Mosaic 是整套的从微服务到微前端的解决方案。从它官网的图例即可以理解它的架构:

它也是经过组件化加碎片化的方式聚合前端

这三种方案都没有明确说明如何解决我上面提出的各类问题。

结语

最后借用 Simon Brown 的一条 twitter 来结束这篇文章:

I'll keep saying this ... if people can't build monoliths properly, microservices won't help.

若是你连单体应用都写很差,微前端也帮不上什么忙


本文同时也发布在个人知乎专栏,欢迎关注

相关文章
相关标签/搜索