Elm + React:入门容易,坚持难

开始做某件事情很容易——终止才是难事。——Brian Eno

经常会有人问:“Culture Amp 还在使用 Elm 吗?“,我会私下回答说我们不再投资 Elm 了,并解释原因。他们往往会说我的答案非常有价值,应该公开分享出来。但之前我一直没有这么做。

2016 年,我们开始在 Culture Amp 使用 Elm,先是用于单团队实验,最后它成为我们新前端代码的首选语言。我曾在三个会议演讲中公开讲述过这个故事:

1.《生产中的 Elm:惊喜与痛点》:

https://youtu.be/LZj_1qVURL0

2.《前端开发人员使用 Elm 的幸福感》:

https://youtu.be/kuOCx0QeQ5c

3.《规模化的 Elm:更多的惊喜,更多的痛点》:

https://youtu.be/uQjivmLan0E

从 2018 年年中到 2020 年年中,我主持并制作了 20 集 Elm Town 播客,并在我们办公室帮忙组织了几个年度的 Elm 墨尔本聚会,直到疫情爆发后才停办。

过去这些年,我为 Elm 发声过很多。那么何不谈谈我们脱离 Elm 的经历呢?

我对自己说,没有人会对别人不想做某事的故事感兴趣。所有会议演讲和病毒式传播的帖子,开端都是由新奇事物来吸引大家的注意,结尾往往平淡无奇。想象一下技术领导者让团队成员停止使用自己喜欢的工具,这确实很糟糕。

如果我公开宣布 Culture Amp 离开 Elm 社区,暗示 Elm 存在一些致命缺陷或错误,这是对 Elm 的不公平。世界上没有完美的技术,每个工具各有利弊,需要我们自己去做利弊的权衡。

更重要的是,我担心大家会认为在 Culture Amp 使用 Elm 是错误的,他们自己不会去思考究竟是对错。因此,我开头说:我一直没有对放弃使用 Elm 进行公开分享。

对于工程师而言,随着他们在职业生涯中达到更高的级别,他们面临的最大挑战就是做出决策,平衡他们得到的给定工具带来的即时喜悦(或挫败感)和同一工具可能会随着时间和规模,为他们的团队、公司或客户带来的成本(或收益)。这些对于我们在行业中处于领导地位的人们来说非常重要,当我们经过权衡使用某一工具时,剩余的工具也可以在另外的场景中发挥重要作用。

这就是一个故事,讲述了 Culture Amp 在自豪地宣传 Elm 是其构建 Web UI 的首选语言四年后,Culture Amp 决定——决定放弃它的过程。

Elm 的简要介绍

以下是 Elm 的简单介绍,如果你不熟悉 Elm 的话可以快速浏览一下:Elm 是一个“好用的 Web 应用程序开发语言”。它能编译成 JavaScript,因此可以在任何 Web 浏览器上运行。但作为一种基于 ML 的函数式编程语言,它看起来像 Haskell,而不是像 JavaScript。

JavaScript 有许多括号和花括号:

const greeting = sayHello("Kevin");
function sayHello(name) {
  if (name == "Kevin") {
      return "Hi, Kev!";
  }
  return `Hello, ${name}.`;
}

Elm 的语法要简洁得多:

greeting =
    sayHello "Kevin"
sayHello name =
    if name == "Kevin" then
        "Hi, Kev!"
    else
        "Hello, " ++ name ++ "."

Elm 的语法更简洁,相比 JavaScript 它是一种更简单的语言,功能也比 JavaScript 少的多。这种简单性是 Elm 的一个特点:Elm 的设计目标是不会给你过多的绳子去绞死自己。

Elm 具有的一个特性是静态类型系统。在上面的代码示例中,Elm 将推断并强制要求 sayHello 必须使用一个 String 参数进行调用。你也可以(而且应该)声明函数的类型,以帮助 Elm 在你犯错时及时捕捉它们:

greeting : String
greeting =
    sayHello "Kevin"
sayHello : String -> String
sayHello name =
    if name == "Kevin" then
        "Hi, Kev!"
    else
        "Hello, " ++ name ++ "."

除了这种简单、函数式、静态类型的语言之外,Elm 还包含了构建 Web 应用程序所需的所有功能,包括虚拟 DOM 渲染、状态管理、效果和订阅,几乎所有你可能需要的功能都内置了。

Elm 还因为以下三个方面而出名:

  • 开发过程中极其有帮助的错误提示。

  • 运行时没有错误。

  • 生成的 JavaScript 包非常小。

Elm 是由 Evan Czaplicki 发明的,是他十年工作的成果,偶尔得到社区的合作者和像 NoRedInk 这样的公司的赞助。

我们欠 Elm 什么

回顾 Elm 在 Culture Amp 的表现,它完全实现了它的承诺。我们用 Elm 构建的产品部分从首次生产部署就无错误地运行;工程师们开玩笑说,使用 Elm 构建的功能的发布日实际上意味着工作的结束,有点不可思议。除了多年来有两次不兼容的发布需要进行一些迁移工作(第一次是次要的,第二次更重要一点)以外,Elm 本身非常稳定,我们实际上不需要做任何工作,就能让我们的依赖项保持最新状态(这是 NPM 生态系统中的一个重要负担)。

具有讽刺意味的是,这种稳定性在几个场合实际上对我们产生了负面影响,因为当 Elm 代码库需要关注时,已经有好几年没有人看过它了,构建它的团队通常已经完全忘记了它的工作方式!值得庆幸的是,Elm 的简单性使得代码很难复杂化,因此当有人需要阅读它们时,那些被遗忘的代码库通常很容易阅读。

除了那些技术上的优点之外,Elm 还带来了一些无形的好处:在澳大利亚竞争激烈的招聘市场上,Elm 帮助我们脱颖而出。仅在墨尔本,就有数十家资金充裕的公司会聘请你写 JavaScript。Culture Amp 是为数不多的几家可以让你使用强类型、函数式编程语言编写 Web UI 的公司之一。结合至今仍然让我兴奋的产品使命,Elm 吸引了一些最优秀的工程师,他们对“会考虑使用 Elm 的公司”感兴趣。

这也可能有两面性。我们在 Elm 之旅的早期得到了一些很好的建议,如果一个工程师想加入你的团队唯一的原因是你的技术栈,那可能是一个警示信号。因此,Culture Amp 避免雇用纯粹关注技术的工程师。作为一家产品公司,我们寻求聘请那些对我们的产品和使命感到兴奋,并愿意在必要时学习新东西来推动进展的人。当面试中有人告诉我们他们对这里工作的兴趣是因为他们喜欢函数式编程(例如),我们会将其视为一个指示,表明他们可能不是一个好的团队成员。我们不止一次因为这种动机不匹配而选择不聘用某位候选人,在多年的历程中,有一两次我希望我们更加严格地遵循这一原则(对于工程师和我们自己来说都是如此)。

总的来说,我对 Elm 对 Culture Amp 的影响感到满意。在企业成长的关键阶段,Elm 使其能够生产可靠、易于维护的 Web 应用程序,并吸引了对这些结果有优先考虑的工程师,即使这意味着与众不同。它使我们的团队比原本预计的发展得更成功。

Elm + React:入门容易,坚持难

在使用 Elm 以前,Culture Amp 已经开始用 React 了。如果你想尝试 Elm,可以先试着把 React 应用程序改写成 Elm。将 Elm 用作 React 组件嵌入 React 应用程序中非常容易:可以将 Elm 应用程序作为 React 组件运行。您可以先将应用程序 UI 的一个小矩形区域编写为 Elm 应用程序。如果您喜欢它,就将该矩形区域扩展到填满整个屏幕,然后删除 React。这就是推荐的方式。

2018 年,刚成立的设计系统团队开始遇到麻烦。该团队需要构建和维护可重复使用的用户界面组件和样式库,以节省时间并在独立构建 Culture Amp 平台各项功能的越来越多的团队之间建立一致性。因为有些团队使用 React 构建,而另一些团队使用 Elm 构建,所以 Culture Amp 的设计系统 Kaizen 需要支持两种构建方式——至少在 Elm 能够 “填充浏览器窗口” 之前,这种情况在当时至少还需要两年时间。

我们最初的方法是将设计系统组件构建为具有相同功能的一对实现:一个是 Elm,另一个是 React。为了将这两者捆绑在一起,这两个实现都将导入并使用相同的 CSS 模块(使用 Sass 编写)。您可以在我们的 Button 组件中(截至 2021 年底)看到一个示例,其中包括一个 Button.elm 和一个 Button.tsx,以及一个被两者导入的 styles.scss 文件(感谢我为此目的创建的 elm-css-modules-loader)。

这种方法一开始非常成功。那些熟悉 React 的团队越来越倾向于采用 Elm,因此他们具备了为两个版本的组件做出改变并保持同步的技能和信心。但是在 2018 年,这种情况开始发生改变。

一些团队,即最早热衷于采用 Elm 的积极采用者完成了摆脱 React 的迁移。这些团队努力拥抱了 Elm 的极致类型安全、纯函数式编程,他们最不想做的事情就是在为设计系统组件做出改变时再次使用生疏的 React 技能。

我们越来越难以保持两个版本的组件同步。这种负担越来越多地落在了小型的设计系统团队身上。在一个 React 组件中添加的组件功能可能没有添加到其 Elm 版本中(反之亦然),这样的事情在他们的待办列表中积累,逐渐地,一个组件的两个版本成为了一个带有重叠功能集的两个组件。本来应该将它们联系在一起的单个 CSS 模块成为了一个 Sass 模块中两个组件样式的混合。

这给我们的设计系统团队带来的痛苦足以推动我们开始尝试使用 Web 组件,看看它们是否能够提供一种更好的方法来构建一个语言无关的共享 UI 组件库。

Web Components 实验

Web Components 是一个用于创建模块化、可重用组件的浏览器技术集合,可以像原生 HTML 元素一样使用它们。表面上看,Web Components 似乎是为解决我们所遇到的问题而量身定制的:需要同时在 Elm 和 React 应用程序中使用的组件。

我们进行了几次 Web Components 实验,如果在 Culture Amp 中维护多个前端框架(如 Elm/React/Svelte/Angular/ 任何框架)是不可避免的,我们可能会坚持下去。不过,Web Components 是一组低级别的技术,实际上需要它们自己的框架来进行扩展。在 2020 年,当我们认真探索这个领域时,我们喜欢 Stencil 的外观,它是一个非常类似于 React 的框架,你可以编写带有渲染函数返回 JSX 的 JavaScript 类。在 2023 年,Lit 似乎正在成为事实上的标准(尽管 Stencil 有一个新团队和新的主要发布版本,仍值得一看)。

在致力于使用 Web Components 之前,我们进行了一项雄心勃勃的实验。我们选择了我们 API 最密集的组件——Title Block,它是一个功能特别丰富的组件,由许多子组件组合而成,可以创建一个可配置的头部区域,放置在应用程序的 UI 顶部,然后尝试将其转换为 Stencil 组件。

在这个实验中,我编写了 Stencil 的 Elm 输出目标。如果我们选择在 Kaizen 中使用 Stencil 组件,这个插件将允许我们将它们发布为 TypeScript 类型的 React 组件和 Elm 类型的 Elm 模块。在这个项目中,我必须做出一些妥协(因为我的代码生成器无法将某些复杂的 TypeScript 类型合理地转换为 Elm 类型 / 解码器 / 编码器),但我认为它已经完成了大约 80% 的工作。

Title Block 已经在 React 和 Elm 中实现,但是负责将其移植到 Stencil 的设计系统工程师花了一个多月的时间才交付了一个几乎完整的版本,并且没有人对其 API 感到特别满意。

因为需要作为静态 HTML 标签使用,Web Components 支持的 API 格式比 JavaScript 视图框架更有限。我们的 Elm 和 React 工程师都习惯将丰富的数据类型传递给组件,例如将记录 / 对象作为配置,或将函数作为渲染道具。Web Components 基本上限制用户将组件 HTML 属性(文本字符串)传递给组件并将函数作为事件监听器连接起来。一旦 Web Component 在文档中加载,您可以调用方法和设置 JavaScript 属性,但是在初始渲染(以及可能重新渲染 DOM 树)后连接必要的组件配置在 React 和 Elm 中都会变得相当混乱。

如果选择使用 Shadow DOM(乍一看似乎是一个非常有吸引力的选择:在组件级别强制 DOM 和样式封装 – 太棒了!),那么这基本上意味着您将不得不采用 Web Components 框架(如 Stencil)提供的任何 CSS 解决方案。您不能只使用喜欢的 CSS 工具来为应用程序的 CSS 捆绑包贡献组件样式,因为这些“轻 DOM”样式不会应用于在 Shadow DOM 内呈现的组件。例如,在我们的标题块组件中,它呈现了许多按钮和菜单组件,按钮和菜单的样式不会传递到这些呈现的子组件,除非您的框架在其 Shadow DOM 内为每个组件加载样式表(它隐藏在标题块的 Shadow DOM 内)。像 Stencil 这样的框架具有很好的 CSS 支持,可以为您处理每个组件的样式表加载,但这是在构建设计系统组件时将我们的工程师带离他们熟悉的工具之一。

最终,我们的实验揭示了 Web Components(即使有一个很好的框架)与 React 和 Elm 有足够不同的地方,使用它们实际上意味着将第三个视图框架添加到我们的技术栈中,它也会有自己的小毛病,限制,学习曲线和维护负担。与降低团队为我们的设计系统做出贡献的障碍相反,Web Components 将增加障碍。这可能会加剧我们想要解决的挑战:团队开始认为只有小型设计系统团队的工程师可以对我们的共享组件进行更改,这使该团队成为公司几乎每个 UI 项目的关键路径。

最终,我们根据从这个实验中学到的内容,决定不继续使用 Stencil 和 Web Components。

临界质量

这样我们就面临着一个选择:使用 Elm 还是 React。同时维持两者对我们而言成本有点太高,不太现实。

最终倾向于 React 的一个关键原因是我们收购了另一家完全使用 React 编写代码的公司,该公司的团队对 Elm 一无所知。一夜之间,我们从一个 Elm 和 React 使用占比差不多的公司(可能会决定加倍投入 Elm),变成了大约有 75% 都在使用 React 的公司。

在那个时候,TypeScript 已经变得足够强大和开发者友好,能够平衡 Elm 最初吸引我们的一些特性:可用的类型系统、足够好的错误信息等。React 已经内置了更多有用的状态管理基元,大致上与 Elm 的“电池包含”状态管理相匹配。

同时,Elm 自身开发以及其工具的发展势头也开始减缓。Elm 不再旨在“成为主流!”,或者至少实现这个愿景的努力(例如语言服务器和编辑器集成、静态和服务器渲染、CSS 集成和自动化测试工具不再是核心语言特性,而是社区项目,发展缓慢)。我们经常遇到针对我们的代码库或构建环境独有的工具问题,必须自己贡献修复。Culture Amp 是一家中等规模的技术公司,可以承担为其依赖的开源生态系统做出贡献,但在 Elm 的情况下,感觉我们需要投入的贡献要大于我们获得的回报,才能使其对我们发挥良好的作用。

考虑到所有这些,我们 CTO 也感受到了一些寻求规模经济的健康压力,因为 Culture Amp 已经超过 100 名工程师为产品做出贡献,我可以看出 Culture Amp 只能支持一个前端应用程序框架 – 而动力不在 Elm 的一边。

在内部,情况也已经明显了。Elm 0.18 → 0.19 的重大更改是合理的,但是它花费了多个团队的少数志愿者大约一年的时间才完成(最终我花了一个月的空闲时间完成了最后的几个部分)。当没有人找到时间和动力在您的堆栈中保持技术健康时,您可以推断人们对它的感受。

做出改变

当我意识到需要做出决定时,我列出了我认为在公司最热衷于 Elm 的工程师的名单。他们是那些在 Elm 聚会上遇见我们并加入我们的人,或者是在其他工程师遇到 Elm 问题时自愿与他们配对的人。他们是每天仍在以 Elm 发布新功能的团队的技术负责人。这是一个大约有 6 个人的名单。我安排了与他们每个人的一对一会议,讨论在 Culture Amp 让 Elm 成功的挑战,以及我认为也许是时候退役它作为新项目的选择。

Culture Amp 的工程领导保持着内部的“技术雷达”,其中列出了四个类别的技术:“采纳”,“试验”,“限制”和“保持”。我让这些工程师知道我正在考虑将 Elm 从“采纳”移到“限制”,询问他们的想法,并倾听他们的意见。

如果你感兴趣,这里是我们对“限制”的定义:

这项技术要么只被批准用于非常特定的上下文或用例,要么我们认为对于大多数新项目来说,有更好的“采用”选择。拥有使用这些技术构建的资源的团队仍必须支持它们,甚至可能需要扩展它们。

每一个工程师都表示理解并同意这个决定。那些拥有活跃的 Elm 代码库的工程师提出了建设性的建议,关于如何减轻对他们的影响(例如,其中一位建议将所有 Elm 组件从设计系统移动到他们的代码库中,实际上创建一个他们将在其代码库生命周期内维护的分支)。

这些谈话感觉很好,很诚实。没有人因此辞职(至少不是马上),也没有表现出这种想法。我认为这在一定程度上要归功于上面提到的招聘方法(避免纯技术导向的工程师)。

在所有这些谈话结束后,我坐下来在我们的前端工程实践频道中写了一个反馈请求:

征求反馈:在 Culture Amp 使用 Elm

嗨,@practice_front_end_eng!在过去几周中,我已经和那些在 Culture Amp 前端工程技术混合中最多使用并推崇 Elm 的工程师们进行了几次交谈,探讨我们是否应该继续选择它作为新项目的技术栈。

作为对我们的立场的提醒,可以在 Confluence 上查看“如何在 Elm 和 React 之间进行选择”。近期的构建周期中有少数例外(尤其是在 #team_ted 中,他们最近在 Elm 之外的单块应用程序中做了很棒的工作),当我们信任他们为 Culture Amp 做出正确决策时,我们的大部分团队和阵营都选择在 TypeScript 中使用 React 进行新项目的开发。

考虑到这一趋势,以及找到“更少但更好”的方法的需求,我即将做出一个决定,将 Elm 在我们的前端技术列表中的状态从“采纳”移动到“限制”。这意味着我们将继续维护和增加现有 Elm 代码库的功能,但我们将避免在新项目中选择它,以便更有效地集中我们的共同努力,确保 React/TypeScript 代码库的健康和可持续性,甚至为实验未来的新语言 / 框架创造空间。

在我最终确定这个决定之前,我想给所有工程师一个机会与我联系,给出反馈。你喜欢使用 Elm 并想要有自由继续在新项目中使用吗?Elm 是否是您尚未尝试过,但认为它可能改进团队构建用户界面的方式?即使您不认为自己是前端工程师,如果您对我有反馈,我也很乐意听取 – 让我们在本周末(10 月 16 日)之前提出看法吧。

谢谢!

有几位工程师发表了他们的想法。我们的前端基础团队的 Louis Quinnell 发表了这篇深入思考的分析,阐述了 Elm 的好处,以及为什么我们在 Culture Amp 没有感受到它们:

我认为 Elm 很棒。这也是我对 Culture Amp 产生兴趣的原因——我最初是通过 Elm 的 slack 联系了 @kevin!

我在一家软件公司工作时发现了 Elm,那时我们的工作性质涉及到很多上下文切换。项目会来去,通常需要进行一堆的初始工作,然后是几轮更改,维护合同,有时还需要为进一步的工作提供新的预算。在任何时候,我们都会与几个客户同时处于这个过程的不同阶段。

我们需要能够有效地切换上下文。我们必须将新的人员放到旧项目中,并让他们快速进行更改,同时不希望他们由于对该代码库的不熟悉而出现问题。

我们通过采用模式和静态分析的标准化来部分解决了这个问题——例如,我们采用了具有非常高严格性的 TypeScript——这让我们走得更远了。

然而,我们最终遇到了“JavaScript 疲劳”的问题:我们用来解决维护负担的工具本身正在创造维护负担!

Elm 通过使用单个依赖项来强制执行所有这些良好的模式和编译器功能,从而解决了这个问题。在我加入 Culture Amp 之前,我没有机会真正地使用它,但如果我重新开始,我仍然会考虑使用类似 Elm 的东西,原因就是上面所述,而且我不认为 Culture Amp 的需求有多么不同……

……除了 Elm 真正被设计为你的整个前端栈。

我们通过投资工具(即超级酷的黑客技巧)来解决这个问题,使 Elm 可以与我们的混合栈集成。但是,这样使用 Elm 会产生一些后果:

首先,我们只有在某些地方才有 Elm 的信心,而你是否最终会进入 Elm 代码库则可能是个摇奖游戏(或者不幸的事情,这取决于你的感觉)。

其次,我们不能将 Elm 用作我们的单个依赖项——它实际上只是我们的其他工具和代码需要考虑的一个(大)复杂性。

这意味着我们既没有看到 Elm 作为低维护前端堆栈的好处,也没有看到它作为保证一致低成本上下文切换的方式的好处。

因此,我支持将 Elm 列入限制层面的决定。我还有其他原因,但这就是关键!

最终没有反对意见。

我在我们的技术雷达上更新了有关 Elm 的状态和描述:

2016-2020 年期间,Elm 是 Culture Amp 前端技术栈中不断增长的一部分,特别是在我们还没有强大且相对可用的类型系统 TypeScript 之前,Elm 是非常受欢迎的。然而,自从收购 Zugata 以及大型 performance-ui 代码库,以及 React 和 TypeScript 的不断成熟,我们相信选择一种单一的语言和框架(React)作为新项目的最佳路径,因为这将在前端实践中为我们带来规模经济。使用 Elm 编写的代码库将继续需要进行维护,在某些情况下需要进行扩展,但当我们开始新项目时,Elm 不再是可选项了。

这就是成功(有时)的样子

虽然我们经常赞扬 Elm 的逐步采用方式,但我要给任何想要效仿我们的步伐的团队一个警告:如果 Elm 的势头停滞不前,似乎不可能填满整个视口,那么你可能需要考虑一个退出策略。这种中间状态是不可持续的,除非你能够承担大量的投入,支持设计系统团队维护平行组件或与框架无关的组件实现方面的热情。

但回顾过去,我仍然很高兴我们在 Culture Amp 中使用了 Elm。当然,如果没有 Elm,有些事情可能会更容易。例如,我们不会有两个现在仍在使用 Elm 编写的相对较大的 Web 应用程序,负责这些应用程序的团队认为它们的代码是历史的好奇心印迹,总有一天需要完全重写。

但有些事情也会更难:Culture Amp 完全使用 Elm 构建了其第二个产品 Culture Amp Effectiveness(一个 360 度评估工具)的用户界面。使用当时 React 生态系统中可用的工具,建立该产品所需的时间会更长,我们会发布更多的错误,多年的维护成本也会更高。

我可以列出至少一打我们成功聘请并与之共事的,表现出色的工程师名单,这是我职业生涯中的一个亮点。但如果当初我们没有选择一种帮助我们与众不同的技术,我可能永远不会遇到这些人。在“足够奇怪”的状态下,也有一些值得一提的经历。

一段关系的结束并不意味着它是失败的。我们已经不再把 Elm 作为首选技术了。有时,成功不仅意味着擅长开辟新路径,学会终结一些事情也是一样要熟练的。

原文链接:

https://kevinyank.com/posts/on-endings-why-how-we-retired-elm-at-culture-amp/

相关阅读:

从新 React 文档看未来 Web 的开发趋势

前端工程化实战:React 的模块化开发、性能优化和组件…

我被 React 劫持了,很痛苦又离不开

手写一个 react,看透 react 运行机制

本文文字及图片出自 InfoQ

余下全文(1/3)
分享这篇文章:

请关注我们:

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注