如何使用 Emotion 创建列表组件

Avatar of Robin Rendle
Robin Rendle

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费额度!

本周我在 Sentry 进行了一些重构,我注意到我们没有一个通用的列表组件,可以在项目和功能之间使用。 因此,我开始着手创建一个,但问题在于:我们在 Sentry 中使用 Emotion 来设置样式,我对它的了解仅限于皮毛,并且在文档中被描述为…

[…] 一个用于使用 JavaScript 编写 CSS 样式的库。 除了具有源映射、标签和测试实用程序等出色的开发者体验外,它还提供强大且可预测的样式组合。 支持字符串和对象样式。

如果您从未听说过 Emotion,其基本思想如下:当我们在拥有大量组件的大型代码库上工作时,我们希望确保可以控制 CSS 的级联。 因此,假设您在一个文件中有一个 .active 类,并且您想确保它不会影响另一个文件中具有 .active 类的完全独立组件的样式。

Emotion 通过向您的类名添加自定义字符串来解决此问题,以便它们不会与其他组件发生冲突。 以下是如何输出 HTML 的示例

<div class="css-1tfy8g7-List e13k4qzl9"></div>

很巧妙,不是吗? 不过,还有许多其他工具和工作流程可以实现类似的功能,例如 CSS 模块

要开始创建组件,我们首先需要将 Emotion 安装 到我们的项目中。 我不会详细介绍这些内容,因为根据您的环境和设置,它们会有所不同。 但是,一旦完成,我们就可以继续创建如下所示的新组件

import React from 'react';
import styled from '@emotion/styled';

export const List = styled('ul')`
  list-style: none;
  padding: 0;
`;

在我看来,这看起来很奇怪,因为我们不仅为 <ul> 元素编写样式,而且还定义了组件也应该渲染一个 <ul>。 将标记和样式都放在一个地方感觉很奇怪,但我确实喜欢它的简单性。 它只是有点扰乱了我的心理模型以及 HTML、CSS 和 JavaScript 之间关注点分离。

在另一个组件中,我们可以导入此 <List> 并像这样使用它

import List from 'components/list';

<List>This is a list item.</List>

我们添加到列表组件的样式将转换为类名,例如 .oefioaueg,然后添加到我们在组件中定义的 <ul> 元素中。

但我们还没有完成! 对于列表设计,我需要能够使用相同的组件渲染 <ul><ol>。 我还需要一个允许我在每个列表项中放置图标的版本。 就像这样

Emotion 的酷(也有些奇怪)之处在于,我们可以使用 as 属性来选择在导入组件时要渲染哪个 HTML 元素。 我们可以使用此属性来创建我们的 <ol> 变体,而不必创建自定义 type 属性或其他内容。 恰好看起来像这样

<List>This will render a ul.</List>
<List as="ol">This will render an ol.</List>

这不仅对我来说很奇怪,对吧? 然而,它非常巧妙,因为这意味着我们不必在组件本身中执行任何奇特的逻辑来更改标记。

在这一点上,我开始记下此组件的完美 API 可能是什么样子,因为然后我们可以从那里倒推。 我想象的是这样

<List>
  <ListItem>Item 1</ListItem>
  <ListItem>Item 2</ListItem>
  <ListItem>Item 3</ListItem>
</List>

<List>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 1</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 2</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 3</ListItem>
</List>

<List as="ol">
  <ListItem>Item 1</ListItem>
  <ListItem>Item 2</ListItem>
  <ListItem>Item 3</ListItem>
</List>

因此,在进行此草图之后,我知道我们需要两个组件,以及能够在 <ListItem> 中嵌套图标子组件的功能。 我们可以像这样开始

import React from 'react';
import styled from '@emotion/styled';

export const List = styled('ul')`
  list-style: none;
  padding: 0;
  margin-bottom: 20px;

  ol& {
    counter-reset: numberedList;
  }
`;

这种特殊的 ol& 语法是告诉 Emotion 这些样式仅在元素作为 <ol> 渲染时才适用。 通常最好在此元素中添加 background: red; 以确保您的组件正确渲染内容。

接下来是我们的子组件 <ListItem>。 值得注意的是,在 Sentry 中我们也使用 TypeScript,因此在定义 <ListItem> 组件之前,我们需要先设置我们的 props

type ListItemProps = {
  icon?: React.ReactNode;
  children?: string | React.ReactNode;
  className?: string;
};

现在我们可以添加 <IconWrapper> 组件,该组件将在 ListItem 中调整 <Icon> 组件的大小。 如果您还记得上面的示例,我希望它看起来像这样

<List>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 1</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 2</ListItem>
  <ListItem icon={<IconBusiness color="orange400" size="sm" />}>Item 3</ListItem>
</List>

IconBusiness 组件是一个预先存在的组件,我们希望将其包装在 span 中以便可以对其进行样式设置。 谢天谢地,我们只需要一点 CSS 即可将图标与文本正确对齐,而 <IconWrapper> 可以为我们处理所有这些操作

type ListItemProps = {
  icon?: React.ReactNode;
  children?: string | React.ReactNode;
  className?: string;
};

const IconWrapper = styled('span')`
  display: flex;
  margin-right: 15px;
  height: 16px;
  align-items: center;
`;

完成此操作后,我们终于可以在这两个组件下方添加 <ListItem> 组件,尽管它要复杂得多。 我们需要添加 props,然后在 icon prop 存在时渲染上面的 <IconWrapper>,并渲染传递给它的图标组件。 我还在下面添加了所有样式,以便您可以看到我如何为每个变体设置样式

export const ListItem = styled(({icon, className, children}: ListItemProps) => (
  <li className={className}>
    {icon && (
      <IconWrapper>
        {icon}
      </IconWrapper>
    )}
    {children}
  </li>
))<ListItemProps>`
  display: flex;
  align-items: center;
  position: relative;
  padding-left: 34px;
  margin-bottom: 20px;
	
  /* Tiny circle and icon positioning */
  &:before,
	& > ${IconWrapper} {
    position: absolute;
    left: 0;
  }

  ul & {
    color: #aaa;
    /* This pseudo is the tiny circle for ul items */ 
    &:before {
      content: '';
      width: 6px;
      height: 6px;
      border-radius: 50%;
      margin-right: 15px;
      border: 1px solid #aaa;
      background-color: transparent;
      left: 5px;
      top: 10px;
    }
		
    /* Icon styles */
    ${p =>
      p.icon &&
      `
      span {
        top: 4px;
      }
      /* Removes tiny circle pseudo if icon is present */
      &:before {
        content: none;
      }
    `}
  }
  /* When the list is rendered as an <ol> */
  ol & {
    &:before {
      counter-increment: numberedList;
      content: counter(numberedList);
      top: 3px;
      display: flex;
      align-items: center;
      justify-content: center;
      text-align: center;
      width: 18px;
      height: 18px;
      font-size: 10px;
      font-weight: 600;
      border: 1px solid #aaa;
      border-radius: 50%;
      background-color: transparent;
      margin-right: 20px;
    }
  }
`;

就是这样! 使用 Emotion 构建了一个相对简单的 <List> 组件。 不过,在完成此练习后,我仍然不确定是否喜欢这种语法。 我认为它让简单的事情变得非常简单,但中等大小的组件比应有的复杂得多。 此外,对于新手来说,它可能会非常令人困惑,这让我有点担心。

但我想一切都是学习经历。 无论如何,我很高兴有机会处理这个小组件,因为它教会了我一些关于 TypeScript、React 和如何使我们的样式更具可读性的好东西。