经常会有人问:“Culture Amp 还在使用 Elm 吗?”,我会私下回答说我们不再投资 Elm 了,并解释原因。他们往往会说我的答案非常有价值,应该公开分享出来。但之前我一直没有这么做。2016 年,我们开始在 Culture Amp 使用 Elm,先是用于单团队实验,最后它成为我们新前端代码的首选语言。我曾在三个会议演讲中公开讲述过这个故事:1.《生产中的 Elm:惊喜与痛点》:https://youtu.be/LZj_1qVURL02.《前端开发人员使用 Elm 的幸福感》:https://youtu.be/kuOCx0QeQ5c3.《规模化的 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 : Stringgreeting = sayHello “Kevin”sayHello : String -> StringsayHello 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 没有感受到它们: