弥合 CSS 和 JavaScript 之间的差距:CSS-in-JS

Avatar of Matija Marohnić
Matija Marohnić

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

在本文中,我们将深入探讨 CSS-in-JS 的概念。如果您已经熟悉此概念,您可能仍然会喜欢浏览这种方法的哲学,您可能对下一篇文章更感兴趣。

Web 开发是一个高度跨学科的领域。我们习惯于与多种语言密切合作。而且,随着 Web 应用程序开发变得越来越普遍和细致入微,我们经常寻找创造性的方法来弥合这些语言之间的差距,以使我们的开发环境和工作流程更轻松、更高效。

最常见的示例通常是在使用模板语言时。例如,一种语言可以用来生成更冗长的语言(通常是 HTML)的代码。这是前端框架的关键方面之一 - 操纵 HTML 应该是什么样子?这方面的最新变化是 JSX,因为它实际上不是模板语言;它是 JavaScript 的语法扩展,它使使用 HTML 变得非常简洁。

Web 应用程序会经历许多状态组合,仅管理内容通常具有挑战性。这就是为什么 CSS 有时会被忽视 - 尽管通过不同的状态和媒体查询管理样式同样重要且具有挑战性。在本系列的两个部分中,我想重点关注 CSS 并探索它与 JavaScript 之间的桥梁。在本系列中,我将假设您正在使用像 webpack 这样的模块打包器。因此,我将在示例中使用 React,但相同或类似的原理也适用于其他 JavaScript 框架,包括 Vue。

CSS 领域正在向许多方向发展,因为有很多挑战需要解决,而且没有“正确”的路径。我一直花了很多精力尝试各种方法,主要是在个人项目中,所以本系列的意图只是为了**通知**,而不是规定。

CSS 的挑战

在深入代码之前,有必要解释为 Web 应用程序设置样式的最显著挑战。我将在本系列中讨论的挑战包括作用域、条件和动态样式以及可重用性。

作用域

作用域是 众所周知 的 CSS 挑战,它指的是编写不会泄漏到组件之外的样式的理念,从而避免意外的副作用。我们希望在理想情况下实现它,同时又不影响创作体验。

条件和动态样式

虽然前端应用程序中的状态开始变得越来越复杂,但 CSS 仍然是静态的。我们只能有条件地应用样式集 - 如果一个按钮是主按钮,我们可能会应用“primary”类,并在单独的 CSS 文件中定义它的样式,以应用它在屏幕上的外观。拥有几个预定义的按钮变体是可以管理的,但如果我们想要有各种各样的按钮,比如为 Twitter、Facebook、Pinterest 等定制的按钮呢?我们真正想做的是简单地传递一种颜色并使用 CSS 定义状态,比如悬停、聚焦、禁用等。这称为动态样式,因为我们不再在预定义样式之间切换 - 我们不知道接下来会发生什么。内联样式可能会出现在脑海中以解决这个问题,但它们不支持伪类、属性选择器、媒体查询或类似的东西。

可重用性

重用规则集、媒体查询等一直是我最近很少见到的一个话题,因为 Sass 和 Less 等预处理器已经解决了这个问题。但我仍然想在本系列中重新讨论它。

在本系列的两个部分中,我将列出一些处理这些挑战的技术及其局限性。没有一种技术优于其他技术,它们甚至不是互斥的;您可以根据决定哪些内容可以提高项目质量来选择一种或多种技术。

设置

我们将使用名为Photo的示例组件演示不同的样式技术。我们将渲染一个自适应图像,该图像可能具有圆角,同时显示替代文本作为标题。它将像这样使用

<Photo publicId="balloons" alt="Hot air balloons!" rounded />

在构建实际组件之前,我们将抽象掉srcSet属性以使示例代码简洁。因此,让我们创建一个utils.js文件,使用Cloudinary生成不同宽度的图像的两个实用程序

import { Cloudinary } from 'cloudinary-core'

const cl = Cloudinary.new({ cloud_name: 'demo', secure: true })

export const getSrc = ({ publicId, width }) =>
  cl.url(publicId, { crop: 'scale', width })

export const getSrcSet = ({ publicId, widths }) => widths
  .map(width => `${getSrc({ publicId, width })} ${width}w`)
  .join(', ')

我们设置了 Cloudinary 实例以使用 Cloudinary 演示云的名称,以及它的url方法以根据指定的选项为图像publicId生成 URL。我们只对在本组件中修改宽度感兴趣。

我们将分别使用这些实用程序来处理srcsrcset属性

getSrc({ publicId: 'balloons', width: 200 })
// => 'https://res.cloudinary.com/demo/image/upload/c_scale,w_200/balloons'

getSrcSet({ publicId: 'balloons', widths: [200, 400] })
// => 'https://res.cloudinary.com/demo/image/upload/c_scale,w_200/balloons 200w,
      https://res.cloudinary.com/demo/image/upload/c_scale,w_400/balloons 400w'

如果您不熟悉srcsetsizes属性,我建议先阅读一些关于自适应图像的信息。这样,您在阅读示例时会更容易理解。

CSS-in-JS

CSS-in-JS 是一种样式方法,它将 CSS 模型抽象到组件级别,而不是文档级别。这个想法是,CSS 可以作用于特定的组件 - 而且只有那个组件 - 在这种程度上,这些特定的样式不会与其他组件共享或泄漏到其他组件,并且只有在需要时才会被调用。CSS-in-JS 库通过在<head>中插入<style>标签来在运行时创建样式。

最早使用此概念的库之一是JSS。下面是一个使用其语法的示例

import React from 'react'
import injectSheet from 'react-jss'
import { getSrc, getSrcSet } from './utils'

const styles = {
  photo: {
    width: 200,
    '@media (min-width: 30rem)': {
      width: 400,
    },
    borderRadius: props => (props.rounded ? '1rem' : 0),
  },
}

const Photo = ({ classes, publicId, alt }) => (
  <figure>
    <img
      className={classes.photo}
      src={getSrc({ publicId, width: 200 })}
      srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}
      sizes="(min-width: 30rem) 400px, 200px"
    />
    <figcaption>{alt}</figcaption>
  </figure>
)
Photo.defaultProps = {
  rounded: false,
}

export default injectSheet(styles)(Photo)

乍一看,styles对象看起来像是用对象表示法编写的 CSS,具有额外的功能,比如传递一个函数来根据道具设置值。生成的类是唯一的,因此您永远不必担心它们与其他样式冲突。换句话说,您可以免费获得作用域!大多数 CSS-in-JS 库的工作方式就是这样 - 当然,在功能和语法上有一些变化,我们将在后面的内容中介绍。

您可以通过属性看到,渲染图像的宽度从200px开始,然后当视口宽度至少为30rem时,宽度增加到400px。我们生成了一个额外的800源来覆盖更大的屏幕密度

  • 1x 屏幕将使用200400
  • 2x 屏幕将使用400800

styled-components是另一个 CSS-in-JS 库,但它使用更熟悉的语法,巧妙地利用了标记模板文字而不是对象,使其看起来更像 CSS

import React from 'react'
import styled, { css } from 'styled-components'
import { getSrc, getSrcSet } from './utils'

const mediaQuery = '(min-width: 30rem)'

const roundedStyle = css`
  border-radius: 1rem;
`

const Image = styled.img`
  width: 200px;
  @media ${mediaQuery} {
    width: 400px;
  }
  ${props => props.rounded && roundedStyle};
`
  
const Photo = ({ publicId, alt, rounded }) => (
  <figure>
    <Image
      src={getSrc({ publicId, width: 200 })}
      srcSet={getSrcSet({ publicId, widths: [200, 400, 800] })}
      sizes={`${mediaQuery} 400px, 200px`}
      rounded={rounded}
    />
    <figcaption>{alt}</figcaption>
  </figure>
)
Photo.defaultProps = {
  rounded: false,
}

export default Photo

我们经常创建语义上中立的元素,比如<div><span>,仅仅是为了样式化目的。这个库以及许多其他库允许我们以单一操作的方式创建和设置它们的样式。

我最喜欢的这种语法的优点是它就像普通的 CSS 一样,只是没有插值。这意味着我们可以更轻松地迁移我们的 CSS 代码,并且可以使用我们现有的肌肉记忆,而不是不得不熟悉用对象语法编写 CSS。

请注意,我们可以将几乎任何东西都插值到我们的样式中。这个具体的示例演示了如何将媒体查询保存到变量中并在多个地方重用它。自适应图像非常适合这种情况,因为sizes属性基本上包含 CSS,因此我们可以使用 JavaScript 使代码更加DRY。

假设我们决定要视觉上隐藏标题,但仍然让屏幕阅读器可以访问它。我知道一个更好的方法是使用alt属性,但为了这个示例,让我们使用另一种方法。我们可以使用一个称为polished的样式混合库 - 它与 CSS-in-JS 库配合得很好,非常适合我们的示例。这个库包含一个称为hideVisually的混合,它正好满足我们的需求,我们可以通过插值它的返回值来使用它

import { hideVisually } from 'polished'

const Caption = styled.figcaption`
  ${hideVisually()};
`

<Caption>{alt}</Caption>

即使 hideVisually 输出一个对象,styled-components 库也懂得如何将它作为样式进行插值。

CSS-in-JS 库拥有许多高级功能,例如主题、供应商前缀,甚至 内联关键 CSS,这使得完全停止编写 CSS 文件变得容易。在这一点上,你开始明白为什么 CSS-in-JS 变得如此吸引人。

缺点和局限性

CSS-in-JS 的明显缺点是它引入了运行时:样式需要通过 JavaScript 加载、解析和执行。CSS-in-JS 库的作者添加了各种智能优化,例如 Babel 插件,但仍然会存在一些运行时成本。

同样重要的是要注意,这些库不会被 PostCSS 解析,因为 PostCSS 不是为了被引入运行时而设计的。因此,许多人使用 stylis 作为替代,因为它速度更快。这意味着我们不幸地不能使用 PostCSS 插件。

我将提到的最后一个缺点是工具。CSS-in-JS 正在快速发展,文本编辑器扩展、代码检查器、代码格式化器等需要赶上新功能才能保持同步。例如,人们正在使用 VS Code 扩展 styled-components 来使用类似 styled-components 的 CSS-in-JS 库,例如 emotion,即使它们并不都具有相同的特性。我甚至看到提出的功能的 API 选择受到保留语法高亮的目標的影响!

未来

有两个新的 CSS-in-JS 库,Linaria 和 astroturf,它们通过将 CSS 提取到文件中实现了零运行时。它们的 API 与 styled-components 相似,但在功能和目标上有所不同。

 Linaria 的目标是模仿 styled-components 等 CSS-in-JS 库的 API,通过内置功能(如作用域、嵌套和供应商前缀)来实现。相反, astroturf 是基于 CSS 模块构建的,插值能力有限,鼓励使用 CSS 生态系统而不是依赖 JavaScript。

如果你想尝试使用它们,我为这两个库构建了 Gatsby 插件。

使用这些库时,请牢记两点。

  1. 拥有实际的 CSS 文件意味着我们可以使用熟悉的工具(如 PostCSS)来处理它们。
  2. Linaria 在幕后使用自定义属性(也称为 CSS 变量),请务必考虑 它们在浏览器中的支持情况 ,然后再使用此库。

结论

CSS-in-JS 是用于弥合 CSS 和 JavaScript 之间的差距的一体化样式解决方案。它们易于使用,并包含有用的内置优化——但所有这些都需要付出代价。最值得注意的是,通过使用 CSS-in-JS,我们实际上是从 CSS 生态系统中退出,并依赖 JavaScript 来解决我们的问题。

零运行时解决方案通过带回 CSS 工具来缓解一些缺点,这将 CSS-in-JS 的讨论提升到了一个更有趣的层次。与 CSS-in-JS 相比,预处理工具的实际限制是什么?这将在本系列的下一部分中介绍。

文章系列

  1. CSS-in-JS(本文)
  2. CSS 模块、PostCSS 和 CSS 的未来