云与性能测试

近年来,随着云计算技术的发展和各种诸如AWS、GCP、阿里云等云平台的日趋成熟,越来越多的的用户选择把系统搭建在云端,因此云测试的概念随即产生。云测试看字面意思就是关于云计算、云平台的测试,而它大体又可以分成两种类型:测试云(Test Cloud)和用云测试(TaaS)。 测试云,顾名思义测试的目标是云,测试者通过设计测试去保证云平台本身和部署在云端的应用正确性。而用云测试是指利用搭建在云端的测试服务 — TaaS (Test as a Service)来进行更高效的测试。云计算有着超大规模、虚拟化、高可靠性、高可伸缩性和按需服务等诸多优点,但平台的特殊性也给测试带来了新的挑战和机遇,其中性能测试受其影响颇深,本文旨在针对云测试的两种类型探讨云与性能测试。

测试云

云环境最大的特点就是能够通过高伸缩性按需为用户分配资源,也正是因为这个特点,我们对于基于云平台的性能测试与普通系统性能测试的最大的区别就是要考虑测试云服务的伸缩功能,因为云服务的伸缩功能可能存在以下风险:

  1. 云服务的auto scaling 不能执行
  2. Auto scaling会造成服务崩溃
  3. Scaling up时资源不足

因此我们设计的测试应该包含以下内容:

测试目标

  1. 确认auto scaling能够根据所制定的策略执行
  2. 确认auto scaling能得到相应的资源
  3. 确认云服务的性能能够满足不同的压力变化

测试方法

给云端系统一直施加压力到性能边界值后继续加压,随后给系统减少压力,观察系统在边界值前后的性能表现。

测试技术

  1. 利用load profile进行施压
  2. 边界值分析
  3. Process cycle test

测试步骤

  1. 给系统施加压力,压力不会超过性能边界值
  2. 维持压力一段时间,确保系统在压力下的错误率在可接受范围内
  3. 给系统施加更多的压力,使系统超过性能边界值,我们期望系统会自动scale up
  4. 系统scale up之后,我们期望response time会下降,但是不会触发scale down
  5. 维持压力一段时间,确保系统在scale up后一段时间内的的错误率在可接受范围内
  6. 降低系统的压力到性能scale up前的状态,确保系统可以自动scale down
  7. 系统scale down之后,我们期望response time会有上升但不会重新引起scale up

期望结果

TaaS

在TaaS出现前,性能测试一般都是在本地的测试环境中通过几台电脑对被测环境加压进行的,在这种模式下,测试环境的搭建和维护不仅要耗费大量资源,而且测试环境由于并不能完全模拟真实生产环境以至于测试结果存在一定的局限性。而云计算出现后,一些基于云端性能测试服务(CLT – Cloud Load Test)相比于本地的性能测试展现出了很多优点:

  1. CLT更简单,大多数情况下, 云端的资源更好管理,环境更容易搭建,用户只需设置简单的一些参数或者提供简单的测试脚本就能在云端执行测试。同时,很多CLT工具允许用户把不同性能测试工具的自动化脚本如Gatling,Jmeter,Selenium直接移植使用,为用户节省了二次开发时间。
  2. CLT工具可以提供更多的load generator,压力测试中用户通过在云端启动load generator对被测系统进行施压,因为在云端可以调用资源体量巨大,因此用户可以完全模拟生产环境中可能面对的超大压力。
  3. 测试成本低。CLT提供的测试服务是按照云端机器的运行时间进行收费。在没有测试需求时,用户并不用为机器的运行和维护买单,大大降低了用户实施性能测试的成本,为一些没有大型长期性能测试需求的企业节省了许多开支。
  4. CLT提供的测试硬件资源大多分布在全球不同区域,在进行性能测试时,用户可以根据可能的实际情况选择不同区域的机器定制化的为被测系统加压,所得的测试结果由于更接近真实的网络情况而更加准确。

目前市面上可以提供CLT的的产品很多,他们都有着自己不同的优点,比如SOASTA提供全面的云测试服务,功能强大,但收费较高,又比如最新技术雷达上新增的Flood IO也是一款简单好用的CLT服务,其优点在于允许客户把已有的Selenium,Gatling或者Jmeter的测试脚本直接在其平台上使用,为客户节省了许多迁移成本。现实中,大家可以通过自己项目的具体需求选择适合自己的云测试平台。


更多精彩洞见,请关注微信公众号:思特沃克

Share

微网关与服务啮合

技术雷达:现在越来越多的大型组织在向更加自组织的团队结构转型,这些团队拥有并运营自己的微服务,但他们如何在不依赖集中式托管的基础架构下,确保服务之间必要的一致性与兼容性呢?为了确保服务之间的有效协作,即使是自组织的微服务也需要与一些组织标准对齐。服务啮合(SERVICE MESH)在服务发现、安全、跟踪、监控与故障处理方面提供了一致性,且不需要像API网关或ESB这样的共享资产。服务啮合的一个典型实现包含轻量级反向代理进程,这些进程可能伴随每个服务进程一起被部署在单独的容器中。反向代理会和服务注册表、身份提供者和日志聚合器等进行通信。通过该代理的共享实现(而非共享的运行时实例),我们可以获得服务的互操作性和可观测性。一段时间以来,我们一直主张去中心化的微服务管理方法,也很高兴看到服务啮合这种一致性模式的出现。随着linkerd和Istio等开源项目的成熟,服务啮合的实现将更加容易。

新瓶旧酒还是厚积薄发?

对于持续关注云服务架构设计最新发展趋势的同学来说,刚过去的2017年,最火的词莫过于CNCF了,这是一个专注于“云原生”(Cloud Native)的基金会,由众多战斗在云服务一线的公司(包括了谷歌、AWS、阿里云等等)组建而来,旨在推进云原生的架构标准化与最佳实践的普及。

而在最近一期的技术雷达中,“云原生”(Cloud Native)和 “微服务”(Microservices)也引出了许多相关的技术,随着 Kubernetes、Docker 等一众容器管理工具的普及,我们也看到在容器的内部,微服务的架构设计也发生着一些变化,其中“服务啮合”(Service Mesh)就成为了大家关注的热点。

那么这些变化到底是新瓶旧酒,还是厚积薄发?我们不妨先从一个更耳熟能详的架构——“网关”(Gateway)谈起。

网关(Gateway)的作用

作为微服务工具链中的元老,“网关”(Gateway) 的引入为微服务API提供统一的入口和平台,不同的服务可以得到一致的管理。使用网关的架构可以减少企业大量的重复开发。甚至有一些通用的逻辑也可以使用网关来承载(如Zuul、Enovy、OpenResty等)。

不论初心为何,这些网关们随着时光流转,功能也变得越来越丰富,网关可以负责解决不同服务的服务注册发现、负载均衡、配额流控、监控日志、缓存加速、配置分离、安全管控、跟踪审计等问题。这一系列的功能,我们可以大致分为两类:“数据面”和“控制面”。

数据面(Data Plane)负责在数据包粒度上进行筛选和处理

  • 路由转发
  • 负载均衡
  • 安全通信
  • 缓存加速
  • 认证鉴权
  • 日志审计
  • 健康检查
  • 熔断限流

控制面(Control Plane)负责在服务粒度上进行统筹和管理

  • 注册发现
  • 配置管控
  • 弹性伸缩
  • 统筹遥测
  • 容错自愈
  • 策略执行
  • 证书签发

这一系列的功能,就是网关面临的问题域。在了解问题域之后,让我们回归本篇的主题:继承了“网关”(Gateway)衣钵的“微网关”(MicroGateway)和“服务啮合”(Service Mesh),它们到底是什么?

什么是微网关?

随着微服务的普及,传统的中心化网关变得越来越厚重,由于与中心化节点通信,带来了大量网络、IO开销以及单点问题,往往无法满足我们对于实时性、高可用的要求。另外越来越多的自治化需求,与原有集权式微服务治理方法之间,也产生出许多冲突矛盾。因此,与微服务化相适应的,可以本地化、分布式部署的微网关(MicroGateway)也逐渐涌现出来。

什么是服务啮合?

服务啮合(Service Mesh)是一种为了保证“服务到服务”的安全、快速和可靠的通信而产生的基础架构层,区别于应用层、通信层的一种新的云原生上下文内的抽象层。如果你正在构建云原生的应用程序,在微服务拓扑结构日益复杂的今天,服务啮合层的提出,可以帮助开发者将服务的交互通信问题与微服务内部的业务问题隔离开来,专注于各自的领域。

演进中的微网关与服务啮合

当我们了解到微网关与服务啮合的作用之后,就可以一起来看一下微网关与服务啮合架构是如何一步步设计出来的。

低侵入性组件(Low-Invasive Component)

最初的服务间互访,常常由于业务尚不清晰,给标准化带来了障碍。因此我们常常见到一些由领域专家提供的低侵入性的组件,为服务的开发者提供抽象的规范,使其能轻松获得定制化能力。

组件可以更好地规范问题,并且尽可能地将组件封装为简单的接口,早期的服务发现常常通过该类方式实现,例如 Eureka 套件通过引入 Client 来获得完整的如报告、监控、熔断等能力。

我们在一些 IAM (Identity Access Management)的服务设计中采用了这种模式,为各个业务服务提供了一致的认证鉴权接口,由领域专家驱动,设计规范化的调用模式。由于该类组件尽可能设计为低侵入性的接口,因此微服务团队也可以更加便利地根据不同场景取舍是否使用该组件提供的功能,例如通过配置文件加 feature toogle 简单地在开发环境中关闭认证鉴权的功能,以加快开发进程。

反向代理(Reverse Proxy)

随着服务成熟度的提高,我们可以发现一些常见的非业务强相关的逻辑,可以从原有的服务中剥离出来,通过反向代理统一进行过滤处理。

反向代理,可以为微服务处理请求的前后环节增添通用逻辑,例如 apigee 提供的 API proxy 封装,通过反向代理模式为原有的服务添加 PreFlow、PostFlow,解决请求生命周期前后常见的问题,例如检查配额和记录调用频度,对 CORS 等 Http Header 的添加和消费,这些功能有些类似于传统的 Filter 模型,但是却可以独立部署。

反向代理可以提供更高的可用性,并帮助微服务开发者从这些常见细节中解脱出来。

侧车模式(Sidecar Pattern)

准确来说,侧车模式(Sidecar Pattern)本身并非微网关或者服务啮合技术独有,它只是一种特定的软件模块共生关系。侧车模式可以是一个反向代理,也可以作为一个服务存在。

作为反向代理使用的Sidecar进程可以过滤请求与返回内容,实现如安全通信、认证鉴权、服务端/客户端负载均衡、自动路由等功能。

作为服务使用的Sidecar进程可以为主服务提供额外能力,实现分布式缓存同步、配置文件拉取、日志搜集等功能。

侧车模式常见于分布式缓存和安全基础设施网关,通过与微服务进程共同启停的服务或容器,可以更方便地与微服务一并调度,享受微服务管理平台本身提供的服务发现、注册、配置、扩容能力。通过共享生命周期,在简单部署和灵活应用中寻找一个平衡

我们在微服务框架 Jhipster 提供的基础能力中,可以直接通过注解使用 Hazelcast 的分布式缓存,正是通过 Sidecar 模式实现的,拥有共生的分布式缓存实例后,可轻松实现服务接口的缓存,而分布式缓存自身的同步策略等问题都被封装在 Sidecar 进程中,无需开发者花费大量时间重新开发和调试。Sidecar也可以用于实现例如OAuth等安全相关的守护服务,帮助微服务处理业务界限外的专项问题。

原生的基础设施(Native Infrastructure)

服务啮合带来的最大的不同就是原生无感知,通过侧车模式部署的反向代理,与一些容器系统级的配置结合,更彻底地解决微服务在数据面与管理面的能力一致性问题。

服务啮合框架 Istio 提供了 Istio-Initializer 和 istioctl 工具,你可以在Kubernetes的容器整备过程中,注入所需的配置和 Envoy 容器,将Sidecar Proxy、Sidecar Service注册为容器集群中的原生服务,可以在享受弹性部署的同时,享受数据面和控制面协同提供的标准化能力。无痛地加入Istio提供的功能,如 iptables 代理转发、双向TLS认证、限流策略、日志收集等等。

图片来自:http://t.cn/RETWzGV

我们在设计服务啮合层时也可以考虑使用更原生的服务组织与部署策略,例如将微服务容器注册为系统服务、通过控制流对容器进行编排、尽可能与微服务共享生命周期和运行环境来提高可用性与性能等等。

从现在开始,拥抱微服务的云原生生态

既是新瓶旧酒,又是厚积薄发,云原生趋势下的微服务也在不断的演进,逐渐变成我们最初希望的“会呼吸”的模样。我们建议您考虑在一些适用的场景,尤其是微服务化的架构设计中,考虑使用微网关与服务啮合,并总结最佳实践与我们交流。

让我们一起期待云原生生态下的微服务,为数字化时代提供更多的想象力。


更多精彩洞见,请关注微信公众号:思特沃克

Share

ThoughtWorks和崛起的中国开源软件

最近,一份来自Adobe开发者Fil Maj的分析报告吸引了我们的注意,他通过GitHub公开的REST API,对2017年内GitHub上超过200万活跃用户(活跃意味着对公开项目超过10次提交)的数据进行分析,得出了关于开源世界贡献力量的公司排名:

从这份排名上,我们可以有一些非常有趣的发现

像早期拥抱开源的组织如Google,彻头彻尾的开源先锋Red Hat,以及诸多业界开源标准的制定者参与者如IBM、Intel,它们相应的排名位置并不令人惊讶。

相反,微软位居第一的事实让人大跌眼镜(或者说眼前一亮)。那个曾经谴责开源是“癌症”后者“反美”的微软已经一去不复返了,它在商业模式上积极寻求改变的姿态,让世人对它这些年的进步有目共睹,尤其是它一直以来对于开发者价值的珍视,吸引了越来越多的开发者踊跃加入到Azure平台中来。

让我们更加欣喜的是ThoughtWorks处于第八名的位置,考虑到我们目前全球只有3000名左右开发者,700多名开源贡献者的数字意味着我们有四分之一的开发者拥有对开源积极的热情。我们相信开源的力量,不仅对于业界知名的开源软件贡献努力,也开源了自有的软件产品——Go(开源持续交付服务器)、Gauge(开源测试自动化工具)。

值得注意的是,中国的软件公司阿里巴巴、腾讯和百度也榜上有名,这也一洗过去它们更多是开源软件消费者的印象,一跃成为在开源世界不可小觑的力量。我们耳熟能详的WeexDruidEChartsAtlas还有BeeHive因为国内广泛而特殊的应用场景,在国际市场也受到了空前的关注。

可以看出,从这个巨大而繁荣的软件市场向GitHub等开源网站发布的开源项目的数量正在持续增多,质量也将持续提高。这也是为什么我们会将“崛起的中国开源软件市场”作为上一期ThoughtWorks技术雷达的主题之一。我们相信,星星之火终成燎原之势,中国软件生态也将伴随着经济扩张而加速成长。

Share

从URL开始,定位世界

从我们输入URL并按下回车键到看到网页结果之间发生了什么?换句话说,一张网页,要经历怎样的过程,才能抵达用户面前?下面来从一些细节上面尝试一下探寻里面的秘密。

前言:键盘与硬件中断

说到输入URL,当然是从手敲键盘开始。对于键盘,生活中用到的最常见的键盘有两种:薄膜键盘、机械键盘。

  • 薄膜键盘:由面板、上电路、隔离层、下电路构成。有外观优美、寿命较长、成本低廉的特点,是最为流行的键盘种类。键盘中有一整张双层胶膜,通过胶膜提供按键的回弹力,利用薄膜被按下时按键处碳心于线路的接触来控制按键触发。
  • 机械键盘:由键帽、机械轴组成。键盘打击感较强,常见于游戏发烧友与打字爱好者。每一个按键都有一个独立的机械触点开关,利用柱型弹簧提供按键的回弹力,用金属接触触点来控制按键的触发。

(机械键盘独立触点原理图)

键盘传输信号到操作系统后便会触发硬件中断处理程序硬件中断是操作系统中提高系统效率、满足实时需求的非常重要的信号处理机制,它是一个异步信号,并提供相关中断的注册表(IDT)与请求线(IRQ)。键盘被按压时,将通过请求线将信号输入给操作系统,CPU在当前指令结束之后,根据注册表与信号响应该中断并利用段寄存器装入中断程序入口地址。具体可参看操作系统与汇编相关书籍。

当然本文主要不是介绍硬件与操作系统中的细节,前言只是想说明,从输入URL到浏览器展现结果页面之间有太多底层相关的知识,怀着一颗敬畏的心并且在有限的篇幅中是无法详细阐述的,所以本文会将关注点放在一个稍高的角度上来看,从浏览器替我们发送请求之后到看到页面显示完成这中间中发生了什么事情,比如DNS解析、浏览器渲染。

下图是从用户发送一条请求开始到最终看到页面的流程简化图,本文将以该请求为线索,在下面一步一步探索其中的细节。

浏览器解析URL

按下回车键之前

比如我按下一个“b”键,会出现很多待选URL给我,第一个便是百度。那么其实是在浏览器接收到这个消息之后,会触发浏览器的自动完成机制,会在你之前访问过的搜索最匹配的相关URL,会根据特定的算法,甚至涉及到机器学习来分析出你有可能想搜索的关键字,显示出来供用户选择。

按下回车键之后

依据上述键盘触发原理,一个专用于回车键的电流回路通过不同的方式闭合了。然后触发硬件中断,随之操作系统内核去处理对应中断。省略其中的过程,最后交给了浏览器这样一个“回车”信号。那么浏览器(本文涉及到的浏览器版本为Chrome 61)会进行以下但不仅限于以下炫酷(乱七八糟)的步骤:

  1. 解析URL:您输入的是http还是https开头的网络资源 / file开头的文件资源 / 待搜索的关键字?然后浏览器进行对应的资源加载进程。
  2. URL转码:RFC标准中规定部分字符可以不经过转码直接用于URL,但是汉字不在范围内。所以如果在网址路径中包含汉字将会被转码,比如https://zh.wikipedia.org/wiki/HTTP严格传输安全转换成 https://zh.wikipedia.org/wiki/HTTP%E4%B8%A5%E6%A0%BC%E4%BC%A0%E8%BE%93%E5%AE%89%E5%85%A8
  3. HSTS:鉴于HTTPS遗留的安全隐患,大部分现代浏览器已经支持HSTS。对于浏览器来说,浏览器会检测是否该网络资源存在于预设定的只使用HTTPS的网站列表,或者是否保存过以前访问过的只能使用HTTPS的网站记录,如果是,浏览器将强行使用HTTPS方式访问该网站。

DNS解析

不查DNS,读取缓存

  • 浏览器中的缓存:对于Chrome,缓存查看地址为:chrome://net-internals/#dns
  • 本地hosts文件:以Mac与Linux为例,hosts文件所在路径为:/etc/hosts。所以有一种翻墙的方式就是修改hosts文件避免GFW对DNS解析的干扰,直接访问真正IP地址,但已经不能完全生效因为GFW还有根据IP过滤的机制。

发送DNS查找请求

DNS的查询方式是:按根域名->顶级域名->次级域名->主机名这样的方式来查找的,对于某个URL,如下所示

查询步骤为:

  1. 查询本地DNS服务器。本地DNS服务器地址为连接网络时路由器指定的DNS地址,一般为DHCP自动分配的路由器地址,保存在/etc/resolv.conf,而路由器的DNS转发器将请求转发到上层ISP的DNS,所以此处本地DNS服务器是局域网或者运营商的。
  2. 从”根域名服务器”查到”顶级域名服务器”的NS记录和A记录(IP地址)。世界上一共有十三组根域名服务器,从A.ROOT-SERVERS.NET一直到M.ROOT-SERVERS.NET,由于已经将这些根域名服务器的IP地址存放在本地DNS服务器中。
  3. 从”顶级域名服务器”查到”次级域名服务器”的NS记录和A记录(IP地址)
  4. 从”次级域名服务器”查出”主机名”的IP地址

DNS解析图示

以www.google.com为例,下面是在阿里云实例上作的完整的DNS查询过程:

  1. 由于本次测试是在阿里云上的实例进行测试,所以首先从100.100.2.138这个阿里内网DNS服务器查找到所有根域名服务器的映射关系。
  2. 访问根域名服务器(f.root-servers.net),拿到com顶级域名服务器的NS记录与IP地址。
  3. 访问顶级域名服务器(e.gtld-servers.net),拿到google.com次级域名服务器的NS记录与IP地址。
  4. 访问次级域名服务器(ns2.google.com),拿到www.google.com的IP地址
  5. 所以总的来说,DNS的解析是一个逐步缩小范围的查找过程。

建立HTTPS、TCP连接

确定发送目标

拿到IP之后,还需要拿到那台服务器的MAC地址才行,在以太网协议中规定,同一局域网中的一台主机要和另一台主机进行直接通信,必须要知道目标主机的MAC地址。所以根据ARP(根据IP地址获取物理地址的一个TCP/IP协议)获取到MAC地址之后保存到本地ARP缓存之后与目标主机准备开始通信。具体细节参见维基百科DHCH/ARP。

建立TCP连接

为什么握手一定要是三次?

TCP三次握手

  • 第一次与第二次握手完成意味着:A能发送请求到B,并且B能解析A的请求
  • 第二次与第三次握手完成意味着:A能解析B的请求,并且B能发送请求到A

这样就保证了A与B之间既能相互发送请求也能相互接收解析请求。同时避免了因为网络延迟产生的重复连接问题,比如A发送一次连接请求但网络延迟导致这次请求是在A重发连接请求并完成与B通信之后的,有三次握手的话,B返回的建立请求A就不会理睬了。

短连接与长连接?

短连接过程演示

上图是一个短连接的过程演示,对于长连接,A与B完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。另外,由于长连接的实现比较困难,需要要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,并且长连接对于服务器的压力也会很大,所以推送服务对于一般的开发者是非常难以实现的,这样的话就出现了很多不同的大型厂商提供的消息推送服务。

进行TLS加密过程

  • Hello – 握手开始于客户端发送Hello消息。包含服务端为了通过SSL连接到客户端的所有信息,包括客户端支持的各种密码套件和最大SSL版本。服务器也返回一个Hello消息,包含客户端需要的类似信息,包括到底使用哪一个加密算法和SSL版本。
  • 证书交换 – 现在连接建立起来了,服务器必须证明他的身份。这个由SSL证书实现,像护照一样。SSL证书包含各种数据,包含所有者名称,相关属性(域名),证书上的公钥,数字签名和关于证书有效期的信息。客户端检查它是不是被CA验证过的。注意服务器被允许需求一个证书去证明客户端的身份,但是这个只发生在敏感应用。
  • 密钥交换 – 先使用RSA非对称公钥加密算法(客户端生成一个对称密钥,然后用SSL证书里带的服务器公钥将改对称密钥加密。随后发送到服务端,服务端用服务器私钥解密,到此,握手阶段完成。)或者DH交换算法在客户端与服务端双方确定一将要使用的密钥,这个密钥是双方都同意的一个简单,对称的密钥,这个过程是基于非对称加密方式和服务器的公钥/私钥的。
  • 加密通信 – 在服务器和客户端加密实际信息是用到对称加密算法,用哪个算法在Hello阶段已经确定。对称加密算法用对于加密和解密都很简单的密钥,这个密钥是基于第三步在客户端与服务端已经商议好的。与需要公钥/私钥的非对称加密算法相反。

服务端的处理

静态缓存、CDN

为了优化网站访问速度并减少服务器压力,通常将html、js、css、文件这样的静态文件放在独立的缓存服务器或者部署在类似Amazon CloudFront的CDN云服务上,然后根据缓存过期配置确定本次访问是否会请求源服务器来更新缓存。

负载均衡

负载均衡具体实现有多种,有直接基于硬件的F5,有操作系统传输层(TCP)上的 LVS,也有在应用层(HTTP)实现的反向代理(也叫七层代理),下面简单介绍一下最后者。

在请求发送到真正处理请求的服务器之前,还需要将请求路由到适合的服务器上,一个请求被负载均衡器拿到之后,需要做一些处理,比如压缩请求(在nginx中gzip压缩格式是默认配置在nginx.conf内的,所以默认开启,如果不对数据量要求特别精细的话,默认配置完全可以满足基本需求)、接收请求(接收完毕后才发给Server,提高Server处理效率),然后根据预定的路由算法,将此次请求发送到某个后台服务器上。

其中需要提到的一点是反向代理,先理解一下正向代理的原理:正向代理是将自己要访问的资源告诉Proxy,让Proxy帮你拿到数据返回给你,Proxy服务于Client,常用于翻墙和跨权限操作。反向代理也是将自己要访问的资源告诉Proxy,让Proxy帮你拿到数据返回给你,但是Proxy会将请求接受完毕之后发送给某一合适的Server,这个时候Client是不知道是根据什么规则并且也不知道最后是哪一个Server服务于它的且Proxy服务于Server,所以叫反向代理,常用于负载均衡、安全控制。

正向代理和反向代理

服务器的处理

对于HTTPD(HTTP Daemon)在服务器上部署,最常见的 HTTPD 有 Linux 上常用的 Apache 和 Nginx。对于Java平台来说,Tomcat是Spring Boot也会默认选用的Servlet容器实现,以Tomcat为例,对于请求的处理如下:

  1. 请求到达Tomcat启动时监听的TCP端口。
  2. 解析请求中的各种信息之后创建一个Request对象并填充那些有可能被所引用的Servlet使用的信息,如参数,头部、cookies、查询字符串等。
  3. 创建一个Response对象,所引用的Servlet使用它来给客户端发送响应。
  4. 调用Servlet的service方法,并传入Request和Response对象。这里Servlet会从Request对象取值,给Response写值。
  5. 根据我们自己写的Servlet程序或者框架携带的Servlet类做进一步的处理(业务处理、请求的进一步处理)
  6. 最后根据Servlet返回的Response生成相应的HTTP响应报文。

浏览器的渲染

浏览器的功能是从服务器上取回你想要的资源,然后展示在浏览器窗口当中。资源通常是 HTML 文件,也可能是 PDF,图片,或者其他类型的内容。也可以显示其他类型的插件(浏览器扩展)。例如显示PDF使用PDF浏览器插件。资源的位置通过用户提供的 URI(Uniform Resource Identifier) 来确定。

浏览器解释和展示 HTML 文件的方法,在 HTML 和 CSS 的标准中有详细介绍。这些标准由 Web 标准组织 W3C(World Wide Web Consortium) 维护。

图片来自:http://t.cn/hbvnTt

下面会以Chrome中使用的浏览器引擎Webkit为例,根据上图来简单介绍浏览器的渲染。具体解析、渲染会涉及到非常多的细节,请参考HTML5渲染规范和对应的页面GPU渲染实现。

HTML解析

浏览器拿到具体的HTML文档之后,需要调用浏览器中使用的浏览器引擎中处理HTML的工具(HTML Parser)来将HTML文档解析成为DOM树,将以便外部接口(JS)调用。

  • 文档内容解析:将一大串字符串解析为DOM之前需要从中分析出结构化的信息让HTML解析器可以很方便地提取数据进行其他操作,所以对于文档内容的解析是第一步。解析器有两个处理过程——词法分析(将字符串切分成符合特定语法规范的符号)与语法分析(根据符合语法规范的符号构建对应该文档的语法树)。
  • HTML解析:根据HTML语法,将HTML标记到语法树上构建成DOM(Document Object Model)。

CSS解析

  • 根据CSS词法和句法分析CSS文件和<style>标签包含的内容以及style属性的值
  • 每个CSS文件都被解析成一个样式表对象(StyleSheet object),这个对象里包含了带有选择器的CSS规则,和对应CSS语法的对象

页面渲染

解析完成后,浏览器引擎会通过DOM树和CSS Rule树来构造渲染树。渲染树的构建会产生Layout,Layout是定位坐标和大小,是否换行,各种position, overflow, z-index属性的集合,也就是对各个元素进行位置计算、样式计算之后的结果。

接下来,根据渲染树对页面进行渲染(可以理解为“画”元素)。

当然,将这个渲染的过程完成并显示到屏幕上会涉及到显卡的绘制,显存的修改,有兴趣的读者可以深入了解。本文只是将对于我们软件开发者来说需要了解的部分进行总结,还有很多过程与机制没有提到,github上的这个项目期待你的贡献。

参考

Share

「微前端」- 将微服务理念扩展到前端开发(实战篇)

前言与大纲

本文分为理论和实战上下两篇。本篇为微前端的实战篇,共计约 5k 字,预计阅读时间 10 mins。

技术雷达之「微前端」- 将微服务理念扩展到前端开发(上:理论篇)中,我们介绍了微前端在单体应用和微服务的架构演进中所产生的缘由,将微服务理念运用到前端开发就是为了解决臃肿前端的当前现状。与此同时,合理拆分微前端也给我们的应用开发带来显而易见的好处,在本篇当中我们将逐一介绍微前端的实践方案与可能遇到的问题和对应的优化建议。

文章大纲

  • 微前端的可选实践方案(4 种 +)
    • 创建更小的 Apps(而不是 Components)
    • 如何组合微前端的 App 模块?
      • Option 1: 使用后端模板引擎插入 HTML
        • Option 1.1: 渐进式从后端进行加载
      • Option 2: 使用 IFrame 隔离运行时
      • Option 3: 客户端 JavaScript 异步加载
      • Option 4: WebComponents 整合所有功能模块
    • 不同 App 模块之间如何交互?
    • More Options…
  • 微前端的页面优化与实例
    • 多模块页面加载问题与优化建议
    • 微前端在 AEM(CMS)项目的应用
    • 现成解决方案:Single-SPA “meta framework”
  • 总结与思考:微前端的优缺点
    • 微前端的优点
    • 微前端的缺点
    • 持续思考…
  • 附:参考资料

微前端的可选实践方案(4 种+)

创建更小的 Apps(而不是 Components)

首先让我们来创建一个典型 Web 应用程序的基本组件(Header、ProductList、ShoppingCart),以 Header 组件为例:

# src/App.js
export default () =>
  <header>
    <h1>Logo</h1>
    <nav>
      <ul>
        <li>About</li>
        <li>Contact</li>
      </ul>
    </nav>
  </header>;

然后需要注意的是我们会用到 Express 对刚刚创建的 React 组件进行服务器端渲染,使之成为一个 App 模块:

# server.js
fs.readFile(htmlPath, 'utf8', (err, html) => {
  const rootElem = '<div id="root">';
  const renderedApp = renderToString(React.createElement(App, null));

  res.send(html.replace(rootElem, rootElem + renderedApp));
});

再依次创建其他 Apps 并独立部署:

如何组合微前端的 App 模块?

在每个独立团队创建好各自的 App 模块后,我们就可以将网站或 Web 应用程序视为由各种模块的功能组合。下文将介绍多种技术实践方案来重新组合这些模块(有时作为页面,有时作为组件),而前端(不管是不是 SPA)将只需要负责路由器(Router)如何选择和决定要导入哪些模块,从而为最终用户提供一致性的用户体验。

Option 1: 使用后端模板引擎插入 HTML

# server.js
Promise.all([
    getContents('https://microfrontends-header.herokuapp.com/'),
    getContents('https://microfrontends-products-list.herokuapp.com/'),
    getContents('https://microfrontends-cart.herokuapp.com/')
  ]).then(responses =>
    res.render('index', { header: responses[0], productsList: responses[1], cart: responses[2] })
  ).catch(error =>
    res.send(error.message)
  )
);
# views/index.ejs
  <head>
    <meta charset="utf-8">
    <title>Microfrontends Homepage</title>
  </head>
  <body>
    <%- header %>
    <%- productsList %>
    <%- cart %>
  </body>

但是,这种方案也存在弊端,即某些 App 模块可能会需要相对较长的加载时间,而在前端整个页面的渲染却要取决于最慢的那个模块。

比如说,可能 Header 模块的加载速度要比其他部分快得多,而 ProductList 则因为需要获取更多 API 数据而需要更多时间。通常情况下我们希望尽快将网页显示给用户,而在这种情况下后台加载时间就会变得更长。

Option 1.1: 渐进式从后端进行加载

当然,我们也可以通过修改一些后端代码来渐进式地(Progressive)往前端发送 HTML,但与此同时却徒增了后端复杂度,并且又将前端的渲染控制权交回了后端服务器。而且我们的优化也取决于每个模块加载的速度,若是进行优化就必须按一定顺序进行加载。

Option 2: 使用 IFrame 隔离运行时

<body>
  <iframe width="100%" height="200" src="https://microfrontends-header.herokuapp.com/"></iframe>
  <iframe width="100%" height="200" src="https://microfrontends-products-list.herokuapp.com/"></iframe>
  <iframe width="100%" height="200" src="https://microfrontends-cart.herokuapp.com/"></iframe>
</body>

我们也可以将每个子应用程序嵌入到各自的 <iframe> 中,这使得每个模块能够使用任何他们需要的框架,而无需与其他团队协调工具和依赖关系,依然可以借助于一些库或者 Window.postMessageAPI 来进行交互。

  • 优点
    • 最强大的是隔离了组件和应用程序部分的运行时环境,因此每个模块都可以独立开发,并且可以与其他部分的技术无关
    • 可以各自使用完全不同的前端框架,可以在 React 中开发一部分,在 Angular 中开发一部分,然后使用原生 JavaScript 开发其他部分或任何其他技术。
    • 只要每个 iframe 来自同一个来源,消息传递也就相当直接和强大。参考文档 Window.postMessageAPI
  • 缺点
    • Bundle 的大小非常明显,因为可能最终会多次发送相同的库,并且由于应用程序是分开的,所以在构建时也不能提取公共依赖关系。
    • 至于浏览器的支持,基本上不可能嵌套两层以上的 iframe(parent - > iframe - > iframe)。
  • 如果任何嵌套的框架需要能够滚动或具有 Form 表单域,那样的情况处理起来就会变得特别痛苦。Option 3: 客户端 JavaScript 异步加载
function loadPage (element) {
  [].forEach.call(element.querySelectorAll('script'), function (nonExecutableScript) {
    var script = document.createElement("script");
    script.setAttribute("src", nonExecutableScript.src);
    script.setAttribute("type", "text/javascript");
    element.appendChild(script);
  });
}

document.querySelectorAll('.load-app').forEach(loadPage);
<div class="load-app" data-url="header"></div>
<div class="load-app" data-url="products-list"></div>
<div class="load-app" data-url="cart"></div>

简单来说,这种方式就是在客户端浏览器通过 Ajax 加载应用程序,然后将不同模块的内容插入到对应的 div 中,而且还必须手动克隆每个 script 的标记才能使其工作。

需要注意的是,为了避免 Javascript 和 CSS 加载顺序的问题,建议将其修改成类似于 Facebook bigpipe 的解决方案,返回一个 JSON 对象 { html: ..., css: [...], js: [...] } 再进行加载顺序的控制。

Option 4: WebComponents 整合所有功能模块

Web Components 是一个 Web 标准,所以像 Angular、React/Preact、Vue 或 Hyperapp 这样的主流 JavaScript 框架都支持它们。你可以将 Web Components 视为使用开放 Web 技术创建的可重用的用户界面小部件,也许会是 Web 组件化的未来。

Web Components 由以下四种技术组成(尽管每种技术都可以独立使用):

  • 自定义元素(Custom Elements)对外提供组件的标签,实现自定义标签:可以创建自己的自定义 HTML 标签和元素。每个元素可以有自己的脚本和 CSS 样式。还包括生命周期回调,它们允许我们定义正在加载的组件特定行为。
  • HTML 模板(HTML <template>定义组件的 HTML 模板能力:一种用于保存客户端内容的机制,该内容在页面加载时不被渲染,但可以在运行时使用 JavaScript 进行实例化。可以将一个模板视为正在被存储以供随后在文档中使用的一个内容片段。
  • 影子 DOM(Shadow DOM)封装组件的内部结构,并且保持其独立性:允许我们在 Web 组件中封装 JavaScript,CSS 和 HTML。在组件内部时,这些东西与主文档的 DOM 分离。
  • HTML 导入(HTML Imports)解决组件组合和依赖加载:在微前端的上下文中,可以是包含我们要使用的组件在服务器上的远程位置。
# src/index.js
class Header extends HTMLElement {
  attachedCallback() {
    ReactDOM.render(<App />, this.createShadowRoot());
  }
}
document.registerElement('microfrontends-header', Header);
<body>
    <microfrontends-header></microfrontends-header>
    <microfrontends-products-list></microfrontends-products-list>
    <microfrontends-cart></microfrontends-cart>
</body>

在微前端的实践当中:

  • 每个团队使用各自的技术栈创建他们的组件,并把它包装到自定义元素(Custom Element)中(如 <microfrontends-header></microfrontends-header>)。
  • Web 组件就是应用程序中包含的组件的本地实现,如菜单,表单,日期选择器等。每个组件都是独立开发的,主应用程序项目利用它们组装成最终的应用程序。
  • 特定元素(标签名称,属性和事件)的 DOM 规范还可以充当跨团队之间的契约或公共 API。
  • 创建可被导入到 Web 应用程序中的可重用组件,它们就像可以导入任何网页的用户界面小部件。
<link rel="import" href="/components/microfrontends/header.html">
<link rel="import" href="/components/microfrontends/products-list.html">
<link rel="import" href="/components/microfrontends/cart.html">
  • 优点
    • 代码的可读性变得非常清晰,组件资源内部高内聚,组件资源由自身加载控制,作用域独立。
    • 功能团队可以使用组件及其功能,而不必知道实现,他们只需要能够与 HTML DOM 进行交互。
    • 使用 PubSub 机制,组件可以发布消息,其他组件可以订阅特定的主题。幸运的是浏览器内置了这个功能。比如购物车可以在 window 订阅此事件并在应该刷新其数据时得到通知。
  • 缺点
    • 可惜的是,Web 组件规范跟服务器渲染无关。没有 JavaScript,就没有所谓的自定义元素。
      • 浏览器和框架的支持不够,需要更多的 polyfills 从而影响到用户页面的加载体验。
      • 我们需要在整个 Web 应用程序上做出改变,把它们全部转换成 Web Components。
      • 社区不够活跃,Web Components 还没有真正流行起来,也许永远也不会。

不同 App 模块之间如何交互?

# angularComponent.ts
const event = new CustomEvent('addToCart', { detail: item });
window.dispatchEvent(event);
# reactComponent.js
componentDidMount() {
  window.addEventListener('addToCart', (event) => {
    this.setState({ products: [...this.state.products, event.detail] });
  }, false);
}
  • 得益于浏览器的原生 API,Custom Event 可以与其他任何技术和框架一起工作。比如,我们可以将消息从 Angular 组件发送到 React 组件。其实这也是现在 API 之间普遍使用 JSON 进行通信的原因,即使没有人使用 NodeJS 作为服务器端。
  • 但是,新的问题又出现了。我们该如何测试这种跨模块之间的交互?需要编写类似于后端微服务之间的 Contract Testing 或 Integration Testing 吗?并没有答案。

More Options…

  • 组件库 – 根据主 App 的技术栈,不同的组件和 App 模块拆分作为库的形式提供给主App,所以主 App 是由不同组件组成的。但是组件库的升级将成为一个大麻烦,比如对 Header 组件进行了更改,那么如果已经有 50 个页面使用了 Header 组件该怎么办?必须要求每一页都升级它的 Header,而且升级过程中用户还会在整个网站不同页面上看到不一致的标题。并且,在两边还必须都使用相同的技术,比如 Header 组件中使用了 ClojureScript,而 Content 组件中又用了 Elm,那么该怎么办?构建工具就必须在编译时处理不同的语言。
  • 将 App 模块作为 React 黑盒组件分发给消费者模块 – 应用程序的状态完全包含在组件中,API 只是通过 props 暴露出来。这种方式其实增加了应用程序之间的耦合,因为它迫使每个人都使用 React,甚至会使用相同版本的 React,但是这似乎也是一个比较好的折衷。
  • Edge Side Includes(ESI)/Server Side Includes(SSI) – 通过特殊的文件后缀 (shtml,inc) 或简单的标记语言来对那些可以加速和不能加速的网页中的内容片断进行描述,将每个网页划分成不同的小部分分别赋予不同的缓存控制策略。SSI / ESI 方法的缺点是,最慢的片段决定了整个页面的响应时间。

微前端的页面优化与实例

多模块页面加载问题与优化建议

  • 使用 skeleton screen 响应式布局:如上图 LinkedIn 所做的那样,首先展现给用户一个页面的空白版本,然后在这个页面中逐渐加载和填充相应的信息。否则中间的信息流部分的内容最初是空白的,然后在 JavaScript 被加载和执行过后,信息流就会因为需要占用更多的空间而推动整个页面的布局。虽然我们可以控制页面来固定中间部分的高度,但在响应式网站上,确定一个确切的高度往往很难,而且不同的屏幕尺寸可能会有所不同。但更重要的问题是,这种高度尺寸的约定会让不同团队之间产生紧密的联系,从而违背了微前端的初衷。
  • 使用浏览器异步加载加快初始渲染:对于加载成本高且难以缓存的碎片,将其从初始渲染中排除是一个好主意。比如说 LinkedIn 首页的信息流就是一个很好的例子。
  • 共享 UI 组件库保证视觉体验一致:在前端设计中,必须向用户呈现外观和感觉一致的用户界面。建议可以建立一个共享组件库(包含 CSS、字体和 JavaScript)。将这些资源托管在 CDN,每个微前端就可以在其 HTML 输出中引用它们的位置。每个组件库的版本都正确地对资源进行版本控制,而每个微前端都指定要使用的组件库的版本和显式更新依赖关系。
  • 使用集中式服务(Router)来管理 URL:可以理解为前端的 Gateway,不同的 URL 对应不同应用程序所包含的内容。建议通过一个集中式的 URLs Router 来为应用程序提供一个 API 来注册他们自己的 URL,Router 将会位于 Web 应用程序的前面,根据不同的用户请求指向不同的 App 模块组合。
  • 提取共同依赖作为 externals 加载:虽然说不同 App 模块之间不能直接共享相同的第三方模块,当我们依然可以将常用的依赖比如 lodashmoment.js等公共库,或者跨多个团队共同使用的 reactreact-dom。通过 Webpack 等构建工具就可以把打包的时候将这些共同模块排除掉,而只需要在 HTML <header> 中的 <script>中直接通过 CDN 加载 externals 依赖。
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/react.min.js"
  crossorigin="anonymous"></script>
<script
  src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/react-dom.min.js"
  crossorigin="anonymous"></script>

微前端在 AEM(CMS)项目的应用

我们在「三靠谱」(已和谐客户名称)的 Marketplace 项目当中也曾经探索过 AEM + React 混合开发的解决方案,其中就涉及到如何在 AEM 当中嵌入 React 组件,甚至将 AEM 组件又强行转化为 React 组件进行嵌套。现在回过头来其实也算是微前端的一种实践:

  • AEM 仅仅包含网页内容,不包含 domain 相关的结构化数据。
  • React 组件被托管在 AEM 组件当中,再经由 AEM 传递给组件所需要的属性,比如 IDs 或 APIs 的 URL 等等
  • 后端微服务则包含 domain 结构化数据,由对应的 React 组件通过 Ajax 进行数据查询。
  <div id="cms-container-1">
    <div id="react-input-container"></div>
    <script>
      ReactDOM.render(React.createElement(Input, { ...injectProps }), document.getElementById('react-input-container'));
    </script>
  </div>
  <div id="cms-container-2">
    <div id="react-button-container"></div>
    <script>
      ReactDOM.render(React.createElement(Button, {}), document.getElementById('react-button-container'));
    </script>
  </div>

现成解决方案:Single-SPA “meta framework”

开源的 single-spa 自称为「元框架」,可以实现在一个页面将多个不同的框架整合,甚至在切换的时候都不需要刷新页面(支持 React、Vue、Angular 1、Angular 2、Ember 等等):

  • Build micro frontends that coexist and can each be written with their own framework.
  • Use multiple frameworks on the same page without refreshing the page (React, AngularJS, Angular, Ember, or whatever you’re using)
  • Write code using a new framework, without rewriting your existing app
  • Lazy load code for improved initial load time.
  • Hot reload entire chunks of your overall application (instead of individual files).

请看示例代码,所提供的 API 非常简单:

import * as singleSpa from 'single-spa';

const appName = 'app1';

const loadingFunction = () => import('./app1/app1.js');
const activityFunction = location => location.hash.startsWith('#/app1');

singleSpa.declareChildApplication(appName, loadingFunction, activityFunction);
singleSpa.start();
# single-spa-examples.js

declareChildApplication('navbar', () => import('./navbar/navbar.app.js'), () => true);
declareChildApplication('home', () => import('./home/home.app.js'), () => location.hash === "" || location.hash === "#");
declareChildApplication('angular1', () => import('./angular1/angular1.app.js'), hashPrefix('/angular1'));
declareChildApplication('react', () => import('./react/react.app.js'), hashPrefix('/react'));
declareChildApplication('angular2', () => import('./angular2/angular2.app.js'), hashPrefix('/angular2'));
declareChildApplication('vue', () => import('src/vue/vue.app.js'), hashPrefix('/vue'));
declareChildApplication('svelte', () => import('src/svelte/svelte.app.js'), hashPrefix('/svelte'));
declareChildApplication('preact', () => import('src/preact/preact.app.js'), hashPrefix('/preact'));
declareChildApplication('iframe-vanilla-js', () => import('src/vanillajs/vanilla.app.js'), hashPrefix('/vanilla'));
declareChildApplication('inferno', () => import('src/inferno/inferno.app.js'), hashPrefix('/inferno'));
declareChildApplication('ember', () => loadEmberApp("ember-app", '/build/ember-app/assets/ember-app.js', '/build/ember-app/assets/vendor.js'), hashPrefix('/ember'));

start();

值得一提的是,single-spa 已经进入到最新一期技术雷达的评估阶段。这意味着 single-spa 会是值得研究一番的技术,以确认它将对你产生何种影响,你应该投入一些精力来确定它是否会对你所在的组织产生影响。

摘自技术雷达:

SINGLE-SPA是一个 JavaScript 元框架,它允许我们使用不同的框架构建微前端,而这些框架可以共存于单个应用中。一般来说,我们不建议在单个应用中使用多个框架,但有时却不得不这么做。例如当你在开发遗留系统时,你希望使用现有框架的新版本或完全不同的框架来开发新功能,single-spa 就能派上用场了。鉴于很多 JavaScript框架 都昙花一现,我们需要一个解决方案来应对未来框架的变化,以及在不影响整个应用的前提下进行局部尝试。在这个方向上,single-spa 是一个不错的开始。

总结与思考:微前端的优缺点

微前端的优点

  • 敏捷性 – 独立开发和更快的部署周期:
    • 开发团队可以选择自己的技术并及时更新技术栈。
    • 一旦完成其中一项就可以部署,而不必等待所有事情完毕。
  • 降低错误和回归问题的风险,相互之间的依赖性急剧下降。
  • 更简单快捷的测试,每一个小的变化不必再触碰整个应用程序。
  • 更快交付客户价值,有助于持续集成、持续部署以及持续交付。
  • 维护和 bugfix 非常简单,每个团队都熟悉所维护特定的区域。

微前端的缺点

  • 开发与部署环境分离
    • 本地需要一个更为复杂的开发环境。
    • 每个 App 模块有一个孤立的部署周期。
    • 最终应用程序需要在同一个孤立的环境中运行。
  • 复杂的集成
    • 需要考虑隔离 JS,避免 CSS 冲突,并考虑按需加载资源
    • 处理数据获取并考虑用户的初始化加载状态
    • 如何有效测试,微前端模块之间的 Contract Testing?
  • 第三方模块重叠
    • 依赖冗余增加了管理的复杂性
    • 在团队之间共享公共资源的机制
  • 影响最终用户的体验
    • 初始 Loading 时间可能会增加
    • HTML 会需要服务器端的渲染

持续思考…

  • 变幻莫测)前端的技术选型?
    • 前端 JavaScript 框架工具穷出不穷,过几个月就要重写前端项目?比如最近又出来了声称要取代 Webpack(Parcel)和 Yarn(Turbo)的工具。伴随着前端框架的更新换代,如果整个项目一起升级/重构的话压力大、风险高,那不如拆分微前端直接支持多 framework,或者同一 framework 的不同版本?`
  • 在 Mobile/Mobile Web 上的悖论
    • 受限于 Mobile 尺寸大小,单一页面所能展现的内容本就有限。
    • 既然已经分出了不同的子页面,那何不如直接 Route 即可?
  • 合理划分的边界:DDD(领域驱动开发)
    • 最大的挑战是搞清楚如何合理拆分应用程序。
    • 糟糕的设计可能成为开发和维护的噩梦。
  • Don’t use any of this if you don’t need it
    • Do not use the ideas described here until it is needed, it will make things more complex.
    • If you are in a big company, those ideas could help you.
  • 软件架构到底在解决什么问题?—— 跨团队沟通的问题
    • 在正常情况下,每个团队拥有开发和维护其特性所需的一切,都应该有自己的能力来完成自己的特性,并最大限度地减少团队要求其他部门获得许可和/或帮助。
    • 当引入 library 或 framework 时的好处是只需要少数人讨论,而不用涉及超过 100 人的决策和他们的各种需求。这样一场大讨论不仅会耗费时间和精力,而且会迫使我们采用最不起眼的方法来选择 library,而不是选择专门针对每个 team 的问题领域的方案。

所谓架构,其实是解决人的问题;所谓敏捷,其实是解决沟通的问题;

附:参考资料

本次技术雷达「微前端」主题的宣讲 Slides 可以在我的博客找到:「技术雷达」之 Micro Frontends:微前端 – 将微服务理念扩展到前端开发 – 吕立青的博客


更多精彩洞见,请关注微信公众号:思特沃克

Share