八、使你的组件看着漂亮

我们进入 React 最佳实践和设计模式的旅程现在已经达到了我们想要使我们的组件看起来漂亮的程度。为了做到这一点,我们将讨论为什么常规 CSS 可能不是设计组件样式的最佳方法的所有原因,并且我们将检查各种替代解决方案。

从内联样式开始,然后是镭、CSS 模块和styled-components,本章将引导您通过 JavaScript 了解 CSS 的神奇世界。

在本章中,我们将介绍以下主题:

  • 常规 CSS 在规模上的常见问题
  • 在 React 中使用内联样式的含义及其缺点
  • Radium 库如何帮助解决内联样式问题
  • 如何使用 Webpack 和 CSS 模块从头开始设置项目
  • CSS 模块的特性以及为什么它们是避免全局 CSS 的一个很好的解决方案
  • styled-components,一个新的库,提供了一种设计组件样式的现代方法

技术要求

要完成本章,您需要以下内容:

  • Node.js 12+
  • Visual Studio 代码

您可以在本书的 GitHub 存储库中找到本章的代码:https://github.com/PacktPublishing/React-17-Design-Patterns-and-Best-Practices-Third-Edition/tree/main/Chapter08

JavaScript 中的 CSS

在社区中,每个人都同意,2014 年 11 月,克里斯托弗·切多在全国 JS 会议上发表演讲时,React 组件的造型发生了一场革命。

克里斯托弗在互联网上也被称为vjeux*,他在 Facebook 工作,并参与 React。在他的演讲中,他以 Facebook 所面临的规模讲述了所有与 CSS 相关的问题。所有这些都值得理解,因为其中一些非常常见,它们将帮助我们引入内联样式和局部作用域类名等概念。

以下是 CSS 的问题列表,基本上是大规模 CSS 的问题:

  • 全局命名空间
  • 依赖关系
  • 死码消除
  • 缩小
  • 共享常数
  • 非确定性分辨率
  • 隔离

CSS 的第一个众所周知的问题是所有选择器都是全局的。无论我们如何组织我们的风格,使用名称空间或一个过程,例如块、元素、修饰符BEM)方法,最终,我们总是在污染全局名称空间,我们都知道这是错误的。这不仅在原则上是错误的,而且在大型代码库中也会导致许多错误,从长远来看,这使得可维护性非常困难。与大型团队合作时,知道某个特定的类或元素是否已经过样式化是非常重要的,而且大多数情况下,我们倾向于添加更多的类,而不是重用现有的类。

CSS 的第二个问题是依赖项的定义。事实上,很难清楚地说明特定组件依赖于特定 CSS,并且必须加载 CSS 才能应用样式。由于样式是全局的,所以任何文件中的任何样式都可以应用于任何元素,并且很容易失去控制。

第三,前端开发人员倾向于使用预处理器将 CSS 拆分为子模块,但最终会为浏览器生成一个大的全局 CSS 包。由于 CSS 代码库往往很快变得庞大,我们失去了对它们的控制,第三个问题与死代码消除有关。快速识别哪些样式属于哪个组件并不容易,这使得删除代码非常困难。事实上,由于 CSS 的级联特性,删除选择器或规则可能会在浏览器中导致意外结果。

使用 CSS 的另一个痛苦是在 CSS 和 JavaScript 应用程序中缩小选择器和类名。这似乎是一项简单的任务,但事实并非如此,尤其是在动态应用类或在客户端连接类时;这是第四个问题。

不能缩小和优化类名对性能来说是非常糟糕的,它会对 CSS 的大小产生巨大的影响。对于常规 CSS 来说,另一个非常常见的操作是在样式和客户端应用程序之间共享常量。例如,我们经常需要知道收割台的高度,以便重新计算依赖于它的其他元素的位置。

通常,我们使用 JavaScript API 读取客户端中的值,但最佳解决方案是共享常量并避免在运行时进行昂贵的计算。这是 vjeux 和 Facebook 的其他开发者试图解决的第五个问题。

第六个问题涉及 CSS 的非确定性解决方案。事实上,在 CSS 中,顺序很重要,如果 CSS 是按需加载的,则无法保证顺序,这会导致错误的样式应用于元素。

例如,假设我们希望优化请求 CSS 的方式,仅当用户导航到某个特定页面时才加载与该页面相关的 CSS。如果与最后一个页面相关的 CSS 有一些规则也适用于不同页面的元素,那么最后加载的 CSS 可能会影响应用程序其余部分的样式。例如,如果用户返回到上一个页面,他们可能会看到一个页面的 UI 与第一次访问时略有不同。

控制所有样式、规则和导航路径的各种组合是非常困难的,但同样,能够在需要时加载 CSS 可能会对 web 应用程序的性能产生关键影响。

最后但并非最不重要的是,克里斯托弗·切多认为 CSS 的第七个问题与隔离有关。在 CSS 中,几乎不可能在文件或组件之间实现适当的隔离。选择器是全局的,很容易被覆盖。仅仅通过知道应用于元素的类名来预测元素的最终样式是很困难的,因为样式不是孤立的,应用程序其他部分中的其他规则可能会影响不相关的元素。这可以通过使用内联样式来解决。

在下一节中,我们将了解将内联样式与 React 一起使用意味着什么,以及它的优点和缺点。

理解和实现内联样式

官方 React 文档建议开发人员使用内联样式来设置 React 组件的样式。这似乎很奇怪,因为我们在过去的几年里都认识到分离关注点很重要,我们不应该混合使用标记和 CSS。

React 试图改变关注点分离的概念,将其从技术分离转移到组件分离。当标记、样式和逻辑紧密耦合时,将它们分离到不同的文件中,并且其中一个文件不能没有另一个文件,这只是一种错觉。即使它有助于保持项目结构的整洁,也不会带来任何真正的好处。

在 React 中,我们组合组件以创建应用程序,其中组件是我们结构的基本单元。我们应该能够在整个应用程序中移动组件,并且无论在何处呈现,它们都应该在逻辑和 UI 方面提供相同的结果。

这就是为什么在我们的组件中并置样式并在元素上使用内联样式应用它们在 React 中是有意义的原因之一。

首先,让我们看一个例子,说明使用节点的 style 属性将样式应用于 React 中的组件意味着什么。我们将创建一个带有文本Click me!的按钮,并对其应用颜色和背景色:

const style = { 
  color: 'palevioletred', 
  backgroundColor: 'papayawhip'
};

const Button = () => <button style={style}>Click me!</button>;

如您所见,在 React 中使用内联样式为元素设置样式非常容易。我们只需要创建一个对象,其中属性是 CSS 规则,值是我们在常规 CSS 文件中使用的值。

唯一的区别是,连字符的 CSS 规则必须是 camelCased 才能兼容 JavaScript 的,并且值是字符串,因此它们必须用引号包装。

供应商前缀有一些例外情况。例如,如果我们想在webkit上定义一个转换,我们应该使用WebkitTransition属性,webkit前缀以大写字母开头。此规则适用于所有供应商前缀,但小写的ms除外。

其他用例是数字——它们可以不用引号或测量单位来书写,默认情况下,它们被视为像素。

以下规则应用100像素的高度:

const style = { 
  height: 100
}

通过使用内联样式,我们还可以做一些常规 CSS 难以实现的事情。例如,我们可以在运行时重新计算客户端上的一些 CSS 值,这是一个非常强大的概念,您将在下面的示例中看到。

假设您要创建一个表单字段,其中字体大小根据其值而变化。因此,如果字段的值为24,则字体大小将为 24 像素。对于普通的 CSS,如果不付出巨大的努力和重复的代码,这种行为几乎不可能重现。

让我们看看使用内联样式有多容易,首先创建一个FontSize组件,然后声明一个值状态:

import { useState, ChangeEvent } from 'react'

const FontSize = () => {
  const [value, setValue] = useState<number>(16)
}

export default FontSize

我们实现了一个简单的更改处理程序,其中我们使用事件的 target 属性来检索字段的当前值:

const handleChange = (e: ChangeEvent<HTMLInputElement>) => { 
  setValue(Number(e.target.value))
}

最后,我们呈现number类型的输入文件,这是一个受控组件,因为我们使用状态来更新其值。它还有一个事件处理程序,每次字段值更改时都会触发该处理程序。

最后但并非最不重要的一点是,我们使用字段的 style 属性来设置其font-size值。如您所见,我们使用 camelCased 版本的 CSS 规则来遵循 React 约定:

return ( 
  <input 
    type="number" 
    value={value} 
    onChange={handleChange} 
    style={{ fontSize: value }} 
  /> 
)

呈现前面的组件时,我们可以看到一个输入字段,它根据其值更改其字体大小。它的工作方式是,当值更改时,我们将字段的新值存储在状态中。修改状态会强制组件重新渲染,我们使用新的状态值来设置字段的显示值及其字体大小;它很简单,功能强大。

计算机科学中的每一种解决方案都有其缺点,并且总是代表着一种权衡。不幸的是,对于内联样式,问题很多。

例如,对于内联样式,不可能使用伪选择器(例如,:hover)和伪元素,如果要创建具有交互和动画的 UI,这是一个非常重要的限制。

有一些变通方法,例如,您可以始终创建真实的元素而不是伪元素,但是对于伪类,有必要使用 JavaScript 来模拟 CSS 行为,这不是最佳的。

同样的情况也适用于媒体查询,无法使用内联样式进行定义,这使得创建响应性 web 应用程序变得更加困难。由于样式是使用 JavaScript 对象声明的,因此也不可能使用样式回退:

display: -webkit-flex; 
display: flex;

JavaScript 对象不能有两个同名的属性。应该避免样式回退,但是如果需要,能够使用样式回退总是好的。

CSS 的另一个无法使用内联样式进行模拟的特性是动画。这里的解决方法是全局定义动画,并在元素的“样式”属性中使用它们。对于内联样式,每当我们需要用常规 CSS 覆盖样式时,我们总是被迫使用!important关键字,这是一种不好的做法,因为它会阻止任何其他样式应用于元素。

使用内联样式最困难的事情是调试。我们倾向于使用类名在浏览器 DevTools 中查找元素,以调试和检查应用了哪些样式。对于内联样式,项目的所有样式都列在它们的style属性中,这使得检查和调试结果非常困难。

例如,我们在本节前面创建的按钮以以下方式呈现:

<button style="color:palevioletred;background-color:papayawhip;">Click me!</button>

就其本身而言,阅读起来似乎并不困难,但如果你想象你拥有数百种元素和数百种样式,你就会意识到问题变得非常复杂。

此外,如果您正在调试一个列表,其中每个项目都具有相同的style属性,并且如果您动态修改其中一项以在浏览器中检查结果,您将看到您仅将样式应用于该列表,而不将样式应用于所有其他同级,即使它们共享相同的样式。

最后但并非最不重要的一点是,如果我们在服务器端呈现我们的应用程序(我们将在第 9 章服务器端呈现以获取乐趣和利润中介绍此主题),那么当使用内联样式时,页面的大小会更大。

使用内联样式,我们将 CSS 的所有内容放入标记中,这会向发送给客户端的文件中添加额外的字节数,并使 web 应用程序看起来更慢。压缩算法可以帮助实现这一点,因为它们可以轻松压缩相似的模式,在某些情况下,加载关键路径 CSS 是一个很好的解决方案;但总的来说,我们应该尽量避免。

事实证明,内联样式带来的问题比它们试图解决的问题更多。出于这个原因,社区创建了不同的工具来解决内联样式的问题,但将样式保留在组件内部或组件的本地,以充分利用这两个方面。

在 Christopher Chedeau 的演讲之后,许多开发人员开始讨论内联样式,并进行了许多解决方案和实验,以找到用 JavaScript 编写 CSS 的新方法。起初有两三种解决方案,而今天有 40 多种。

在以下部分中,我们将介绍最流行的解决方案。

探索镭库

为解决我们在上一节中遇到的内联样式问题而创建的第一个库之一是Radium。它由强大实验室的优秀开发人员维护,并且仍然是最流行的解决方案之一。

在本节中,我们将了解 Radium 是如何工作的,它解决了哪些问题,以及为什么它是一个伟大的库,可以与 React 一起用于设置组件样式。我们将创建一个非常简单的按钮,类似于我们在本章前面的示例中构建的按钮。

我们将从一个没有样式的基本按钮开始,我们将添加一些基本样式,以及伪类和媒体查询,以便了解库的主要功能。

我们将开始使用的按钮创建如下:

const Button = () => <button>Click me!</button>

首先,我们必须使用npm安装镭:

npm install --save radium @types/radium

安装完成后,我们可以导入库并在其中包装按钮:

import Radium from 'radium'

const Button = () => <button>Click me!</button>

export default Radium(Button)

Radium函数是一个高阶组件HOC)(参见第 4 章探索所有组合模式),它扩展了Button的功能,返回一个新的增强组件。如果我们在浏览器中渲染按钮,此时将看不到任何特别的内容,因为我们没有对其应用任何样式。

让我们从一个简单的样式对象开始,在这里我们设置背景颜色、填充、大小和一些其他 CSS 属性。正如我们在上一节中所看到的,React 中的内联样式是使用带有 camelCased CSS 属性的 JavaScript 对象定义的:

const styles = { 
  backgroundColor: '#ff0000', 
  width: 320, 
  padding: 20, 
  borderRadius: 5, 
  border: 'none', 
  outline: 'none'
}

前面的代码段与带有 React 的纯内联样式没有什么不同,如果我们按如下方式将其传递给按钮,我们可以在浏览器中看到应用于按钮的所有样式:

const Button = () => <button style={styles}>Click me!</button>

结果是以下标记:

<button data-radium="true" style="background-color: rgb(255, 0, 0); width: 320px; padding: 20px; border-radius: 5px; border: none; outline: none;">Click me!</button>

在这里您可以看到的唯一区别是,有一个data-radium属性设置为true附加到元素。

现在,我们已经看到内联样式不允许我们定义任何伪类;让我们来看看如何用镭来解决这个问题。

将伪类(如:hover)与镭一起使用非常简单。我们必须在样式对象内创建一个:hover属性,剩下的由镭来完成:

const styles = { 
  backgroundColor: '#ff0000', 
  width: 320, 
  padding: 20, 
  borderRadius: 5, 
  border: 'none', 
  outline: 'none', 
  ':hover': { 
    color: '#fff' 
  } 
}

如果将此样式对象应用于按钮并在屏幕上进行渲染,则可以看到将鼠标移到按钮上会生成一个带有白色文本的按钮,而不是默认的黑色文本。太好了!我们可以同时使用伪类和内联样式。

但是,如果您打开 DevTools 并尝试在Styles面板中强制设置:hover状态,您将看到什么也没有发生。之所以可以看到悬停效果,但无法用 CSS 模拟,是因为 Radium 使用 JavaScript 应用和删除style对象中定义的悬停状态。

如果在 DevTools 打开的情况下将鼠标悬停在元素上,可以看到style字符串发生了变化,并且颜色被动态添加到元素中:

<button data-radium="true" style="background-color: rgb(255, 0, 0); width: 320px; padding: 20px; border-radius: 5px; border: none; outline: none; color: rgb(255, 255, 255);">Click me!</button> 

Radium 的工作方式是为每一个可以触发伪类行为的事件添加一个事件处理程序并监听它们。

一旦触发其中一个事件,Radium 就会更改组件的状态,并使用正确的状态样式重新渲染。这在一开始可能看起来很奇怪,但这种方法没有真正的缺点,而且性能方面的差异是无法察觉的。

我们可以添加新的伪类,例如:active,它们也可以工作:

const styles = { 
  backgroundColor: '#ff0000', 
  width: 320, 
  padding: 20, 
  borderRadius: 5, 
  border: 'none', 
  outline: 'none', 
  ':hover': { 
    color: '#fff'
  }, 
  ':active': { 
    position: 'relative', 
    top: 2
  } 
}

Radium 支持的另一个关键功能是媒体查询。媒体查询对于创建响应性应用程序至关重要,Radium 再次使用 JavaScript 在我们的应用程序中启用 CSS 功能。

让我们看看它是如何工作的——API 非常相似;我们必须在样式对象上创建一个新属性,并嵌套媒体查询匹配时必须应用的样式:

const styles = { 
  backgroundColor: '#ff0000', 
  width: 320, 
  padding: 20, 
  borderRadius: 5, 
  border: 'none', 
  outline: 'none', 
  ':hover': { 
    color: '#fff' 
  }, 
  ':active': { 
    position: 'relative', 
    top: 2
  }, 
  '@media (max-width: 480px)': { 
    width: 160 
  } 
}

要使媒体查询正常工作,我们必须做一件事,那就是用镭提供的StyleRoot组件包装我们的应用程序。

为了使媒体查询正常工作,特别是在服务器端渲染时,Radium 将在文档对象模型DOM中的样式元素中注入与媒体查询相关的规则,所有属性设置为!important

这是为了避免在库找出匹配查询之前,在应用于文档的不同样式之间闪烁。在style元素中实现样式可以通过让浏览器执行其常规工作来防止这种情况。

因此,我们的想法是导入Radium.StyleRoot组件:

import Radium from 'radium'

然后,我们可以将整个应用程序包装在其中:

const App = () => { 
  return ( 
    <Radium.StyleRoot> 
      ... 
    </Radium.StyleRoot> 
  ) 
}

因此,如果打开 DevTools,可以看到 Radium 将以下样式注入 DOM:

<style>@media (max-width: 480px) { .rmq-1d8d7428{width: 160px !important;}}</style>

rmq-1d8d7428类也已自动应用于按钮:

<button class="rmq-1d8d7428" data-radium="true" style="background-color: rgb(255, 0, 0); width: 320px; padding: 20px; border-radius: 5px; border: none; outline: none;">Click me!</button>

如果现在调整浏览器窗口的大小,您可以看到,对于小屏幕,按钮会变小,这与预期的一样。

在下一节中,我们将学习如何使用 CSS 模块。

使用 CSS 模块

如果您觉得内联样式不适合您的项目和团队,但您仍然希望样式尽可能靠近您的组件,那么有一种解决方案适合您,称为CSS 模块。CSS 模块是 CSS 文件,默认情况下,所有类名和动画名都在本地范围内。让我们看看如何在我们的项目中使用它们;但首先,我们需要配置 Webpack。

网页第 5 页

在深入研究 CSS 模块并学习它们如何工作之前,了解它们是如何创建的以及支持它们的工具是很重要的。

第 2 章清理您的代码中,我们了解了如何编写 ES6 代码并使用 Babel 及其预设进行传输。一旦应用程序增长,您可能还希望将代码库拆分为模块。

您可以使用 Webpack 或 Browserify 将应用程序划分为小模块,在需要时可以导入这些模块,同时仍然为浏览器创建一个大的捆绑包。这些工具被称为模块绑定器,它们所做的是将应用程序的所有依赖项加载到一个可以在浏览器中执行的捆绑包中,而浏览器没有任何模块概念(目前还没有)。

在 React 世界中,Webpack 特别流行,因为它提供了许多有趣和有用的功能,其中第一个是加载程序的概念。使用 Webpack,您可以潜在地加载除 JavaScript 之外的任何依赖项(如果有加载程序的话)。例如,您可以在捆绑包中加载 JSON 文件以及图像和其他资产。

2015 年 5 月,CSS 模块的创建者之一马克·达格利什(Mark Dalgleish)发现,你也可以在一个网页包包中导入 CSS,他推动了这个概念的发展。他认为,由于 CSS 可以在本地导入到组件中,所以所有导入的类名也可以在本地确定作用域,这很好,因为这将隔离样式。

建立一个项目

在本节中,我们将了解如何设置一个非常简单的 Webpack 应用程序,使用 Babel 传输 JavaScript 和 CSS 模块,将本地范围的 CSS 加载到包中。我们还将介绍 CSS 模块的所有功能,并查看它们可以解决的问题。首先要做的是移动到空文件夹并运行以下命令:

npm init

这将创建一个带有一些默认值的package.json文件。

现在,是时候安装依赖项了,第一个是webpack,第二个是webpack-dev-server,我们将使用它在本地运行应用程序并动态创建捆绑包:

npm install --save-dev webpack webpack-dev-server webpack-cli

一旦安装了 Webpack,就可以安装 Babel 及其加载程序了。由于我们使用 Webpack 创建捆绑包,我们将使用 Babel loader 在 Webpack 本身内传输 ES6 代码:

npm install --save-dev @babel/core @babel/preset-env @babel/preset-react ts-loader

最后,我们安装style-loader和 CSS 加载器,这两个加载器是我们启用 CSS 模块所需要的:

npm install --save-dev style-loader css-loader

还有一件事要做,以使事情变得更简单,那就是安装html-webpack-plugin,这是一个插件,可以创建一个 HTML 页面来动态承载我们的 JavaScript 应用程序,只需查看网页包配置,而无需创建常规文件。此外,我们需要安装fork-ts-checker-webpack-plugin包,使 TypeScript 与 Webpack 一起工作:

npm install --save-dev html-webpack-plugin fork-ts-checker-webpack-plugin typescript

最后但并非最不重要的一点是,我们安装了reactreact-dom以在我们的简单示例中使用它们:

npm install react react-dom

现在已经安装了所有的依赖项,现在是时候配置一切使其工作了。

首先,您需要在根路径中创建一个.babelrc文件:

{
 "presets": ["@babel/preset-env", "@babel/preset-react"]
}

首先要做的是在package.json中添加一个npm脚本来运行webpack-dev-server,这将服务于开发中的应用程序:

"scripts": { 
  "dev": "webpack serve --mode development --port 3000" 
}

In Webpack 5, you need to use this way to call webpack instead of webpack-dev-server but you still need to have this package installed.

Webpack 需要一个配置文件来了解如何处理我们在应用程序中使用的不同类型的依赖项,为此,我们必须创建一个名为webpack.config.js的文件,该文件导出一个对象:

module.exports = {}

我们导出的对象表示 Webpack 用于创建捆绑包的配置对象,根据项目的大小和功能,它可以具有不同的属性。

我们希望示例非常简单,因此我们将添加三个属性。第一个是entry,它告诉 Webpack 我们的应用程序的主文件在哪里:

entry: './src/index.tsx'

第二个是module,在这里我们告诉 Webpack 如何加载外部依赖项。它有一个名为rules的属性,其中我们为每种文件类型设置了一个特定的加载程序:

module: { 
  rules: [
    {
      test: /\.(tsx|ts)$/,
      exclude: /node_modules/,
      use: {
        loader: 'ts-loader',
        options: {
          transpileOnly: true
        }
      }
    }, 
    { 
      test: /\.css/,
      use: [
        'style-loader',
        'css-loader?modules=true'
      ]
    } 
  ]
}

我们的意思是,匹配.ts.tsx正则表达式的文件是使用ts-loader加载的,因此它们被传输并加载到包中。

您可能还注意到我们在.babelrc文件中添加了预设。正如我们在第 2 章清理代码中看到的,预设是一组配置选项,用于指导巴贝尔如何处理不同类型的语法(例如,TSX)。

rules数组中的第二个条目告诉 Webpack 在导入 CSS 文件时要做什么,它使用css-loadermodules标志来激活 CSS 模块。转换的结果随后被传递到style-loader,后者将样式注入页面的标题中。

最后,我们启用 HTML 插件为我们生成页面,使用前面指定的输入路径自动添加script标记:

const HtmlWebpackPlugin = require('html-webpack-plugin')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

plugins: [
  new ForkTsCheckerWebpackPlugin(),
 new HtmlWebpackPlugin({
    title: 'Your project name',
    template: './src/index.html',
    filename: './index.html'
  })
]

完整的webpack.config.js应如以下代码块所示:

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin')

const isProduction = process.env.NODE_ENV === 'production'

module.exports = {
  devtool: !isProduction ? 'source-map' : false, // We generate source maps 
  // only for development
  entry: './src/index.tsx',
  output: { // The path where we want to output our bundles
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[hash:8].js',
    sourceMapFilename: '[name].[hash:8].map',
    chunkFilename: '[id].[hash:8].js',
    publicPath: '/'
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.json', '.css'] // Here we add the 
    // extensions we want to support
  },
  target: 'web',
  mode: isProduction ? 'production' : 'development', // production mode 
  // minifies the code
  module: { 
    rules: [
      {
        test: /\.(tsx|ts)$/,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
          options: {
            transpileOnly: true
          }
        }
      }, 
      { 
        test: /\.css/,
        use: [
          'style-loader',
          'css-loader?modules=true'
        ]
      } 
    ]
  }, 
  plugins: [
    new ForkTsCheckerWebpackPlugin(),
 new HtmlWebpackPlugin({
      title: 'Your project name',
      template: './src/index.html',
      filename: './index.html'
    })
  ],
  optimization: { // This is to split our bundles into vendor and main
    splitChunks: {
      cacheGroups: {
        default: false,
        commons: {
          test: /node_modules/,
          name: 'vendor',
          chunks: 'all'    
        }
      }
    }
  }
}

然后,要配置 TypeScript,您需要这个tsconfig.json文件:

{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "lib": ["dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": false,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es6"
  },
  "include": ["src/**/*.ts", "src/**/*.tsx"],
  "exclude": ["node_modules"]
}

要使用 TypeScript 导入css文件,您需要在src/declarations.d.ts处创建一个声明文件:

declare module '*.css' {
  const content: Record<string, string>
  export default content
}

然后,您需要在src/index.tsx处创建主文件:

import { render } from 'react-dom'

const App = () => {
  return <div>Hello World</div>
}

render(<App />, document.querySelector('#root'))

最后,您需要在src/index.html处创建初始 HTML 文件:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" 
      />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

我们完成了,如果我们在终端中运行npm run dev命令并将浏览器指向http://localhost:8080,我们应该能够看到正在提供的以下标记:

<!DOCTYPE html> 
<html> 
  <head> 
    <meta charset="UTF-8"> 
    <title>Your project name</title>
    <script defer src="/vendor.12472959.js"></script>
    <script defer src="/main.12472959.js"></script> 
  </head> 
 <body>    <div id="root"></div>
  </body> 
</html>

完美–我们的 React 应用程序正在运行!现在让我们看看如何将一些 CSS 添加到项目中。

局部作用域 CSS

现在,是时候创建我们的应用程序了,它将由一个简单的按钮组成,与我们在前面的示例中使用的按钮相同。我们将使用它来显示 CSS 模块的所有特性。

让我们更新src/index.tsx文件,这是我们在网页包配置中指定的条目:

import { render } from 'react-dom'

然后我们可以创建一个简单的按钮。像往常一样,我们将从一个非样式按钮开始,我们将逐步添加样式:

 const Button = () => <button>Click me!</button>

最后,我们可以将按钮渲染到 DOM 中:

render(<Button />, document.querySelector('#root'))

现在,假设我们想对按钮应用一些样式——背景色、大小等等。我们创建了一个名为index.css的常规 CSS 文件,并将以下类放入其中:

.button { 
  background-color: #ff0000; 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
}

现在,我们说使用 CSS 模块,我们可以将 CSS 文件导入 JavaScript;让我们看看它是如何工作的。

在我们定义按钮组件的index.js文件中,我们可以添加以下行:

import styles from './index.css'

import语句的结果是一个styles对象,其中所有属性都是index.css中定义的类。

如果我们运行console.log(styles),我们可以在 DevTools 中看到以下对象:

{ 
  button: "_2wpxM3yizfwbWee6k0UlD4" 
}

因此,我们有一个对象,其中属性是类名,值(显然)是随机字符串。稍后我们将看到它们是非随机的,但是让我们先检查一下我们可以对该对象做什么。

我们可以使用该对象设置按钮的类名属性,如下所示:

const Button = () => ( 
  <button className={styles.button}>Click me!</button> 
);

如果我们回到浏览器,我们现在可以看到我们在index.css中定义的样式已经应用于按钮。这不是魔术,因为如果我们签入 DevTools,应用于元素的类就是附加到我们在代码中导入的style对象的同一字符串:

<button class="_2wpxM3yizfwbWee6k0UlD4">Click me!</button>

如果我们查看页面的标题部分,现在可以看到相同的类名也被注入到页面中:

<style type="text/css"> 
  ._2wpxM3yizfwbWee6k0UlD4 { 
    background-color: #ff0000; 
    width: 320px; 
    padding: 20px; 
    border-radius: 5px; 
    border: none; 
    outline: none; 
  } 
</style>

这就是 CSS 和样式加载器的工作方式。

CSS 加载器允许您将 CSS 文件导入 JavaScript 模块,当模块标志被激活时,所有类名都在本地作用于它们导入的模块。如前所述,我们导入的字符串是非随机的,但它是使用文件的哈希和其他一些参数生成的,在代码库中是唯一的。

最后,style-loader获取 CSS 模块转换的结果,并将样式注入页面的标题部分。这是非常强大的,因为我们拥有 CSS 的全部功能和表达能力,再加上具有局部作用域类名和显式依赖项的优势。

正如本章开头提到的,CSS 是全局的,这使得在大型应用程序中很难维护。对于 CSS 模块,类名是本地范围的,它们不能与应用程序不同部分中的其他类名冲突,从而强制执行确定性结果。

此外,在组件中显式导入 CSS 依赖项有助于我们清楚地看到哪些组件需要哪些 CSS。它对于消除死代码也非常有用,因为当我们出于任何原因删除一个组件时,我们可以准确地知道它使用的是哪个 CSS。

CSS 模块是常规 CSS,因此我们可以使用伪类、媒体查询和动画。

例如,我们可以添加如下 CSS 规则:

.button:hover { 
  color: #fff; 
} 

.button:active { 
  position: relative; 
  top: 2px; 
} 

@media (max-width: 480px) { 
  .button { 
    width: 160px 
  } 
}

这将转换为以下代码并注入到文档中:

._2wpxM3yizfwbWee6k0UlD4:hover { 
  color: #fff; 
} 

._2wpxM3yizfwbWee6k0UlD4:active { 
  position: relative; 
  top: 2px; 
} 

@media (max-width: 480px) { 
  ._2wpxM3yizfwbWee6k0UlD4 { 
    width: 160px 
  } 
}

类名被创建,并且在使用按钮的任何地方都会被替换,这使得它像预期的那样可靠和本地。

正如您可能已经注意到的,这些类名非常好,但它们使调试变得非常困难,因为我们无法轻松区分哪些类生成了散列。在开发模式中我们可以做的是添加一个特殊的配置参数,通过它我们可以选择用于生成作用域类名的模式。

例如,我们可以按如下方式更改加载程序的值:

{
  test: /\.css/,
  use: [
    { 
      loader: 'style-loader'
    },
    {
      loader: "css-loader",
      options: {
        modules: {
          localIdentName: "[local]--[hash:base64:5]"
        }
      }
    }
  ]
}

这里,localIdentName是参数,[local][hash:base64:5]是原始类名值和五个字符哈希的占位符。其他可用的占位符有[path],表示 CSS 文件的路径,以及[name],表示源 CSS 文件的名称。

激活上一个配置选项,我们在浏览器中得到的结果如下:

<button class="button--2wpxM">Click me!</button>

这是一种可读性更高、调试更容易的方法。

在生产环境中,我们不需要这样的类名,而且我们对性能更感兴趣,所以我们可能需要更短的类名和哈希。

使用 Webpack,它非常简单,因为我们可以在应用程序生命周期的不同阶段使用多个配置文件。此外,在生产中,我们可能希望提取 CSS 文件,而不是将其从捆绑包注入浏览器,这样我们就可以拥有更轻的捆绑包,并将 CSS 缓存在内容交付网络上,以获得更好的性能。

为此,您需要安装另一个名为mini-css-extract-plugin的网页包插件,它可以编写一个实际的 CSS 文件,将 CSS 模块生成的所有作用域类放入其中。

CSS 模块有几个值得一提的特性。

第一个是global关键字。事实上,在任何类前面加上:global意味着要求 CSS 模块不要在本地作用于当前选择器。

例如,假设我们将 CSS 更改如下:

:global .button { 
  ... 
}

输出结果如下:

.button { 
  ... 
}

如果您想要应用不能在本地确定范围的样式,例如第三方小部件,这是很好的。

我最喜欢的 CSS 模块特性是组合。通过组合,我们可以从相同的文件或外部依赖项中提取类,并获得应用于元素的所有样式。

例如,从按钮的规则中提取将背景设置为红色的规则到单独的块中,如下所示:

.background-red { 
  background-color: #ff0000; 
}

然后,我们可以按照以下方式将其组合到按钮中:

.button { 
  composes: background-red; 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
}

结果是按钮的所有规则和composes声明的所有规则都应用于元素。

这是一个非常强大的功能,它以一种迷人的方式工作。您可能期望所有组合的类都在类中复制,在这些类中它们被引用为 SASS@extend所做的,但事实并非如此。简单地说,所有组合的类名都会一个接一个地应用到 DOM 中的组件上。

在我们的具体案例中,我们将有以下内容:

<button class="_2wpxM3yizfwbWee6k0UlD4 Sf8w9cFdQXdRV_i9dgcOq">Click me!</button>

这里,注入页面的 CSS 如下所示:

.Sf8w9cFdQXdRV_i9dgcOq { 
  background-color: #ff0000; 
} 

._2wpxM3yizfwbWee6k0UlD4 { 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
}

如您所见,我们的 CSS 类名具有唯一的名称,这有助于隔离我们的样式。现在,让我们来看看原子 CSS 模块。

原子 CSS 模块

应该很清楚组合是如何工作的,以及为什么它是 CSS 模块的一个非常强大的功能。我开始写这本书时,在 YPlan 工作的公司,我们试图把它推进一步,将composes的力量与原子 CSS(也称为功能 CSS的灵活性结合起来。

原子 CSS 是一种使用 CSS 的方法,其中每个类都有一条规则。

例如,我们可以创建一个类,将margin-bottom设置为0

.mb0 { 
  margin-bottom: 0; 
}

我们可以使用另一个将font-weight设置为600

.fw6 { 
  font-weight: 600; 
} 

然后,我们可以将所有这些原子类应用于元素:

<h2 class="mb0 fw6">Hello React</h2>

这种技术是有争议的,同时特别有效。很难开始使用它,因为您的标记中有太多的类,这使得很难预测最终结果。仔细想想,它与内联样式非常相似,因为除了使用较短的类名作为代理之外,每个规则应用一个类。

反对原子 CSS 的最大理由通常是将样式逻辑从 CSS 移动到标记,这是错误的。类是在 CSS 文件中定义的,但它们是在视图中组成的,每次您必须修改元素的样式时,最终都会编辑标记。

另一方面,我们尝试过使用原子 CSS 一点,我们发现它使原型制作非常快。

事实上,在生成所有基本规则后,将这些类应用于元素并创建新样式是一个非常快速的过程,这是很好的。第二,使用原子 CSS,我们可以控制 CSS 文件的大小,因为只要我们创建具有样式的新组件,我们就使用现有的类,而不需要创建新的类,这对性能非常有利。

因此,我们尝试使用 CSS 模块解决原子 CSS 的问题,并将该技术称为原子 CSS 模块

本质上,您开始创建基本 CSS 类(例如,mb0),然后,您不用在标记中逐个应用类名,而是使用 CSS 模块将它们组合成占位符类。

让我们看一个例子:

.title { 
  composes: mb0 fw6; 
}

下面是另一个例子:

<h2 className={styles.title}>Hello React</h2>

这很好,因为您仍然将样式逻辑保留在 CSS 中,CSS 模块的composes通过应用标记中的所有单个类为您完成了这项工作。

上述代码的结果如下:

<h2 class="title--3JCJR mb0--21SyP fw6--1JRhZ">Hello React</h2>

这里,titlemb0fw6都自动应用于元素。它们的作用域也是局部的,因此我们拥有 CSS 模块的所有优点。

反应 CSS 模块

最后但并非最不重要的一点是,有一个很棒的库可以帮助我们使用 CSS 模块。您可能已经注意到我们是如何使用style对象来加载 CSS 的所有类的,并且因为 JavaScript 不支持连字符属性,所以我们被迫使用 camelCased 类名。

此外,如果我们引用的是 CSS 文件中不存在的类名,则无法知道它,并且undefined被添加到类列表中。对于这些和其他有用的特性,我们可能希望尝试一个可以使 CSS 模块工作更加顺畅的包。

让我们看看这意味着什么,回到我们在本节前面使用的纯 CSS 模块的index.tsx文件,并将其改为使用 React CSS 模块。

该软件包名为react-css-modules,我们首先要做的就是安装它:

npm install react-css-modules

安装包后,我们将其导入我们的index.tsx文件中:

import cssModules from 'react-css-modules'

我们将其用作一个 HOC,将要增强的Button组件和从 CSS 导入的styles对象传递给它:

const EnhancedButton = cssModules(Button, styles)

现在,我们必须更改按钮的实现,以避免使用styles对象。对于 React CSS 模块,我们使用styleName属性,该属性被转换为常规类。

最棒的是,我们可以将类名用作字符串(例如,"button"):

const Button = () => <button styleName="button">Click me!</button>;

如果我们现在将EnhancedButton渲染到 DOM 中,我们将看到与以前相比没有任何变化,这意味着该库可以正常工作。

假设我们尝试将styleName属性更改为引用不存在的类名,如下所示:

import { render } from 'react-dom'
import styles from './index.css'
import cssModules from 'react-css-modules'

const Button = () => <button styleName="button1">Click me!</button>

const EnhancedButton = cssModules(Button, styles)

render(<EnhancedButton />, document.querySelector('#root'))

这样,我们将在浏览器的控制台中看到以下错误:

Uncaught Error: "button1" CSS module is undefined.

当代码库不断增长,并且有多个开发人员在处理不同的组件和样式时,这一点尤其有用。

实现样式化组件

有一个非常有前途的库,因为它考虑了其他库在设置组件样式时遇到的所有问题。在 JavaScript 中编写 CSS 采用了不同的方法,并且已经尝试了许多解决方案,因此现在时机已经成熟,可以创建一个库,该库需要学习所有知识,然后在此基础上构建一些东西。

该库由 JavaScript 社区中两位流行的开发人员构思和维护:Glenn MaddernMax Stoiberg。它代表了解决该问题的一种非常现代的方法,它使用 ES2015 的边缘功能和一些已应用于 React 的高级技术,为样式设计提供了完整的解决方案。

让我们看看如何创建我们在前面章节中看到的相同按钮,并检查我们感兴趣的所有 CSS 功能(例如,伪类和媒体查询)是否与styled-components一起工作。

首先,我们必须通过运行以下命令来安装库:

npm install styled-components

安装库后,我们必须将其导入组件文件中:

import styled from 'styled-components'

此时,我们可以使用styled函数通过styled.elementName创建任何元素,其中elementName可以是div、按钮或任何其他有效的 DOM 元素。

第二件事是定义我们正在创建的元素的样式,为此,我们使用了一个名为tagged template literals的 ES6 特性,这是一种将模板字符串传递给函数的方法,而无需事先对其进行插值。

这意味着函数接收包含所有 JavaScript 表达式的实际模板,这使得库能够使用 JavaScript 的全部功能将样式应用于元素。

让我们首先创建一个具有基本样式的简单按钮:

const Button = styled.button`
  backgroundColor: #ff0000; 
  width: 320px; 
  padding: 20px; 
  borderRadius: 5px; 
  border: none; 
  outline: none; 
`;

这种奇怪的语法返回一个名为Button的适当的 React 组件,它呈现一个按钮元素,并将模板中定义的所有样式应用于该元素。应用样式的方法是创建一个唯一的类名,将其添加到元素中,然后在文档的头部注入相应的样式。

以下是渲染的组件:

<button class="kYvFOg">Click me!</button>

添加到页面的样式如下所示:

.kYvFOg { 
  background-color: #ff0000; 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
}

styled-components的优点在于它几乎支持 CSS 的所有特性,这使得它成为一个很好的候选应用程序。

例如,它支持使用类似 SASS 的语法的伪类:

const Button = styled.button` 
  background-color: #ff0000; 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
  &:hover { 
    color: #fff; 
  } 
  &:active { 
    position: relative; 
    top: 2px; 
  }
`

它还支持媒体查询:

const Button = styled.button` 
  background-color: #ff0000; 
  width: 320px; 
  padding: 20px; 
  border-radius: 5px; 
  border: none; 
  outline: none; 
  &:hover { 
    color: #fff; 
  } 
  &:active { 
    position: relative; 
    top: 2px; 
  } 
  @media (max-width: 480px) { 
    width: 160px; 
  } 
`;

此库还可以为您的项目带来许多其他功能。

例如,创建按钮后,可以轻松替代其样式,并多次使用不同的属性。在模板内部,还可以使用组件收到的道具并相应地更改样式。

另一大特色是主题化。将组件包装在ThemeProvider组件中,可以将主题属性向下注入到三个组件的子组件中,这使得创建 UI 非常容易,其中部分样式在组件之间共享,而一些其他属性取决于当前选择的主题。

毫无疑问,styled-components当你将你的风格提升到下一个层次时,库是游戏规则的改变者,一开始可能看起来很奇怪,因为这种方式是用组件实现风格,但一旦你习惯了,我保证这将是你最喜欢的风格包。

总结

在这一章中,我们看了很多有趣的话题。我们从大规模的 CSS 问题开始,特别是他们在 Facebook 处理 CSS 时遇到的问题。我们了解了内联样式如何在 React 中工作,以及为什么在组件中共同定位样式是好的。我们还研究了内联样式的局限性。然后,我们转到 Radium,它解决了内联样式的主要问题,为我们提供了一个用 JavaScript 编写 CSS 的清晰界面。对于那些认为内联样式是一个糟糕的解决方案的人,我们进入了 CSS 模块的世界,从头开始建立了一个简单的项目。

将 CSS 文件导入到我们的组件中可以使依赖关系变得清晰,并且在本地确定类名的范围可以避免冲突。我们了解了 CSS 模块的composes是如何成为一个伟大的功能,以及我们如何将其与原子 CSS 结合使用来创建一个快速原型框架。

最后,我们快速查看了styled-components,这是一个非常有前途的库,旨在完全改变我们处理组件样式的方式。

到目前为止,您已经了解了许多使用 CSS 样式的方法,包括从内联样式到 CSS 模块或使用库(如styled-components)。在下一章中,我们将学习如何实现服务器端渲染并从中获得好处。*