从Ruby到Node:重写Shopify CLI,提升开发体验
本文最初发布于 Shopify 工程博客。
Shopify CLI(命令行界面)是开发人员在 Shopify 平台上构建和部署 Theme、App、Hydrogen 店面时的重要工具。它提供了按照最佳实践创建新项目的工作流,实现了与开发平台的集成,并可以将产品工件分发给商家。我的团队,即 CLI Foundations,负责为设计和构建 Shopify CLI 的最佳实践和核心功能打基础。我们知道,开发人员在开发 Shopify App 时会大量用到终端,而他们使用 CLI 时并不总是能够获一致而愉快的体验。因此,我们开始使用 Node 彻底重写 Shopify CLI 2(那原本是用 Ruby 编写的),并在去年夏天推出了 Shopify Editions。在这篇博文中,我将介绍下我们团队之前为什么做出了重写的决策以及当时所做的权衡,我们在这个新的迭代中所遵循的原则,以及我们后续要克服的挑战和探索的想法。
在 Shopify CLI 之前,Theme 开发人员用的是我们的另一个 CLI——ThemeKit。我们从 2014 年 10 月就开始维护它。它是用 Ruby 编写的,基于我们在内部 CLI 和服务中使用的一些 Ruby gems 构建,诸如 cli-kit、cli-ui 和 theme-check 等。
当我们在 2018 年 12 月开始开发第一个 Shopify CLI 来帮助 App 开发人员时,考虑到我们已有的 Ruby 资源和知识,选择 Ruby 是明智的。用户需要一个全局的 Ruby 安装才能使用 CLI,但我们通过为所有受支持的操作系统(Windows、Linux 和 macOS)提供安装程序解决了这个问题。2020 年 12 月,我们将 ThemeKit 合并到 Shopify CLI 中,迈出了将所有开发集中在一个 CLI 中的第一步。
2020 年 6 月,我们添加了 UI 扩展,让开发人员可以使用自己的 UI 扩展平台的某些区域,CLI 开始依赖 Node 工具来转译和打包扩展代码。系统要求越来越高,这不是用户所希望的。不过,生态系统正朝着用编译式语言(如 Go 和 Rust)实现 JavaScript 工具的方向发展,因此我们希望可以摆脱对 Node 的依赖。但这种事情并没有发生。尽管像 ESBuild 这样的工具(我们用于打包扩展)是可移植的二进制文件,但它们的可扩展性依赖于在 Node 运行时上动态求值的插件。
此外,Hydrogen 团队已经在 Node 上构建了一些工具,他们开始考虑构建一个新的 CLI,而不是将 Hydrogen 工作流构建到 Shopify Ruby CLI 中,这样他们的用户就不需要在自己的系统中安装 Ruby 运行时。Hydrogen 开发人员希望 npm install 命令能够解析他们在项目中需要的所有依赖项。如果将 Ruby 作为一个依赖项,这种思维模式就会被打破,他们就很容易遇到 CLI 因为需要额外的步骤而拒绝运行的问题。另建一个 CLI 会破坏我们始于将 ThemeKit 合并到 CLI 的统一工作。这可能会导致平台不同区域的 CLI 体验不一致。
最后但同样重要的是,Shopify 越来越依赖于 Web 技术和标准,其中 JavaScript 和 Node 运行时在资源、工具和知识方面更有优势。
所有这些都促使我们思考 Ruby 是否是最适合 CLI 的语言,所以我们回顾了这个决策。我们需要一种技术:
系统要求尽可能少(例如,不需要安装多个运行时);
让我们能够提供一流的开发体验;
内部团队很容易做出贡献。
最终,我们决定用 TypeScript 重写 CLI,以便在 Node 运行时上运行。
在 Shopify 使用的所有编程语言中,Ruby 是大多数开发人员都熟悉的语言,其次是 Node、Go 和 Rust。使用它们都可以构建出一流的开发体验,这要归功于生态系统提供的丰富软件包解决了常见的问题。从这些选项中,Go 和 Rust 都可以轻松地发布运行时不依赖运行时的静态二进制文件。这一点,Node 和 Ruby 也可以通过将源代码和运行时依赖项(又名 vendoring)打包在一起来实现,但设置更复杂,并且可能有一些操作系统不支持。
我们选择 Node 有几个原因。Go 和 Rust 允许分发静态二进制文件,但代价是 Shopify 的人由于不熟悉语言很少能做出贡献。这并不理想,因为 Shopify 希望内部团队可以为 CLI 贡献新的想法。我们只能选择 Ruby 或 Node。
在构建 CLI 方面,Node 有一个与 Ruby 不同的特性:它的模块系统和它所支持的可扩展性。与 Ruby 不同,Node 的模块系统允许同一个传递包有多个版本,而且不会相互冲突。这就让我们可以构建一个模块化的架构,将平台的不同功能域封装在 NPM 包中,而它们都基于一个包含共享功能的包构建。需要注意的是,虽然 Hydrogen 和 App 开发人员只需要一个运行时(Node),但 Theme 开发人员现在还需要两个:Ruby 和 Node。不过,我们已经开始着手消除 Ruby 依赖,我们的目标是在今年晚些时候完成这项工作。
我们做出了技术决策,但我们还得做一些最佳实践、代码架构、模式和约定方面的决策。这是对我们从不同团队习得的经验和我们构建 Ruby CLI 的经验的一次综合运用。我将与大家分享我们在构建卓越的终端体验的过程中对我们影响最大的 7 个决定。
Ruby CLI 的贡献一致性较差,而且是松耦合的(与我们所期望的高度一致的松耦合状态相反),导致内外部分化,进而导致了糟糕的体验。在 Node 版本中,我们必须做一些不同的事情。我们需要一种方法来使贡献保持一致。我们通过:
代码模式:建模命令的业务逻辑。在基于框架(如 Rails)的项目中,框架(如 MVC)通常会支持这些模式,但我们没有框架。因此,我们必须开发自己的模式和机制,并保证开发人员遵循它们。
UI 模式和组件:我们在 Ink 上设计并构建了一个设计系统,以确保所有命令的体验都和 Shopify 类似。
约定:便于开发人员浏览他们的项目和命令。
原则:创造卓越的体验,助力开发人员取得成功。
上述内容体现在一个共享 NPM 包 @shopify/cli-kit 中,所有功能域(Theme、App 和 Hydrogen)都以它为基础构建,还有一套通用的原则,用于 CLI 和任何与平台开发人员交互的界面(例如,文档和合作伙伴仪表板)。像 ESLint 这样的静态分析工具成为我们构建自动化的平台,用于保证贡献与 CLI 的基础和方向相一致。我们实现了像 command-flags-with-env 这样的自定义规则,以支持通过环境变量设置命令标志。
下面的示例展示了一个惯用的 API,特性开发人员可以使用该 API 获取一个有效的会话,与 GraphQL API 进行交互。
const session = await ensureAuthenticatedAdmin(storeFQDN)
文件 idiomatic_API_example.js,托管在 GitHub 上
在 MacOS 环境中开发时,确保代码更改支持 macOS、Windows 和 Linux 是一个繁琐的过程,会导致测试被跳过并出现回归。Node 运行时会使问题加剧,因为已知有些 API 在不同操作系统上的行为不一致。社区正在用 NPM 包克服这些问题。例如,pathe 规范了跨操作系统的路径。为了防止同样的事情再次发生,我们采取了三个策略:
我们在 @shopify/cli-kit 中提供了环境交互(如 IO 操作)模块,并确保它们的 API 跨操作系统兼容。如果我们检测到操作系统不兼容的情况,就会一次性修复它。
我们的测试套件混合了单元测试、集成测试和端到端(E2E)测试,这些测试通过持续集成(CI)在所有支持的操作系统上运行。它会在合并前在 PR 中暴露问题。我们为与环境存在契约关系的模块(如提供 Git 交互实用工具的模块)编写集成测试。
我们提供了在 MacOS、Linux 和 Windows 环境中测试更改的指令。
Conway 定律在我们的组织中得到了体现,我们的存储库中包含了 CLI 的不同组件(如模板和内部 CLI)。Multirepo 设置不会给用户带来什么价值,却使得内部贡献和自动化测试更加麻烦。我们决定以重写为契机改变这种局面,将所有组件放入同一个存储库 shopify/cli 中。Monorepo 设置允许跨多个包和模板原子地贡献更改。
Ruby CLI 命令的业务逻辑是有状态的,有许多假设,并且在命令生命周期中会产生多种副作用。这增加了代码推理、贡献和测试的难度。对于 Node CLI,我们采用了不同的方法。
我们在逻辑设计时尽可能地函数化,并且只要可能,就将副作用集中在命令开始的部分。例如,命令所做的第一件事是在内存中加载和验证项目。这类似于 Web API 在接收请求时所做的事情;在将其传递到可能产生级联效应且处于无效状态的系统之前,它将对其进行验证。我们对函数范式的运用并不是教条式的,但我们的目标是把逻辑变成传递状态的函数的组合。
我们使用 JavaScript 对象和函数作为组合单元。我们默认创建对象的副本,而不是改变传递的实例。只有少数情况下,为了符合语言要求,我们才诉诸于类,如错误类型。我们引入了一个与函数组织有关的软约定,类似于模型 - 视图 - 控制器(MVC)架构模式:
模型(Model):是用来对状态建模的 TypeScript 接口,例如 App 项目、项目配置和会话的内部表示。
命令(View):是用户进行交互的界面,用户在调用 CLI 时会传递参数和标志。它们的职责仅限于解析和验证参数及标志,并提供帮助菜单的内容。
服务(Controller):是业务逻辑的封装单元。所有命令都有一个包含命令业务逻辑的服务,有些服务没有绑定到特定的命令。
除了上面提到的,我们还有提示符,它包含通过标准输入提示用户的函数,以及将一组函数分组到特定域的实用程序。一个例子是与 Shopify GraphQL API 交互的所有函数。
采用函数式编程和最小化副作用简化了单元测试的编写。为了定义和运行它们,我们使用了 Vitest,它在我们开始开发 Node CLI 前数周才刚刚发布。我们决定使用 Vitest,因为它完全支持 ES 模块(我们采用的模块系统)。尽管在工具成熟的过程中,最初会有一些问题,但我们对它提供的体验和与 Jest API 一对一的映射感到满意。
单元测试给了我们信心,相信我们的函数在不同的场景中完成了它们应该做的事,但这还不够——单元测试套件成功运行的结果并不意味着像“app build”这样的工作流在最近创建的项目中成功运行。因此,我们决定投资一个使用 Cucumber 的端到端测试套件,以确保各种工作流可以端到端工作。Cucumber 为我们提供了描述、运行和调试这些测试的工具和 API。你可能知道的,E2E 以维护麻烦和可能引入古怪行为而闻名。不过,在 CLI 中不会那样,因为这里的设置更简单。执行可以隔离,并将范围限定在测试场景中,防止全局状态泄漏到其他测试中导致它们表现异常。下面是我们的一个测试示例:
Scenario: I create and build an app with extensions
Given I create an app named MyTestApp with pnpm as the package manager
And I create an extension named TestPurchaseExtension of type post_purchase_ui
When I build the app
The The extensions are built
文件 example_test_gherkin.txt,托管在 GitHub 上
TypeScript 的类型系统和编译器让我们可以相信,代码单元和外部依赖关系之间的契约是匹配的。CLI 依赖的许多 NPM 包和 @shopify/cli-kit 中提供的模块提供了类型定义,极大地改善了对存储库做贡献的体验。例如,我们正在实现一个为 CLI 设计的新设计系统的组件,我们广泛使用 TypeScript 来确保开发人员以正确的方式使用组件。
在早先一次与 Shopify 之外的 CLI 开发人员的对话中,oclif 作为一个出色的、使用 Node 构建 CLI 的工具和 API 框架出现在我们的视野中。例如,它诞生于 Heroku 的 CLI,用于支持其他 CLI 的开发。在决定使用 Node 之后,我们更彻底地研究了 oclif 的特性集,构建了小型原型,并决定基于它们的 API、约定和生态系统构建 Node CLI。事后看来,这是个好主意。
Oclif 为我们提供了用于声明 CLI 接口的惯用 API,并提供了出色的默认值自定义功能。例如,帮助文档是从代码内的声明自动生成的。此外,它还通过插件系统内置提供了可扩展性,我们已经利用插件开发了 App、Theme 和 Hydrogen。它允许我们将项目组织成边界和职责分工明确的模块。我们利用了 oclif 的 hooks API 来防止 @shopify/cli-kit 通过依赖倒置获得依赖插件的信息。插件和 @shopify/cli-kit 实现依赖于接口。
Node CLI 极大地改善了开发体验:我们统一并简化了 App 开发,带来了全面的一致性,并新增了扩展功能,如函数。不过,我们还有很长的路要走,还有很多东西要学。
我们希望 Theme 开发体验与 App 和 Hydrogen 的开发体验保持一致。目前,Theme 命令仍然在 Ruby 实现中运行,为用户提供 Ruby CLI 体验,开发人员需要在他们的环境中安装 Ruby 运行时,这种情况并不理想。
我们还将继续迭代 App 开发体验,为开发人员提供一些实用的命令,用于创建、开发 App 并部署到平台。自从宣布为开发 App 提供更好的开发体验以来,我们已经收到了许多宝贵的反馈,并且正以此为基础进行迭代,如从 Multirepo 设置迁移到统一 App 模型的一些难点。在构建新想法的原型方面,我们的团队也有一个很好的基础。未来,我们会很高兴分享我们的进展。
关于可扩展性,还有很多内容可以分享,但这是后续博文的主题。
Pedro Piñera Buendía 是 Shopify 的一名高级开发人员。
原文链接:
https://shopify.engineering/overhauling-shopify-cli-for-a-better-developer-experience
声明:本文为 InfoQ 翻译,未经许可禁止转载。
编程已死,AI 当立?教授公开“唱反调”:AI 还帮不了程序员
抗拒使用 GPT-4 和 Copilot 写代码,拥有 19 年编程经验的老程序员“面试”被淘汰
微信扫码关注该文公众号作者