创建组件库时您将面临的困境

原文信息: 查看原文查看原文

The Dilemmas You'll Face When Creating a Component Library

- Andrico

注意:这是几年前我写的文章系列的更新版本,但内容仍然非常相关。

构建组件库是一次充满挑战和回报的经历,但不仅仅是编写外观出色的组件那么简单。在旅程的每一步,您都需要做出选择,以确保您的库适合其预期受众。

在构建您的库时,您需要考虑以下困境:

尽管本系列专注于使用Web组件构建组件库,但这里涵盖的许多困境也可以应用于其他Web框架。如果您想构建您的第一个Web组件库,那么请查看Component Odyssey,这是一套课程,教您构建、样式设计和发布适用于任何框架的Web组件库所需的一切。

我应该编写纯Web组件还是使用库?

注意:您可能认为如果您不打算使用Web组件,则此部分不适用,但Web组件与每个其他Web框架都是互操作的!有许多出色的库旨在使编写Web组件更加愉快,因此值得考虑它们用于未来的项目。

Web组件以其冗长的API和涉及像影子DOM这样的复杂概念而闻名。如果没有辅助库的帮助,编写Web组件可能会很棘手,特别是如果您不熟悉它们。幸运的是,有很多编写Web组件的方式,可以归纳为3个抽象层次:

  1. 使用现有的组件库作为基础,并为主题组件
  2. 使用辅助库从头开始构建组件
  3. 使用纯Web组件从头开始构建组件

使用现有库作为基础

如果您觉得编写Web组件太复杂,您可以从建立在已建立的组件库之上开始。像LionShoelace这样的库提供了完全功能的组件,但有足够的样式挂钩,让您可以按需进行样式设计。

这是Web组件新手变得更熟悉的一种很好的方式。同时,您将大量代码复杂性转移到经过实战测试的库中。选择正确的库意味着您将拥有强大、可访问的组件。如果您想要可高度定制的即用型组件,同时为您的最终用户提供出色的用户体验,这是一个很好的选择。

使用辅助库构建组件

如果您想更深入地了解Web组件并快速开发周期呢?

或者,如果您想更多地控制组件的行为方式呢?

您可以使用库使编写Web组件更加符合人体工程学。这些工具不提供任何即用型组件,而是在纯Web组件API之上提供抽象。一些最受欢迎的工具包括:

  • Lit - 一个用于构建快速、轻量级Web组件的简单库。
  • Stencil - 一个简单的编译器,用于生成Web组件和静态站点生成的PWA
  • Atomico - 一个使用仅函数、钩子和虚拟DOM创建Web组件的微库。

我使用Lit最多,它是我从头开始构建Web组件的首选。它具有出色的TypeScript支持、简化的API和依赖浏览器标准的强烈理念。

Stencil是另一个坚实的选择。它提供了一个简洁的API和框架类工具,比如一个编译器,将其特定风味的Web组件转换为浏览器友好的代码。

如果您有React背景,Atomico是一个很好的选择,因为它使用类似JSX的语法+React风格的钩子。

其中一些,但不是全部,这些选项需要编译器使您的组件浏览器就绪。这对您来说可能不是问题,但这确实意味着在编写组件和在浏览器中使用它们之间有一个中间步骤。

构建纯Web组件

在编写Web组件时,还有一个选择是去纯化。如果您准备编写更冗长的代码并熟悉Web组件规范,那么去纯化是有效的。

您将深入学习影子DOM、自定义元素和HTML模板等概念。结果,它将帮助您更好地理解和更好地欣赏前面提到的一些库。其次,您将提高对其他浏览器概念的理解,因此使用Web组件将使您成为一个更全面的Web开发人员。

观点时间 (🪙🪙)

根据您优先考虑的事情,我会推荐不同的方法。

如果您想快速构建一些可能会被少数用户使用的东西,请尝试在现有库的基础上构建。一个伟大的库将有以下好处:

  • 经过实战测试的组件,随着时间的推移不断改进
  • 将可访问性放在首位
  • 强大的文档
  • 它将帮助您更快地开发界面

如果您想要更多的动手编码,那么我会推荐选择一个辅助库:Lit、Atomico或Stencil。我个人倾向于Lit,我在很多项目中都使用过它。我还使用Lit,因为它不需要编译器就可以使其浏览器兼容。

最后,如果您想深入Web组件的低级API,那么我会建议探索自己构建Web组件。这是更好地了解浏览器平台的好方法,并且可以帮助您成为一个更全面的开发人员。

Web组件的文档可能非常分散,但MDN是一个开始的好地方。您还可以查看Component Odyssey,它以有趣和实用的课程教授Web组件的基础知识。

我应该将我的库导出为单个包还是作用域包?

许多组件库通过单个NPM包提供整个组件套件的访问,而其他库则将组件拆分成自己的包。让我们看看这两种方法。

单个包

这是您的消费者通过单个包安装您的组件库的地方。

Material Web这样的库提供了一个核心UI模块,它导出了几十个组件。您将通过以下方式安装:npm install @material/web并像这样使用组件:

<script type\="module"\>
  import "@material/web/button/filled-button.js";
  import "@material/web/switch/switch.js";
</script\>

<md-filled-button\>点击我</md-filled-button\>
<md-switch\></md-switch\>

作用域包

这是您的用户为他们想要使用的每个组件安装一个包的地方。像Radix UI这样的库采用了“作用域包”方法。通过这种方法,库中的每个组件都可以独立安装。

以下是一个React组件库的示例,因为我找不到一个广泛使用的Web组件库采用这种方法的例子。如果您想使用Radix UI的复选框组件,您需要安装一个专用的复选框包:npm install @radix-ui/react-checkbox并像这样使用组件:

import \* as Checkbox from "@radix-ui/react-checkbox";

const CustomCheckbox \= () \=> (
  <Checkbox.Root\>
    <Checkbox.Indicator\>
      <CheckIcon /\>
    </Checkbox.Indicator\>
  </Checkbox.Root\>
);

export default CheckboxDemo;

然后,如果您想安装其他Radix组件,比如他们的SliderPopover组件,您需要安装一个额外的依赖项。

我应该选择哪个?

在决定时,您考虑以下问题:

  • 您的库是否融入了有见地的设计语言或鼓励使用多个组件?
  • 您是否想避免创建和维护多个包的复杂性?
  • 您是否期望您的最终用户只使用少数组件?*
  • 您是否希望自由地分别对组件进行版本控制?(稍后我们会讲到)如果您对前两个问题回答是,您会考虑通过单个包导出您的组件。如果您对最后两个问题回答是,您会考虑对您的包进行作用域化。第三个答案有一些细微差别,我在下面讨论。

您的库是否融入了设计语言或鼓励使用多个组件?

许多组件库对于如何设计和构建您的用户界面都有自己的见解。它们可能共享一个共同的设计语言,或者跨组件的一致API设计。很可能,您不会在单个项目中混合使用这些组件库。在这种情况下,将组件作为单个包导出是有意义的。

其他项目,如Radix,提供完全功能的白标组件,消费者可以选择。消费者可以选择组件,而不受该库的视觉设计语言的约束。在这种情况下,消费者可能只需要使用少量组件。如果您采用这种方法,那么将每个组件作为单独的包发布是可行的选项。

您是否期望您的最终用户只使用少量组件?

并非所有使用组件库的人都使用该库提供的所有组件。

您可能担心提供单个包,您的消费者的应用程序包将包含任何未使用的组件。如果发生这种情况,将向他们的最终用户发送大量未使用的代码。

在ES模块系统在浏览器中得到广泛支持之前,这是一个问题,因为打包工具很难删除未使用的代码。现在,ES模块得到了广泛支持,并且它允许打包工具对您的导入进行静态分析,打包工具可以更容易地从消费者包中删除未使用的代码。

如果您想了解更多信息,请访问Component Odyssey,它更深入地介绍了浏览器的模块系统。

意见时间 (🪙🪙)

您可能认为《单个包》方法只适用于较小的“简单”组件库,但实际上有一些非常出色的库采用这种方法。一个例子是Shoelace

我过去尝试过这两种解决方案,发现对于组件库来说,管理多个包的复杂性通常是不必要的(但对于其他用例完全有效!)。

还要考虑您的最终用户。安装一个包比安装多个包容易。当每个组件由单独的包管理时,包更新体验会更加繁琐。我将在下面进一步讨论版本控制问题。我的一般理念是选择最简单的方法,只有在必要时才增加复杂性。

如何构建我的库的仓库结构?

如果您要发布单个包,您需要创建一个顶层的package.json文件,该文件包含库中的每个组件。一个组件库的标准结构可能如下所示:

component-library/
src/
button/
button.js
checkbox/
checkbox.js
package.json

如果您要导出作用域包,每个包将独立发布。这意味着每个组件都需要一个package.json文件。由于每个组件都有自己的package.json文件,它将拥有一定程度的自主权,否则它将没有。它可以有自己的脚本、依赖项和版本等。这种方法,您的组件库由不同的项目组成,称为monorepo。您的文件夹目录可能如下所示:

component-library/
packages/
button/
button.js
package.json
checkbox/
button.js
package.json
package.json

根据我的经验,使用monorepos为UI库带来的关键好处包括:

  • 独立发布组件
  • 限制构建和脚本,这可以在特别大的库中加快速度
  • 逻辑上分组组件批次

对于最后一点,您可能有一个alpha/实验性组件的跟踪,这些组件存在于核心组件库之外。使用monorepo将允许您以不同的开发节奏开发和发布这些实验性组件。

所有主要的包管理器都支持monorepos,所以如果您想探索它们,您不需要设置和安装一个全新的工具。

npm | yarn | pnpm

然而,还有一些专门的工具提供了更强大monorepo体验,如果您正在处理一个特别大或复杂的仓库,这些工具可能会派上用场。例如,Turborepo需要额外的配置,但是带来了构建缓存等好处。

意见时间 (🪙🪙)

我在大型后端/前端monorepos中工作过,我也使用monorepos构建过组件库,比如A2K。 除非您真的坚持要对组件库进行版本控制,否则我建议至少在开始时避免使用monorepos。

我应该使用工具来构建和/或打包我的库吗?

使用原生浏览器技术构建组件的一个好处是,它可以简化您的开发环境。如果您正在构建纯Web组件或像Lit这样的库,您不需要构建工具或编译器来将您的代码转译成浏览器兼容的东西。

话虽如此,可能有情况下您想使用构建工具。也许您想要一些额外的语言特性,或者您想使用像TypeScript这样的工具。也许您用来编写Web组件的工具需要一个构建步骤。Stencil有一个编译器,而Atomico提供了两种构建组件的方式,一种兼容web,另一种需要一个构建步骤。

当涉及到构建您的库时,有三个流行的选项:

  • 不构建(无构建)
  • 编译器,如TypeScript和Stencil
  • 配置构建工具

无构建

我们用来构建用户界面的许多工具在浏览器中并不原生支持。对于一些工具来说,这一点很明显,比如Python模板引擎。其他的则不那么明显。像React这样的库需要一个构建工具才能在浏览器中运行,将JSX转换为浏览器友好的东西。

Web组件是浏览器原生的,这意味着您不必运行构建就能让它们在浏览器中工作。您可以采用这种理念,在完全开发您的组件库时使用。您可能想要避免任何需要构建工具的工具。这种工作流程,您编写的代码是浏览器兼容的,被称为“无构建”,即运行它不需要构建步骤。

这种工作流程越来越受欢迎,有一些高调实例的团队移除了对构建工具的依赖。我第一次接触无构建工作流程是通过Pascal Schilp的2019年的文章,该文章展示了无构建可以实现的成就。

编译器(TypeScript、Stencil)

您可能想要使用一种与浏览器基本不兼容的语言或工具。像TypeScript这样的工具非常受欢迎,因为它们在JavaScript之上引入了特性,如类型,以及一个编译器,可以在问题到达用户之前捕捉到它们。

添加编译器需要在开发和发布您的库之前进行编译步骤。如果您要发布TypeScript代码,那么您的消费者将需要设置TypeScript才能使用您的库。

Stencil和其他类似工具也是如此。引入这些工具的主要问题是,您在运行和发布代码之前增加了一个中间步骤。这意味着您的编译器是一个单点故障,如果有bug或“技能问题”,它可能会降低生产力。您还冒着使人们更难为您的库做出贡献的风险,因为他们需要学习工具才能做出贡献。

这并不是说这些工具不好。它们可能提供用户体验或开发人员体验上的好处,这些好处可能更难以(或不可能)在没有这种工具的情况下实现。

构建工具(Vite)

在开发过程中,您需要运行一个开发服务器。许多构建工具同时充当开发服务器,但有些会悄悄地向您的库注入非标准行为。如果您运行Vite服务器,以下JavaScript将开箱即用:

import './index.css';

export function main() {...}

Vite自动向您的项目添加非标准浏览器行为。如果您在不转换的情况下发布此代码,那么它可能会导致不使用与您相同工具的消费者出现问题。

并不是所有工具都这样,Web Dev Server是一个开发服务器,它不会隐式地让您选择浏览器不兼容的行为。我经常在构建Web组件时使用这个。

意见时间 (🪙🪙)

当涉及到构建工具时,我的偏好取决于我的项目。如果我正在构建一个应用程序,我会选择TypeScript。如果我正在构建一个组件库,我会避免TypeScript。我最近一直在使用JSDoc,它通过代码注释提供了浏览器友好的类型安全性。

旁注:应用层面的考虑与库层面的考虑

您可能想要执行某些优化,比如在发布之前对代码进行压缩、打包和转译。您可能需要重新考虑,库开发者是否应该关心这些优化?我的答案是不。

首先,您不知道每个使用您库的应用程序的要求。它们可能只支持现代浏览器,它们可能支持IE11,它们可能有带宽预算,或者没有。确保他们的应用程序满足用户需求是应用程序开发者的责任。他们可能需要压缩代码,将其捆绑成一个单一输出,并将其转译成旧版本的JavaScript。

让我们看看打包,这是一个应用层面的考虑。打包是将多个模块组合成较少但更大的代码块的过程。开发者应该找到正确的平衡,以确保最终用户及时获得所有所需代码。作为库作者,执行此优化不是您的责任,因为应用程序的需求可能会大相径庭。过早的优化甚至可能对使用您库的开发者产生敌意。过去,我使用了一个库,他们发布的代码进行了压缩,这使得调试问题非常困难。

作为一个库作者,您可以将您的组件作为现代的、浏览器友好的JavaScript使用ESM导出,并允许您的消费者对您的代码做任何他们需要做的事情。我在Component Odyssey中介绍了这种方法。

我如何安全地发布我的库?

您对库进行版本控制和发布的方式取决于您是否选择将您的仓库结构化为monorepo。

在monorepo中对多个包进行版本控制和发布比简单地发布单个包要复杂得多。

让我们先看看后者:

发布单个包

发布您的包的最快方法是运行npm publish,但我建议不要这样做。默认的发布脚本没有多少保护措施来防止您犯常见的发布错误。

这很容易:

  • 发布未提交的更改
  • 从错误的分支发布
  • 发布带有失败的测试

NP这样的工具更合适。它自称为“更好的npm publish”,因为它保护开发者免于犯上述错误。

在monorepo内发布包

如果发布多个包,您还需要决定是否希望所有包都被固定为相同的版本或被独立版本控制。

固定

在这种情况下,monorepo中的所有包共享相同的版本。如果您的复选框组件有小的更改并升级到1.2.0,您的警告组件也将升级到版本1.2.0。

缺点是即使组件没有变化,也可能发布新版本的组件。如果开发者使用您组件库中的许多包,这可能导致令人困惑和繁琐的升级体验。

独立

在这种情况下,monorepo中的所有包都有独立版本。如果您的复选框组件有小的更改并升级到1.2.0,您的警告组件的版本将不会改变。

如果您期望用户使用少量组件,或者组件在其效用上是独立的,这是一个很好的方法。它还适用于分离更频繁更改的组件,如alpha或实验性组件。

管理独立版本是一个棘手的操作,特别是如果某些组件依赖于其他组件。流行的包管理器也没有很好地支持这种工作流程,所以您可能需要选择一个专门的发布工具。

Changesets是一个很好的工具,可以在monorepo中降低风险并自动化发布过程。您在开发过程中向Changesets描述您的更改。当您准备发布时,Changesets聚合这些文件,确定每个更改包的正确版本,并发布到NPM。

意见时间 (🪙🪙)

我发布过单一包组件库和多包monorepos。

如果组件库在效用或外观上是一致的,我会发布一个单一包。它更容易版本化,开发者也更容易使用。

如果您想发布多个组件,我建议管理独立版本。我曾经处在一个组织的另一端,他们坚持固定版本控制,这很繁琐。

我的库的测试策略应该是什么?

第一个已知的软件测试团队是在20世纪50年代创建的,所以我猜关于软件测试的争论比关于Web框架的争论要早几十年。

即使现在,您很可能会碰到对软件测试有不同偏好的人。有些人发誓说单元测试是必需的,其他人则只喜欢端到端测试。这通常被可视化为一个“测试金字塔”、一个“测试奖杯”,或完全不同的形状。无论您致力于实践的测试多边形是什么,建立测试策略都很重要,原因有多个:

  1. 您降低了发布破损库的风险
  2. 您可以在库的预期使用范围内进行测试
  3. 您可以为其他贡献者建立一个先例

在浏览器中进行测试提出了一些在其他形式的软件中不存在的独特挑战。首先,您需要支持多个浏览器。每个浏览器还有多个版本,因此浏览器API将在不同程度上受到浏览器及其版本的支持。

考虑您的用户也很重要。由于您的组件库将包含大量的用户界面控件,它们应该按照用户的期望行为。

让我们看看您可以在测试策略中采用的不同类型测试。

无头浏览器测试

这种方法涉及在没有UI的浏览器中测试您的Web组件。您的测试运行器将在终端中启动一个浏览器,加载您的组件,并运行您的测试用例。

这种方法的好处是:

  • 由于没有浏览器UI开销,测试速度很快
  • 您使用一个真实的浏览器进行测试,而不是一个模拟环境
  • 您的测试关注行为,而不是组件的实现

我特别喜欢Kent C Dodds(浏览器测试工具家族Testing Library的创建者)的这句话:

您的测试越是类似于软件的使用方式,它们就能给您带来更多的信心。

Playwright是一个浏览器测试工具,让您可以在Chrome、Firefox和Safari中运行您的代码。Playwright是一个伟大的通用工具,但如果您特别想测试您的Web组件,那么您可以使用Web Test Runner,它结合了像Playwright这样的浏览器启动器和专注于组件测试的DX。

您甚至可以更进一步,编写测试以确保您的组件在不同的框架中也能工作。PatternFly Elements存储库也在不同的Web框架中进行测试。

视觉回归测试

如果您对组件进行了重大更改,很容易错过微妙的样式回归。

  • [ ] 对两者进行比较

目光敏锐的开发人员会发现这个问题,但如果您在团队中工作,处于快节奏的环境中,或者您正在接受开源贡献,那么拥有一种程序化的方式来检测变化是有意义的。

您可以使用视觉回归工具对您的组件进行视觉快照,变化前后。您可以将此集成到您的测试流程中,以确保对任何视觉变化的快照进行手动审查。

这种方法的好处是:

  • 隔离和发现可能被忽视的小UI问题
  • 协调不同的利益相关者并获得批准。

在传统团队中,设计和开发可能是两个独立的业务部门。设计师可能负责视觉设计,开发人员可能负责实现。快照测试是让设计师参与QA流程的好方法。

快照测试工具的设置并不那么简单,它们通常需要:

  • 快照图像的存储
  • 与像GitHub Actions这样的工具集成以通过/失败流程
  • 一个微型网站来显示快照并接受用户操作

一些有用的快照测试工具包括:

手动测试

一个完整的自动化测试套件意味着您可以花更多时间进行探索性测试,以捕获自动化测试无法找到的问题。

这是一篇关于将可访问性测试集成到您的开发流程中的优秀文章。文章推荐了一些手动测试您的UI是否可访问的方法,我已经稍微调整了一下,以专注于测试组件:

  • 您能否在没有鼠标的情况下使用您的组件?使用简单的仅限键盘的手动测试来评估新组件。
  • 当将浏览器放大设置为200%或更高时,您还能使用您的组件吗?
  • 您的组件有深色模式吗?这个深色模式是否适合对光敏感的人?
  • 使用辅助技术进行测试(VoiceOver、Microsoft Narrator和NVDA是免费选项)。

如果您对您的UI库的可访问性感兴趣,您真的应该是,那么为手动可访问性测试腾出时间。使您的组件适用于视力、听力、认知或运动障碍的人士,也会使您的组件对其他人更易用。

如果您对编写更多可访问组件感兴趣,这里有一些很棒的资源:

静态分析测试

最后,还有静态分析测试工具,如linter,例如ESLint。这些工具审查您的代码,并在您输入时在编辑器中标记问题,有些甚至可以自动修复这些问题。ESLint生态系统有插件,可以发现Web组件的问题。ESLint Plugin Lit就是这样一个例子。

这些测试在您工作时在编辑器中运行,所以添加和运行ESLint的开销很小。您可以在开发生命周期的任何阶段运行linter;提交之前、推送之前、打开拉取请求之前、发布之前等。

linter并不是唯一的静态分析工具,TypeScript是另一个例子。像ESLint一样,它具有IDE集成,所以您的编辑器可以在编译之前发现代码中的问题。

如何为我的组件库编写文档

创建清晰且易于使用的文档是让您的库使用者爱上它的一种方式。您还希望创建能够很好地集成到您的工作流程中的文档。以下是您可以为用户提供组件文档的不同方式。

选项1:README.md

如果您的代码库在GitHub上公开显示,您可以在仓库中编写README文件作为文档。Changesets 通过使用顶层的README作为目录,并链接到仓库中其他README文件来实现这一点。

这种方法的好处是双重的:

  • 您不需要创建和部署文档网站
  • 您现在编写的Markdown文件可以作为以后可能使用的静态网站生成器的输入。

您可以从简单开始,并且可以重新利用您编写的Markdown来生成将来的文档网站页面。

这种方法并不适合像组件库这样需要视觉和交互性的东西,用户可能想要亲自尝试组件。

选项2:由Markdown驱动的文档工具

如果您已经通过选项1编写了文档,您可以将这些文件用作静态网站生成器的输入,例如StarlightDocusaurus

这些工具采用您的Markdown和JavaScript,将其编译为优化文档的静态网站。这很有用,但没有额外的工具,您每次更改组件时都需要更新您的文档网站。

Custom Elements Manifest(CEM)是一个规范,描述了如何将Web组件表示为JSON对象。有一个CEM分析器为您的组件生成此对象。输出可以用来创建围绕Web组件的各种工具。例如,API Viewer元素就是其中之一。

选项3:UI目录工具

除了静态网站生成器之外,还有一些更复杂的目录工具,比如Storybook。这些目录工具展示了您的组件的全部功能。

Storybook拥有强大的插件生态系统,允许开发人员模拟事件、更改视口宽度、执行基本的可访问性测试等。

这种方法很好,但我经常看到它与文档网站一起使用,而不是取代它。

意见时间 (🪙🪙)

过去,我会使用像Storybook这样的工具。我喜欢它的功能丰富和生态系统的优秀。最近,我对放弃像Storybook这样的工具感兴趣,而是将文档和组件目录整合在一起,为用户提供单一资源。其他库也采用这种方法。Shoelace讨论了为什么不在他们的文档网站上使用Storybook。

对于最近一个项目,我创建了一个简单的HTML网站来使用我的组件。这是一种快速且易于发布网站的方法,几乎不需要维护。缺点是这是一种非常静态的方法,不适合快速增长和变化的组件库。

我一直在探索使用custom element manifest自动生成组件文档的可能性,但我还没有找到理想的工作流程。

至于部署您的文档网站,市面上有很多不同的工具可以快速让静态网站上线。我部署静态网站的托管平台是Netlify。但您也可以使用其他任何托管网站。

接下来是什么?

在构建组件库时,您还需要做出更多决策。

如果我遗漏了任何内容,请提出问题或拉取请求将其添加到此文档中。

如果您有兴趣了解更多关于构建组件库的信息,那么请查看我的课程Component Odyssey。本课程涵盖了Web组件、许可证、模块系统、对等依赖项和样式解决方案。

您将学习如何节省自己和公司数周的开发时间,构建用户喜爱的组件,并成为一个面向未来的Web开发人员。

分享于 2024-05-27

访问量 123

预览图片