如何构建 React TS Tailwind 设计系统
为什么您要这样做以及如何做的分步指南。
为什么您要这样做以及如何做的分步指南。
这是什么❓ 另一篇“如何做”的文章(帖子)?!
是的。
这对你有什么好处?
听我说完,这将是值得的。
读完本文后,你将能够构建自己的 React + TypeScript + Tailwind + Styled Components 设计组件库。此外,你还将学习如何使用 StoryBook、Chromatic 和 TSDX 等强大的工具来构建工作流。此外,我们还将学习一些关于设置 React 测试库等工具以及如何修复我们遇到的一些恼人的 TS 错误。
此外,我将尝试以一种有趣且信息丰富的方式解释我为什么以及如何走上这条路。
目录:
- 背景
- 如何
- 后缀
谨慎的开始
这篇文章是从这条推文开始的:
看到它引起了一些关注,我根据 Twitter 法则系好安全带,埋头苦干,开始打字。
对我来说,学习过程中的很多事情,一部分是必需的,一部分是兴趣,总之,就像一段旅程。理解这段旅程,才能知道自己是否已经到达目的地。
所以,我又回到了这里,来到一个新的地方,面临着新的挑战......我写了一些关于我卑微的出身的故事,以及一些我作为一名工程师所面临的其他挑战的故事。
这次经历始于我不久前加入的一家新公司。大约一个月后,我接到一个任务,为客户的一款产品实施白标解决方案。就本文而言,公司的具体业务并不那么重要。我之所以选择这条路,是因为公司目前有大约5个不同的应用,甚至更多正在构思和规划中。在创业圈,这就是常态。
就像我说的,我开始为其中一款产品贴牌,但对可用的资源(例如共享包、组件库等)却不太了解。我开始浏览不同组织的 Github Repos,并开始与人们交流,试图了解有哪些资源可以用来完成手头的任务。
我最终找到了 3 个不同的“通用” React 组件库,它们并非在所有应用程序中都使用,但有些……这真是各有不同……作为一家初创企业,代码库中的很多代码都是(现在仍然)“被砍掉”的。这并不是在批评其他工程师,我自己也经历过类似的情况……
所有前端应用都是用 React 构建的,并且在某些时候采用了Styled Components。有些还混合使用了 SaSS,有些使用了TypeScript,有些则使用了 Flow。
我必须开始着手我的任务,所以我就照做了,希望能找到解决办法,同时了解情况的成因以及如何改进。为了完成我的任务,我研究了 Styled Components 的主题功能。但我发现,部分主题是从应用程序传递过来的,而有些则被某个共享包覆盖了。
我也开始在Twitter上评估我的方法:
最后,我找到了一个解决方案并分享给大家:
对我来说,这显然是目前可行的解决方案,但是这引发了我们思考,如果其他客户想要自己的白标,我们需要做什么。
在与团队其他成员分享了所有乐趣,以及之前围绕共享软件包的讨论之后,我意识到一些关于构建设计系统的流程已经开始,但由于各种原因被扼杀在萌芽状态。我认为现在是重启这个过程的好时机,我的同事和工程领导(幸运的是😅)也同意了。
问题
- 跨存储库和应用程序的分散组件。
- 不同的团队致力于多种产品。
- 开发人员无法知道他们可以使用什么。
- 设计师重复设计或重新设计已经存在的组件。
- 产品无法看到有哪些新功能可用。
我们的目标是减少整个组织中编写的组件数量,共享代码,并能够看到我们拥有的内容,同时拥有一个可以管理版本控制和分发的单一存储库。
解决方案是不可避免的——我们需要一个设计系统。
什么是设计系统?
关于这个术语的正确定义,网上有很多资料,也存在不同的观点。我已经读过六篇帖子,解释它的含义。对于不同的职业,它的含义似乎有所不同。
对于开发者来说,这可能是一个共享组件库(或软件包),例如 Material-UI、ChakraUI、BaseUI 等。但对于设计师来说,它可能是一个包含所有设计的 Sketch 文件。我甚至听过产品人员称之为 UI 工具包。
我认为我最喜欢的一般定义是Audrey Hacq在她的文章“您需要了解的有关设计系统的一切”中所述的:
“设计系统是唯一的真相来源,它将所有允许团队设计、实现和开发产品的元素组合在一起。”
如今,设计系统风靡一时。它们是产品设计一致性、更好地与利益相关者沟通以及快速开发新应用的关键。无论你喜不喜欢,它们都是祸福相依。它们有助于跨平台协调,但创建、采用和维护起来却很困难。
https://www.learnstorybook.com/design-systems-for-developers/react/en/introduction/
走向绘图板
我之前忘了说,我们有一个共享库使用了Bit,并且我们有一些 Bit 组件,这些组件在各个应用程序中很少使用。如果你不熟悉 Bit,它的要点是你可以通过他们的云平台单独构建、版本控制和分发组件。这是一个非常强大的超级模块化概念。他们网页上的承诺让你对构建真正可组合的东西感到兴奋。
这就是为什么在第一次迭代中我想出了这个图表:
这看起来是个不错的计划。然而,事情并不总是按照我们的计划进行……
简而言之,在我看来,它对我们的用例来说并不值得。即便如此,我读过一篇发表在 Bit 的博客“Bit's and Pieces”上的文章,标题很贴切,叫《我们如何构建设计系统》,一开始就让我非常乐观。然而,Bit 主页上那些光鲜亮丽的营销信息,却未能实现这个组件组合的乌托邦世界。
我使用这项技术的整个过程值得写一篇博客文章(我甚至已经在笔记中写下了标题:“他们没有告诉你的关于 Bit 的事情”😅)。
我根据能找到的每一份文档和示例,精心设计了整个工作流程,但最终还是不太理想。具体来说,围绕工作流程,我设想提升团队的协作和效率,但有了 Bit,对于任何新加入项目的开发人员来说,这似乎都负担过重。
归根结底,Bit 与 Git 的兼容性不佳。在我看来,拥有一个包含 Git、代码审查和设计审查的精简流程至关重要。
话虽如此,我对Bit没什么不好的评价。我认为它潜力巨大,但尚未完全发挥,没有兑现承诺。不过我会密切关注他们,他们或许会给我们带来惊喜。
但有一个美好的结局......
写下以上文字几周后,我很高兴地说,在我的好友兼队友Yonatan Katz的帮助和坚持下,我们终于能够使用 Bit 实现我们想要的工作流程。整个设计系统都归属于一个仓库,同时每个组件都单独发布到Bit云端 😃 。鉴于修改这些段落会破坏故事的叙事,我选择保留原样,用这篇小文来分享这次挑战的圆满结局和感想。
至于我自己,我需要一个新的计划......
风之物语
我是个开发新闻迷,也是一名被各种炒作驱动的开发从业者 (😜 )。正因如此,我才对 TailwindCSS 赞不绝口。我读到的每一篇文章或推文,都会提到 Tailwind 有多好。此外,我听过的每三个播客,就有一个是Adam Wathan 的节目,或者有人提到他。
Nader Dabit的这条推文展示了我的经历:
给我印象最深的是Max Stoiber的文章《我为什么喜欢 Tailwind》。在文中,他很好地阐述了 Tailwind 的关键特性:该框架的核心是其设计令牌:
“Tailwind 受欢迎的关键在于该框架核心精心构建的设计令牌系统。”
Max 进一步阐述了他所看到的缺点,并提出了一种避免这些缺点的方法。答案就是——twin.macro。
它的要点是它是一个在构建时运行的 Babel 宏,并创建任何给定页面所必需的 Tailwinds 关键 CSS 样式,并且可以使用 JS 库(如 Styled Components 或 Emotion)中的 CSS 进行扩展。
再次,这似乎是一个可靠的计划。
有些人不喜欢 Tailwind,当然,他们的观点也有一定的道理。你可以从不同的角度阅读Jared White的《为什么 Tailwind 不适合我》 。
进入 TSDX
一旦我决定放弃 Bit,为了启动这个项目,我需要一种方法来构建一个模式库,或者更简单地说是一个包。
由于 JavaScript 已死,TypeScript 将成为继任者(当然,我是开玩笑的!😉),我希望找到一种无需过多配置就能轻松启动代码库的方法。于是,我发现了Jared Plamer的项目TSDX。
对于那些不熟悉Jared的人来说,他参与了formik和razzle等项目的开发。此外,有人听说他偶尔会出现在一个疯狂的节拍制作人旁边,戴着颈链,穿着带有美国白头鹰图案的平角短裤(Kenny ,大声喊出来!✊🏽),在Undefined Podcast上对着麦克风讲话。
该项目的标语完美地概括了这一切:
“TSDX 是一个零配置 CLI,可帮助您轻松开发、测试和发布现代 TypeScript 包——因此您可以专注于您出色的新库,而不必再浪费一个下午的时间进行配置。”
它还带有内置模板,其中一个正是我所寻找的react-with-storybook
。
帮助我熟悉 TSDX 的一篇文章是Gabriel Abud撰写的“在 10 分钟内构建您的第一个 Typescript 包” 。
简而言之,它确实做到了它所承诺的功能,而且学习曲线非常低。此外,它还有一个非常巧妙的目录example
,它是一个 React 应用的测试平台,可以用来测试你的代码。与使用Rollup打包器的 TSDX 不同,它使用Parcel来运行(这并非特别重要,我只是觉得它很有趣)。你可以将打包好的代码导入到 Parcel 中进行测试。
不过,值得一提的是,TSDX 附带预配置的 Github Actions,可以用来测试和构建你的软件包。我之前不太了解,也误解了矩阵测试,它.github/workflow/main.yml
有一个节点矩阵配置,可以启动不同类型的操作系统来测试软件包。
请注意,不同操作系统的 Github Action 分钟数计算方式有所不同。这可能会导致你的预算或免费分钟数分配很快用完。吸取我的经验教训吧。我吃过不少苦头,最终把所有 Action 都迁移到了 CircleCI。😳
使用 Chromatic 获取 UI 反馈
我喜欢并推荐与 Storybook 搭配使用的另一个工具是 Chromatic。我是在阅读 Storybooks 的文章《面向开发者的设计系统》时偶然发现它的。它能帮助你更好地管理组件库的整个工作流程。你可以轻松地从团队成员那里获得反馈,它有助于进行可视化测试(在我看来,它几乎让快照测试变得多余),它还能成为你 PR 流程的一部分,并将你的 Storybook 发布到云端。除此之外,它的设置也非常简单(稍后我会详细介绍)。
整合所有
好了,工具都准备好了,是时候开始把这些点连接起来了。我启动了一个新的 TSDX 项目,安装了 Styled Components,然后尝试设置twin.macro
。然而,我遇到了一个问题……在Twin 示例仓库中,没有 Styled Components + Storybook 的示例,所以我配置了一些看似合理的配置。然后,我添加了一些示例,将它们导入到一个新的 Story 中,并尝试运行 Storybook。结果并没有像预期的那样运行。有些代码运行正常,但其他使用该tw
语法的组件却无法运行:
import React from 'react';
import 'twin.macro';
export const Logo = () => (
<a
// Use the tw prop to add tailwind styles directly on jsx elements
tw='w-32 mb-10 p-5 block opacity-50 hover:opacity-100'
href='https://github.com/ben-rogerson/twin.macro'
target='_blank'
rel='noopener noreferrer'
>
<TwinSvg />
</a>
);
const TwinSvg = () => (
<svg fill='black' viewBox='0 0 100 35' xmlns='http://www.w3.org/2000/svg'>
<path d='m31.839 11.667c0-6.2223-3.3515-10.111-10.054-11.667 3.3514 2.3333 4.6082 5.0556 3.7704 8.1667-0.4781 1.7751-1.8653 3.0438-3.4009 4.4481-2.5016 2.2877-5.3968 4.9354-5.3968 10.718 0 6.2223 3.3515 10.111 10.054 11.667-3.3515-2.3333-4.6083-5.0556-3.7704-8.1667 0.478-1.775 1.8653-3.0438 3.4009-4.4481 2.5015-2.2877 5.3967-4.9354 5.3967-10.718z' />
<path d='m-2.7803e-7 11.667c1.4828e-7 -6.2223 3.3515-10.111 10.055-11.667-3.3515 2.3333-4.6083 5.0556-3.7705 8.1667 0.47806 1.7751 1.8653 3.0438 3.4009 4.4481 2.5016 2.2877 5.3968 4.9354 5.3968 10.718 0 6.2223-3.3515 10.111-10.054 11.667 3.3515-2.3333 4.6083-5.0556 3.7704-8.1667-0.47805-1.775-1.8653-3.0438-3.4009-4.4481-2.5015-2.2877-5.3967-4.9354-5.3967-10.718z' />
<path d='m50.594 15.872h-3.9481v7.6c0 2.0267 1.3373 1.995 3.9481 1.8683v3.0717c-5.2853 0.6333-7.3867-0.8233-7.3867-4.94v-7.6h-2.9292v-3.2933h2.9292v-4.2534l3.4386-1.0133v5.2667h3.9481v3.2933zm21.324-3.2933h3.6297l-4.9988 15.833h-3.3749l-3.3113-10.672-3.3431 10.672h-3.375l-4.9987-15.833h3.6297l3.0884 10.925 3.3431-10.925h3.2794l3.3113 10.925 3.1202-10.925zm7.8961-2.375c-1.2099 0-2.1969-1.0134-2.1969-2.185 0-1.2033 0.987-2.185 2.1969-2.185s2.1969 0.98167 2.1969 2.185c0 1.1717-0.987 2.185-2.1969 2.185zm-1.7193 18.208v-15.833h3.4386v15.833h-3.4386zm15.792-16.245c3.566 0 6.1131 2.4067 6.1131 6.5233v9.7217h-3.4386v-9.3733c0-2.4067-1.401-3.6734-3.566-3.6734-2.2606 0-4.0436 1.33-4.0436 4.56v8.4867h-3.4386v-15.833h3.4386v2.0266c1.0507-1.6466 2.77-2.4383 4.9351-2.4383z' />
</svg>
);
之后,我尝试将输出代码拉入我们有用的示例存储库中,这似乎有效。
我继续摆弄和尝试,甚至联系了创作者Ben Rogerson :
他确实帮助我理解了如何为 Twin 添加一些 Tailwind 智能感知:
但我仍然无法在我的库中实现上述语法。我把它放在一边,继续开发,因为我公司有计划,也愿意开始开发这个库。不过,我渴望在某个时候重新开始。
您可以在此处查看 TSDX-Twin repo,其中 3 个组件中有 1 个可以运行。
我最终做的是从头开始创建一个 repo,并使用 Styled Components + Tailwind vanilla。
我可以继续讲述向我的团队推销整个想法的过程,并提及围绕这个项目的所有讨论......但这可能不是你来这里的原因......
好了,故事时间结束了。我们开始正事吧!
设置 TSDX
为了写这篇文章,我将创建一个新的代码库,并在撰写本文时逐步完成。我会提交每一步,以便您可以继续操作或直接查看提交内容。
让我们从启动一个新的 TSDX 项目开始:
// In your terminal / command line run:
npx tsdx create new-project-name
- 这将安装所有初始包并创建一个
new-project-name
文件夹。 - 完成该步骤后,系统将提示您选择一个模板:
- 选择
react-with-storybook
。 - 将安装必要的 NPM 模块。
- 一旦完成,您将收到此确认:
- 现在我们可以
cd
进入目录并运行yarn start
以开始在监视模式下进行开发,但是由于我们正在处理 React 组件库并想要练习 Storybook 驱动开发(SDD),所以我们可以直接运行yarn storybook
并继续进行。 - 在新的 TSDX 项目上启动 Storybook 将产生令人惊叹的“snozzberies”组件:
虽然没有什么值得大书特书的,但是是一个好的开始。
如果我们在编辑器中打开我们的项目,我们应该看到以下文件夹结构:
让我们细分一下文件夹和文件:
.github
:生成的 Github 操作(如果您不熟悉它们,我建议您在这里阅读相关内容),它们的目的是自动化您的工作流程并实现 CI(持续集成)。在此文件夹下我们有 2 个文件:main.yml
:GitHub 操作指南,介绍如何安装依赖项、检查代码、运行测试以及构建软件包。它可以在不同的 Node 版本矩阵和不同的操作系统上运行(正如我之前提到的,了解这一点很重要)。size.yml
:这个小工具可以帮助您跟踪包的大小,该配置在属性package.json
下设置"size-limit"
。生成的输出文件默认大小为 10KB。您可以根据需要更改它。此操作会在您将代码推送到 Github 仓库时运行,如果超出限制,则会导致检查失败。
.storybook
:这是你的 Storybook 配置所在的位置。你可以在“设置”部分阅读更多相关信息。.vscode
:仅当您使用VScode时才会生成此文件夹。由于我在此项目中使用了它,因此它已创建并包含我的工作台设置。我还添加了推荐的扩展程序,如果您决定克隆此代码库,可以尝试使用这些扩展程序。dist
:库代码的打包输出。这部分内容主要供此包的用户使用。它包含压缩后的esm
(ES 模块)和cjs
(Common JS)文件,以及源码映射和TypeScript
类型定义。example
:包含我上面提到的 Playground React App。它不会包含在 bundle 中,也不会发布到你选择使用的任何包管理器中。node_modules
:所有 JavaScript 安装包模块所在的位置。src
:真正的魔法就在这里。这是主要的源文件和代码,它们将被编译到我们的dist
文件夹中。此目录中有一个index.tsx
文件,用于导入您创建的所有其他源文件。在新创建的 TSDX 项目中,您将获得前面提到的“snozzberies”组件的代码。stories
:Storybook故事存放的地方。我们需要将编写的代码放在这里,以便显示 UI。“Snozzberries”组件在这里导入和渲染。test
:我们将在此目录中编写测试。首次打开它时,会发现生成组件的测试位于 下blah.test.tsx
。
除了这些文件夹之外,我们还有常规的通用文件,例如:
package.json
- 显示了我们的依赖列表。gitignore
- 忽略来自的文件git
。LICENSE
- 自动生成 MIT 许可证,可随意更改。README.md
- 此文件由 TSDX 生成,包含更多关于如何使用该工具的信息。如果您计划将此项目发布为软件包,我建议您更改此设置,并写下关于如何安装和使用库的清晰说明。tsconfig.json
:由 TSDX 生成的 TypeScript 配置文件。它由维护人员预先配置,但方式略有不同。我建议保留原样,除非您知道自己在做什么,或者需要一些特别不同的配置。
安装 Tailwind
⚠️ 请记住:Tailwind CSS 需要 Node.js 12.13.0 或更高版本。
要将 Tailwind 纳入其中,您可以参考其安装文档(本节内容与此类似,但我认为将其放在本文中会更方便使用)。由于我已经这样做过(好几次了😅),为了兼容 TSDX 和 Storybook,我们需要使用PostCSS 7 兼容版本。
如果您不熟悉PostCSS,简而言之,它是一个将 CSS 转换为 JavaScript 的工具,允许我们“今天使用明天的 CSS”。它是一个 CSS 预处理器,类似于 SaSS,但有一些区别。
让我们通过运行以下命令来安装依赖项:
yarn add -D tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat postcss@^7 autoprefixer@^9
# alternatively: run with npm install -D ...
注意,我把所有东西都安装成了开发依赖项,因为这是一个包,我们希望将依赖项保持在最低限度,以便我们的工具能够正常工作。对于某些东西,我们可能需要更新
package.json
“peerDependencies”。
接下来,我们需要postcss
在项目的根目录中添加一个配置:
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
现在我们可以用以下方式初始化 Tailwind:
npx tailwindcss init
这也将在根目录中创建一个tailwind.config.js
文件,您可以在其中根据需要添加和自定义配置:
// tailwind.config.js
module.exports = {
purge: [],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {},
plugins: [],
};
接下来,我们需要在 CSS 中包含 Tailwind,在我们的例子中,我们需要两样东西:
- 为了能够将 CSS 导入到组件中,我们需要告诉 TSDX 如何将其添加到代码中。为此,我们需要安装
rollup-plugin-postcss
(因为 TSDX 使用 rollup)。 CSS
在我们的目录中创建一个文件src
,我们将在任何想要使用 Tailwind 的组件中使用它。
好的,现在让我们添加rollup-plugin-postcss
:
yarn add -D rollup-plugin-postcss
TSDX 是完全可定制的,您可以添加任何
rollup
插件,但请注意,它会覆盖默认行为
现在我们将tsdx.config.js
在根目录中创建一个文件,并在其中放入以下代码:
// tsdx.config.js
const postcss = require('rollup-plugin-postcss');
module.exports = {
rollup(config, options) {
config.plugins.push(
postcss({
config: {
path: './postcss.config.js',
},
extensions: ['.css'],
minimize: true,
inject: {
insertAt: 'top',
},
})
);
return config;
},
};
这给出了我们的postCSS
路径,它告诉它我们希望在哪些文件上运行。minimize
关键在于允许我们最小化输出。这里最重要的键是“ inject
”。我们将其设置为“ top
”,以指示CSS 将postCSS
在页面的哪个位置<head>
插入。这对于 Tailwind 至关重要,因为它需要比任何其他样式表具有最高的优先级。
接下来,对于第 2 部分,我们将在目录tailwind.css
下创建一个(可以命名为任何其他名称)文件src
并将其粘贴到:
// src/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
太棒了!这应该能让我们完成任务了。
让我们在当前唯一拥有的组件上检查一下它是否有效:
// src/index.tsx
import React, { FC, HTMLAttributes, ReactChild } from 'react';
// ! Add the CSS import statement !
import './tailwind.css`;
// ...
// we'll add some Tailwind classes on our components to test
export const Thing: FC<Props> = ({ children }) => {
return (
<div className="flex items-center justify-center w-5/6 m-auto text-2xl text-center text-pink-700 uppercase bg-blue-300 shadow-xl rounded-3xl">
{children || `the snozzberries taste like snozzberries`}
</div>
);
};
现在我们运行 StoryBook( yarn storybook
) 并看一下:
这是一个很漂亮的“snozzberries”组件!
现在,是时候整理和准备一下我们的包了,这样我们就可以拥有不止一个组件了。为此,我们将把保存我们心爱的“snozzberries”组件的文件的名称从 更改为index.tsx
。Thing.tsx
然后,我们将创建一个新index.tsx
文件,导出所有组件,并让 TSDX 完成它的工作:
// index.tsx:
export * from './Thing';
// We use the "*" export to get everything out of our file: components and types.
现在,让我们看看我们没有破坏任何东西,并通过运行来查看我们的测试是否正常运行:
yarn test
我们得到这个输出:
您可能会想:“这是怎么回事?”
好吧,Jest 不知道怎么读取CSS
。而且,它也不太在意这个,所以我们只能用 来模拟它identity-obj-proxy
(要了解更多原因,请点击此处)。让我们添加它:
yarn add -D identity-obj-proxy
接下来,我们需要通过将此代码片段添加到我们的文件来将 Jest 指向正确的文件类型package.json
:
// package.json
...
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less|scss|sass)$": "identity-obj-proxy"
}
},
...
现在我们可以再次运行测试,并查看新的结果:
添加样式组件
现在我们已经做好了一切准备,让我们看看 Styled Components 是如何融入其中的……
首先,让我们安装包及其 TypeScript 类型:
yarn add -D styled-components @types/styled-components
现在让我们保持简单并从构建一个Button
组件开始(原始的,是的,我知道......):
// src/Button.tsx
import React, { FC } from 'react';
import styled from 'styled-components';
const StyledButton = styled.button`
background-color: blue;
color: white;
`;
export interface ButtonProps {
text: string;
}
export const Button: FC<ButtonProps> = ({ text }) => {
return <StyledButton>{text}</StyledButton>;
};
我假设你已经具备 Styled Components 的基础知识。如果你是新手,可以查看文档网站。
我们需要将其添加到我们的index.tsx
:
export * from './Thing';
export * from './Button';
为其添加一个故事,以便我们可以看到它:
// stories/Button.stories.tsx
import React from 'react';
import { Meta, Story } from '@storybook/react';
import { Button, ButtonProps } from '../src';
const meta: Meta = {
title: 'Button',
component: Button,
argTypes: {
text: {
control: {
type: 'text',
},
},
},
parameters: {
controls: { expanded: true },
},
};
export default meta;
const Template: Story<ButtonProps> = (args) => <Button {...args} />;
export const SCButton = Template.bind({});
SCButton.args = { text: 'Button' };
瞧!我们丑陋的按钮就完成了:
当然,我们可以做得更好...让我们删除样式并添加一些 Tailwind 类:
// src/Button.tsx
import React, { FC } from 'react';
import styled from 'styled-components';
const StyledButton = styled.button``;
export interface ButtonProps {
text: string;
}
export const Button: FC<ButtonProps> = ({ text }) => {
return (
<StyledButton className='px-8 py-2 font-semibold text-white transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400'>
{text}
</StyledButton>
);
};
现在我们有了这位英俊的家伙:
我们的 Styled Components 中仍然有一些样式,但我们实际上并不需要,而且我们的 JSX 代码有点长且混乱。如果我们将类合并到 Styled Components 中,应该会使其更加简洁,并使我们的关注点更加清晰。为此,我们将使用[attrs
API(https://styled-components.com/docs/api#attrs),它允许我们将 props 附加到 Styled Components 中:
// src/Button.tsx
import React, { FC } from 'react';
import styled from 'styled-components';
const StyledButton = styled.button.attrs(() => ({
className:
'px-8 py-2 font-semibold text-white transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400',
}))``;
export interface ButtonProps {
text: string;
}
export const Button: FC<ButtonProps> = ({ text }) => {
return <StyledButton>{text}</StyledButton>;
};
这种方法非常灵活。怎么做到的呢?假设我们现在想通过按钮“variant”更改文本颜色。我们可以通过向 中添加一个 prop 来实现Button
,我们可以通过更改我们使用的 Tailwind 类名来更改它,或者使用 prop 并通过 Styled Component 字符串插值来更改它。
首先,我们将向variant
组件接口添加一个 prop,并添加 2 个可能的值:
export interface ButtonProps {
text: string;
variant?: 'default' | 'warning';
}
传入:
// we set our "default" variant to... Um, well, to "default" ¯\_(ツ)_/¯
export const Button: FC<ButtonProps> = ({ text, variant = 'default' }) => {
return <StyledButton variant={variant}>{text}</StyledButton>;
};
稍等一下!我们遇到了 TypeScript 错误!
注意到“variant”下面的波浪线了吗?简而言之,TS 告诉我们“你传入了一个我不知道的参数”。让我们来解决这个问题:
// attr function needs the type, but also the "styled" function itself
const StyledButton = styled.button.attrs(
({ variant }: { variant: ButtonVariants }) => ({
className: `px-8 py-2 font-semibold text-white transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400`,
})
)<{ variant: ButtonVariants }>``;
// extract the type out from the interface for reuse.
type ButtonVariants = 'default' | 'warning';
export interface ButtonProps {
text: string;
variant?: ButtonVariants;
}
// There are a ton of other fancy ways of doing this in TS.
言归正传……所以,使用新的variant
prop 来改变文本颜色的一种方法就是使用模板字面量,并为 选择一个不同的 Tailwind 类名text
。另一种方法是在 Styled Components 的反引号中使用相同的 prop:
// Option 1️⃣ :
const StyledButton = styled.button.attrs(
({ variant }: { variant: ButtonVariants }) => ({
className: `px-8 py-2 font-semibold ${
variant === 'default' ? 'text-white' : 'text-red-700'
} transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400`,
})
)<{ variant: ButtonVariants }>``;
// Option 2️⃣ :
const StyledButton = styled.button.attrs(() => ({
className: `px-8 py-2 font-semibold text-white transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400`,
}))<{ variant: ButtonVariants }>`
color: ${({ variant }) => (variant === 'warning' ? 'red' : '')};
`;
这个选项的缺点2
是没有 Tailwinds 的实用类和颜色主题来帮助我们设置样式。然而,如果你仔细想想,混合搭配这两种方法会非常有效。
最后还有一点很有用,那就是使用一个类似这样的库,[tailwind-classnames](https://github.com/muhammadsammy/tailwindcss-classnames)
它可以帮助验证你使用的类名是否正确,如果你没有这样做,TS 会向你发出警告。它拥有已知库的全部功能和 API [classnames](https://www.npmjs.com/package/classnames)
,因为它只是已知库的一个扩展。
添加 React 测试库
我不会解释为什么你应该使用 React Testing Library,或者它和 Enzyme 有什么不同。我只想说,我认为它很棒,而且在我看来,你应该使用它。
解决了这个问题...让我们通过运行以下命令将其添加到我们的项目中:
yarn add -D @testing-library/react @testing-library/jest-dom
接下来,让我们为Button
组件添加一个测试:
// test/Button.test.tsx
import * as React from 'react';
import { render } from '@testing-library/react';
// This below import is what gives us the "toBeInTheDocument" method
import '@testing-library/jest-dom/extend-expect';
// As we are using the Component Story Format we can import it from our
// previously written story.
import { SCButton as Button } from '../stories/Button.stories';
describe('Button', () => {
it('should render the button without crashing', () => {
// render the button and get the getByRole method
const { getByRole } = render(<Button text='test' />);
// getByRole as its name gets a selector by its role.
// in this case we're looking for a `button`.
// then we make sure it's in the document
expect(getByRole('button')).toBeInTheDocument();
});
});
我们还想确保除了渲染之外,它还能被点击。所以我们也来检查一下:
// ... same imports except:
// we've added the fireEvent method to simulate a user click
import { render, fireEvent } from '@testing-library/react';
describe('Button', () => {
//... our former test
it('should call the onClick method when a user clicks on the button', () => {
// mock out our OnClick function
const mockClick = jest.fn();
const { getByRole } = render(<Button text='test' onClick={mockClick} />);
// we store a variable with the button element
const buttonElement = getByRole('button');
// Simulate a user clicking on the button
fireEvent.click(buttonElement);
expect(mockClick).toHaveBeenCalledTimes(1);
});
});
让我们尝试并确保测试能够正常工作yarn test
。
但这是什么😱?
由于 TypeScript 错误,测试失败...🤦🏽♂️
别怕!我们可以修复它……我们回到我们的Button
文件:
// src/Button.tsx
// add the relevant type import
import React, { FC, ButtonHTMLAttributes } from 'react';
// ...
// We'll add the relevant missing type by extending our interface:
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
text: string;
variant?: ButtonVariants;
}
// make sure we pass all the rest of the props to our component:
export const Button: FC<ButtonProps> = ({
text,
variant = 'default',
...rest
}) => {
return (
<StyledButton variant={variant} {...rest}>
{text}
</StyledButton>
);
};
现在我们都是绿色的!
还有一个值得演示的测试是针对带有动态 Tailwind 类的按钮。如果你还记得的话,我们正在测试选项 2️⃣:
const StyledButton = styled.button.attrs(
({ variant }: { variant: ButtonVariants }) => ({
className: `px-8 py-2 font-semibold ${
variant === 'default' ? 'text-white' : 'text-red-700'
} transition duration-500 ease-in-out transform rounded-lg shadow-xl bg-gradient-to-r from-red-300 to-blue-300 hover:from-pink-400 hover:to-indigo-400`,
})
)<{ variant: ButtonVariants }>``;
text-white
我们可以轻松测试,当存在变体时,我们期望得到的类名default
,以及我们确实拥有text-red-700
该变体对应的类名warning
。让我们添加这个测试:
it('should have the right text color class name for variants', () => {
// we extract the "rerender" method to test both variants
const { getByRole, rerender } = render(<Button text='test' />);
const buttonElement = getByRole('button', { name: 'test' });
// if you recall, passing no variant, defaults to "default" variant.
// this is a bit robust, but it serves to illustarte the point
expect(buttonElement.classList.contains('text-white')).toBe(true);
expect(buttonElement.classList.contains('text-red-700')).toBe(false);
// render the other "warning" variant
rerender(<Button text={'test'} variant='warning' />);
// test the opposite of the above:
expect(buttonElement.classList.contains('text-white')).toBe(false);
expect(buttonElement.classList.contains('text-red-700')).toBe(true);
});
如果您使用 Styled Components 添加任何动态 CSS 样式,请使用它
jest-styled-components
进行测试。
使用 TSDX 示例进行健全性检查
现在我们已经测试了新添加的组件,如果我们想更加确信我们的按钮可以与我们输出和捆绑的代码一起工作,我们可以使用 TSDX 示例 repo。
为此,我们将使用以下命令构建代码:
yarn build
然后我们可以转到我们的example
文件夹并安装我们的依赖项:
cd example && yarn install
接下来,我们将导入按钮并将其添加到示例应用程序中:
// example/index.tsx
import 'react-app-polyfill/ie11';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
// the importing location is automatically `dist` folder
import { Thing, Button } from '../.';
const App = () => {
return (
<div>
<Thing />
<Button text="test" />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
我们将使用启动示例应用程序yarn start
,然后我们将访问http://localhost:1234
并看到以下内容:
现在,我们在“snozzberries”组件下找到了按钮。看起来一切正常!
设置半音阶
正如我所提到的,Chromatic 是构建、测试和协作设计系统的完美工具。要开始使用,您可以按照他们的文档进行操作,或者直接在Chromatic 网站注册。
注册完成后,请前往您的仪表盘并创建一个项目,您可以选择一个现有的 GitHub 仓库作为起点。项目创建完成后,您需要安装 Chromatic 软件包:
yarn add --dev chromatic
然后,您可以使用以下命令发布您的 Storybook:
npx chromatic --project-token=<your_project_token>
该过程还将指导您完成该过程并npm
为您创建脚本:
打开“继续设置”链接,我们进入此屏幕:
现在我们可以测试并展示 Chromatic 的工作原理,只需点击“捕捉 UI 变化”按钮即可。为此,让我们修改一下某个组件。经典的“Snozzberries”背景就是一个很好的例子:
// src/Thing.jsx
// ...
// I've changed the bg-blue-300 class to bg-yellow-300 which is the background color:
export const Thing: FC<Props> = ({ children }) => {
return (
<div className='flex items-center justify-center w-5/6 m-auto text-2xl text-center text-pink-700 uppercase bg-yellow-400 shadow-xl rounded-3xl'>
{children || `the snozzberries taste like snozzberries`}
</div>
);
};
再次,让我们运行 Chromatic 脚本,但现在我们可以使用我们新添加的npm
带有项目令牌的脚本:
yarn chromatic
这一次,在过程结束时我们将看到一条消息和一个错误:
然后回到 Chromatic 网站,我们看到的是:
现在点击“欢迎”组件(我们的“snozzberries”组件,我们应该在其故事中重命名😬),这将带我们进入比较屏幕:
在右侧,我们可以看到组件的新“状态”以绿色突出显示。请注意,这不是我们实际设置的颜色,而只是“发生了变化”的颜色。右上角的三个按钮可以切换显示实际的新视觉效果,点击“差异”按钮即可显示:
我们可以点击“接受更改+继续”,这将引导我们获得有关反馈流程的更多解释。
Chromatic 使我们能够围绕 UI 库的构建创建一个工作流程,您可以在其中与开发团队成员和设计师协作,从而简化沟通。为此,强烈建议将其与您的 CI 集成。这样,您就可以将其作为 PR 流程的一部分:在审查代码更改时,您也会审查 UI 更改。
生产准备
开发完成后,我们希望确保我们的包已准备好发布并正确使用。为此,TSDX 为我们提供了另一个便捷的工具——检查脚本。包的默认大小限制在以下属性下size
定义:package.json
size-limit
// package.json
{
// ...
"size-limit": [
{
"path": "dist/react-tw-blog-post.cjs.production.min.js",
"limit": "10 KB"
},
{
"path": "dist/react-tw-blog-post.esm.js",
"limit": "10 KB"
}
],
//...
}
要运行它,我们应该确保所有代码都已构建,然后我们可以size
通过执行以下命令来运行脚本:
yarn build && yarn size
但这是什么?
我们只有 2 个组件,大小却超过 300KB???这似乎不对。
发生这种情况是因为我们在配置中遗漏了一些东西……更确切地说,我们在准备使用 Tailwind 进行生产环境的项目时,忘记了一个关键配置——清除 CSS。如果没有这一步,我们的 CSS 打包文件就会被294.0KB
Gzip 压缩。
按照 Tailwinds 的“删除未使用的 CSS ”部分,我们可以将此配置添加到我们的包中:
// tailwind.config.js
module.exports = {
purge: [
'./src/components/**/*.tsx',
// This is a convention for files that only include Styled Components.
// If you don't use this convention feel free to skip it.
'./src/components/**/*.styled.tsx',
],
darkMode: false, // or 'media' or 'class'
theme: {
extend: {},
},
variants: {
extend: {},
},
plugins: [],
};
另一个重要的注意事项是,为了使 Tailwind 真正运行清除过程,必须将NODE_ENV
设置为 来运行production
。因此,让我们修改一下构建脚本:
// package.json
{
// ...
scripts: {
// ..
build: 'NODE_ENV=production tsdx build';
// ...
}
// ...
}
最后,我们可以重新运行yarn build && yarn size
。现在我们的输出将是:
虽然仍然低于 TSDX 的默认值10KB
,但好多了。正如 CLI 输出中的绿色文本所示,您可以根据需要增加限制。
要优化打包大小输出,还有很多事情要做,其中大部分都与 Tailwind 的使用方式和功能有关。例如,您可以禁用预检设置,这将移除所有 CSS 重置,并减少一些 KB 大小。
最后,您应该整合并最小化默认 Tailwind 配置中使用的内容。根据您的需求进行调整,并尝试让您的设计师自行选择并最小化这些选项。您的项目可能不需要颜色[84
……
查看你的 Tailwind 配置
还有一个很棒的工具可以帮助开发人员和设计人员完成这样的项目tailwind-config-viewer
。正如 repo 页面上所述:
“用于可视化您的 Tailwind CSS 配置文件的本地 UI 工具。”
它可以帮助您查看您选择的 Tailwind 配置。您可以npx
在任何包含tailwind.config.js
文件的项目上使用,并查看其内容。只需运行:
npx tailwind-config-viewer
除了展示您选择的配置之外,您还可以将鼠标悬停在任何类上,只需单击鼠标即可复制它。
出版
这篇文章我不会深入探讨这个主题,它值得另写一篇文章来探讨。不过,使用我之前提到的设置,发布到npm
或github
安装包需要一些额外的设置和配置,但不会太多。按照上述步骤构建的工件已经准备好,可以上传到任何仓库了。
我计划后续再写一篇关于如何使用 Github packages + CircleCI / Github Actions + Changesets进行发布的博文。或许,我甚至会讲解如何发布到Bit
。
这已经是一篇太长的帖子了😅。
其他 Tailwind-in-JS 解决方案
我已经提到过twin.macro
,并且最近了解了JS 中的编译 CSS,显然它也应该可以与之兼容。
另一个非常有趣的项目是Twind
。我在推特上发布了我计划撰写这篇文章的消息后,了解到了这个项目。该项目的维护者之一Sascha Tandel联系了我,并邀请我考虑写一些关于它的内容。我还没有机会深入研究和尝试,但我读过它的文档,觉得它相当有趣。
以下是 Sascha 对此的看法:
Twind是一个无需构建步骤的 Tailwind-first CSS-in-JS 库,它允许使用 twind/shim 模块与现有的 Tailwind HTML 无缝集成。此功能可与您喜欢的框架一起使用,无需任何额外设置。
twind/shim
它会动态检测 HTML 文档中使用的 Tailwind 类,创建相应的 CSS 规则,并将其注入样式表。这里有一个示例供您使用。由于使用了编译器(而不是最终输出),样式相关的成本是已知的固定的。无论您编写了多少样式或使用了多少变体,您的用户只需下载大约 12KB 的代码(这比 styled-components 或平均清除的 Tailwind 构建要少)。在服务器上,我们可以使用twind/shim/server生成要包含在 HTML 中的初始 CSS。
与 Tailwind 不同,Twind 不受类名字符串作为输入的限制。使用实用 CSS 时常见的一个痛点是由类名组成的冗长而笨重的代码行,通常在各个断点处表示样式,这很难理解。单个元素应用数十条规则的情况并不少见。Twind 提供了分组语法来组合常见的变体或前缀。响应式变体和伪变体都支持各种组合:bg-red-500 shadow-xs md:(bg-red-700 shadow) lg:(bg-red-800 shadow-xl)
。
由于 Twind 在运行时生成 CSS,因此无需限制变体的使用。每个变体都可以应用于每个类。此外,变体可以像这样堆叠hover:focus:text-blue-700
。Twind 文档站点包含所有扩展的概述。
作为 Tailwind 不支持的所有一次性样式的便捷出口,Twind 允许编写任意 CSS,使其成为一个完整的 CSS-in-JS 解决方案。
从我们的DM中得知,该团队正在开发一个[styled
模块(https://github.com/tw-in-js/twind/pull/7)并支持TypeScript。此外,该软件包内置了“CSS-in-JS”,因此无需使用Styled Components、Emotion或Goober。
Twin 的 API 与之类似twin.macro
,虽然目前仍处于早期版本(撰写本文时为 v0.15.1),但它的未来前景一片光明。我个人已经迫不及待想尝试一下了!现在,这里有一个 Codesandbox 和 React,你可以自己尝试一下 😃:
结论
我希望我能够指导你如何启动这样的项目。我知道我很享受写这篇文章的过程,也从中学到了很多。我认为我在这篇文章中提到的工具非常可靠,绝对有助于提高生产力,但我知道设置起来并不容易。所以我写下了这篇文章,希望有人不必经历我曾经遇到的麻烦。
当然,这种方法可以进行一些调整和改进。显然,这只是一种主观的构建方式。毕竟,我是一个个体,我有我的观点,伙计。
希望(🤞🏽)如果你读到这里,你会喜欢这篇文章。如果喜欢,请分享、评论、点赞,并点击订阅按钮😜。
干杯!🍻
资源:
- 博客文章 Repo
- 面向开发人员的设计系统/
- Tailwind CSS
- TSDX
- React 测试库
- 样式化组件
- 半音阶
- 双胞胎
- twin.macro
- 构建与购买:组件库版本
- Adele - 设计系统和模式库存储库
- 我编写的一些针对 Tailwind CSS 变量颜色的辅助函数。