在 JavaScript 应用中包含 CSS 的多种方法

Avatar of Dominic Magnifico
Dominic Magnifico

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

欢迎来到前端开发领域一个极具争议的话题! 我相信你们中的大多数人在阅读本文时,都遇到过关于如何在 JavaScript 应用中处理 CSS 的众多激烈讨论。

我想在本文开头声明一点:**没有任何硬性规定确定在 React、Vue 或 Angular 应用中处理 CSS 的一种方法优于其他方法。** 每个项目都不同,每种方法都有其优点! 这可能看起来模棱两可,但我确实知道我们所处的开发社区充满了不断寻求新信息并希望以有意义的方式推动 Web 发展的人们。

抛开对该主题的先入为主的观念,让我们来看看 CSS 架构的迷人世界吧!

让我们数数有多少种方法

只需在 Google 上搜索“如何在 [插入框架名称] 中添加 CSS”,就会出现大量关于如何将样式应用于项目的强烈信念和观点。 为了帮助消除一些噪音,让我们从高层次上检查一些更常用的方法及其用途。

选项 1:一个普通的样式表

我们先从一种熟悉的方法开始:一个普通的样式表。 我们绝对可以在我们的应用中使用 <link> 链接到一个外部样式表,然后就完成了。

<link rel="stylesheet" href="styles.css">

我们可以编写我们习惯使用的普通 CSS,然后继续我们的生活。 这完全没有问题,但是随着应用变得越来越大,越来越复杂,维护单个样式表也变得越来越困难。 解析负责为整个应用设置样式的数千行 CSS 对于任何参与项目的开发人员来说都是一项痛苦的任务。 级联也是一件很棒的事情,但它也变得难以管理,因为您(或项目中的其他开发人员)编写的某些样式会引入应用其他部分的回归。 我们之前遇到过这些问题,并且像 Sass(以及最近的 PostCSS)这样的工具被引入来帮助我们解决这些问题。

我们可以继续沿着这条路走下去,利用 PostCSS 的强大功能编写非常模块化的 CSS 部分,这些部分通过 @import 规则连接在一起。 这需要在 webpack 配置中进行一些设置才能正确设置,但肯定是可以处理的!

无论您最终决定使用(或不使用)什么编译器,您都将通过标题中的 <link> 标签提供一个包含应用所有样式的 CSS 文件。 根据应用的复杂性,此文件有可能变得非常庞大,难以异步加载,并阻止应用的其余部分呈现。(当然,呈现阻止并不总是坏事,但出于所有意图和目的,我们将在此处进行一些概括,并在任何可能的情况下避免呈现阻止脚本和样式。)

这并不是说这种方法没有其优点。 对于小型应用,或由不太关注前端的团队构建的应用,单个样式表可能是不错的选择。 它在业务逻辑和应用样式之间提供了清晰的分离,并且因为它不是由我们的应用生成的,因此完全在我们控制之下,以确保我们编写的代码与输出的代码完全一致。 此外,单个 CSS 文件对于浏览器来说相当容易缓存,因此回头客在下次访问时不必重新下载整个文件。

但是假设我们正在寻找更强大的 CSS 架构,该架构允许我们利用工具的强大功能。 有些东西可以帮助我们管理需要更细致入微的方法的应用。 让我们引入 CSS Modules。

选项 2:CSS Modules

单个样式表中一个相当大的问题是回归的风险。 编写使用相当非特定选择器的 CSS 可能会最终更改应用完全不同区域中的另一个组件。 这就是“作用域样式”方法派上用场的地方。

作用域样式允许我们以编程方式生成特定于组件的类名。 从而将这些样式限定到该特定组件,确保它们的类名是唯一的。 这会导致自动生成的类名,例如 header__2lexd。 末尾的部分是一个唯一的哈希选择器,因此即使您有另一个名为 header 的组件,您也可以向其应用 header 类,并且我们的作用域样式会生成一个新的哈希后缀,如下所示:header__15qy_

CSS Modules 提供了多种方法(取决于实现)来控制生成的类名,但我将把这留给 CSS Modules 文档 来介绍。

**最终,我们仍然生成一个通过标题中的 <link> 标签传递给浏览器的单个 CSS 文件。** 这与我们上面介绍的相同潜在缺点(呈现阻止、文件大小膨胀等)和一些优点(主要是缓存)相关联。 但是这种方法,由于其限定样式的目的,还附带另一个注意事项:至少最初会删除全局作用域。

假设您想要使用 .screen-reader-text 全局类,该类可以应用于应用中的任何组件。 如果使用 CSS Modules,则必须使用 :global 伪选择器,该选择器明确地将其中的 CSS 定义为允许应用中其他组件全局访问的内容。 只要您将包含 :global 声明块的样式表导入到组件的样式表中,您就可以使用该全局选择器。 这不是一个很大的缺点,但需要一些时间来适应。

以下是在实际中使用 :global 伪选择器的示例

// typography.css
:global {
  .aligncenter {
    text-align: center;
  }
  .alignright {
    text-align: right;
  }
  .alignleft {
    text-align: left;
  }
}

您可能会冒着将大量用于排版、表单和大多数网站都具有的通用元素的全局选择器放到一个 :global 选择器中的风险。 幸运的是,通过 PostCSS Nested 或 Sass 等工具的魔力,您可以将部分直接导入到选择器中,使事情变得更清晰

// main.scss
:global {
  @import "typography";
  @import "forms";
}

这样,您就可以在没有 :global 选择器的情况下编写部分,然后直接将它们导入到主样式表中。

另一个需要适应的地方是如何在 DOM 节点中引用类名。 我将让 VueReactAngular 的各个文档自行说明。 我还将为您提供一个在 React 组件中使用这些类引用的示例

// ./css/Button.css

.btn {
  background-color: blanchedalmond;
  font-size: 1.4rem;
  padding: 1rem 2rem;
  text-transform: uppercase;
  transition: background-color ease 300ms, border-color ease 300ms;

  &:hover {
    background-color: #000;
    color: #fff;
  }
}

// ./Button.js

import styles from "./css/Button.css";

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

export default Button;

再次强调,CSS Modules 方法有一些很好的用例。 对于希望利用作用域样式同时保持静态但已编译样式表性能优势的应用,CSS Modules 可能是适合您的选择!

这里也值得注意的是,CSS Modules 可以与您喜欢的 CSS 预处理程序结合使用。 Sass、Less、PostCSS 等都可以集成到利用 CSS Modules 的构建过程中。

但是假设您的应用可以从包含在 JavaScript 中获益。 也许能够访问组件的各种状态,并根据不断变化的状态做出反应,也将是有益的。 假设您还想轻松地将关键 CSS 集成到您的应用中! 让我们引入 CSS-in-JS。

选项 3:CSS-in-JS

CSS-in-JS 是一个相当广泛的主题。 有几个软件包致力于使编写 CSS-in-JS 尽可能轻松。 像 JSSEmotionStyled Components 这样的框架只是构成该主题的众多软件包中的一小部分。

作为大多数这些框架的概括性解释,CSS-in-JS 的工作方式大体相同。 您编写与单个组件关联的 CSS,然后您的构建过程编译应用。 发生这种情况时,大多数 CSS-in-JS 框架将输出仅在任何给定时间呈现页面上的组件的相关 CSS。 CSS-in-JS 框架通过在应用的 <head> 中的 <style> 标签内输出该 CSS 来实现此目的。 这为您提供了一种开箱即用的关键 CSS 加载策略! 此外,与 CSS Modules 非常相似,样式是限定范围的,并且类名是哈希化的。

当你在应用程序中导航时,卸载的组件的样式将从<head>中移除,而加载的组件的样式将追加到其位置。这为应用程序提供了提升性能的机会。它减少了一个HTTP请求,不会阻塞渲染,并确保用户在任何给定时间仅下载查看页面所需的内容。

CSS-in-JS 提供的另一个有趣的机会是能够引用各种组件状态和函数以渲染不同的 CSS。这可能像基于某些状态更改复制类切换一样简单,也可能像主题化一样复杂。

由于 CSS-in-JS 是一个相当#热议的话题,我意识到人们正在尝试使用很多不同的方法来解决这个问题。现在,我与许多其他将 CSS 视为高度重视的人有同样的感受,尤其是在利用 JavaScript 来编写 CSS 时。我最初对这种方法的反应相当负面。我不喜欢将两者交叉污染的想法。但我想保持开放的心态。让我们看看我们作为专注于前端的开发者为了甚至考虑这种方法需要哪些功能。

  • 如果我们正在编写 CSS-in-JS,我们必须编写真正的 CSS。一些软件包提供了编写模板字面量 CSS 的方法,但要求你将属性转换为驼峰命名法——例如,padding-left 变成了 paddingLeft。那不是我个人愿意牺牲的。
  • 一些 CSS-in-JS 解决方案要求你在尝试设置样式的元素上内联编写样式。这种语法,尤其是在复杂的组件中,开始变得非常混乱,而且同样不是我愿意牺牲的。
  • 使用 CSS-in-JS 必须为我提供强大的工具,否则使用 CSS Modules 或普通的样式表很难实现。
  • 我们必须能够利用前瞻性的 CSS,如嵌套和变量。我们还必须能够整合诸如Autoprefixer之类的功能和其他插件来增强开发体验。

这对一个框架来说要求很高,但对于我们这些大部分时间都在学习和实施围绕我们所爱的语言的解决方案的人来说,我们必须确保能够尽可能地继续编写相同的语言。

以下快速了解一下使用 Styled Components 的 React 组件可能是什么样子

// ./Button.js
import styled from 'styled-components';

const StyledButton= styled.button`
  background-color: blanchedalmond;
  font-size: 1.4rem;
  padding: 1rem 2rem;
  text-transform: uppercase;
  transition: background-color ease 300ms, border-color ease 300ms;

  &:hover {
    background-color: #000;
    color: #fff;
  }
`;

const Button = () => (
  <StyledButton>
    Click Me!
  </StyledButton>
);

export default Button;

我们还需要解决 CSS-in-JS 解决方案的潜在缺点——当然不是为了引发更多争议。使用这种方法,我们很容易陷入一个陷阱,导致我们得到一个膨胀的 JavaScript 文件,其中可能包含数百行 CSS——所有这些都发生在开发者甚至看到任何组件方法或其 HTML 结构之前。但是,我们可以将其视为一个机会,非常仔细地检查我们构建组件的方式和原因。通过更深入地思考这一点,我们有可能将其用于我们的优势,并编写更精简的代码,以及更多可重用的组件。

此外,这种方法绝对模糊了业务逻辑和应用程序样式之间的界限。但是,通过良好的文档和经过深思熟虑的架构,项目中的其他开发者可以轻松地接受这个想法,而不会感到不知所措。

太长不看

在任何项目中,都有几种方法可以处理 CSS 架构这个难题,并且在使用任何框架时都可以这样做。事实上,我们作为开发者,拥有如此多的选择,既令人兴奋,又令人难以置信地不知所措。然而,我认为在最终进行的超短社交媒体对话中不断被遗漏的总体主题是,每种解决方案都有其自身的优点和缺点。关键在于我们如何仔细而有条理地实施一个系统,让未来的我们,或者可能接触到代码的其他开发者,感谢我们花时间建立这种结构。