Talk about the CSS-in-JS I understand (1)

I am participating in the "Nuggets · Sail Project". In this article, I mainly want to share with you my understanding of CSS-in-JS.

1. Pain points of traditional CSS

CSS has been one of the cores of Web technology since the beginning, and CSS has become more and more standardized and more powerful in recent years. However, CSS (standardized so far) does not yet have some of the domain knowledge and capabilities required for modern front-end component development , so other technologies are needed to complement it.

These knowledge and abilities mainly include four aspects:

  1. The scope of component styles needs to be controlled at the component level;
  2. Component styles and components need to establish a stronger association at the source code and build levels;
  3. Component styles need to respond to component data changes;
  4. Component styles need to have the ability to reuse and expand in units of components.

Therefore, in the process of modern front-end component development, a lot of solutions have emerged in the community, mainly divided into the following categories:

1. Naming convention

In order to be able to control the scope of component styles at the component level, the simplest and most direct solution we can think of is of course to formulate class name definition specifications. At present, BEM specifications are commonly used in the industry, such as

// style.css
.btn {
  background-color: white;
}
.btn__icon {
  color: black;
}

import './stype.css'
const TButton = props => {
  return (
    <button className="btn">
      <icon className="btn__icon" name={props.incon}></icon>
    </button>
  )
}

Artificially guaranteeing the style isolation of components through naming conventions, this method will have the following problems:

  1. Very dependent on code normalization by the development team
  2. Component styles have no way to respond to changes in component data
  3. Does not provide very fine-grained reuse capabilities

Therefore, this method is not widely used in modern front-end large-scale business applications . This method is more suitable for the development of basic component libraries . The main reasons are:

  1. Using the component library developed by class, the business side can be easily covered by the component style
  2. The basic component library is generally developed by a dedicated team, and the naming convention can be unified
  3. Using the most basic class can effectively reduce the size of the component library

2、Inline styling

Both Vue and React actually provide different implementations of dynamic styles. Take React's JSX syntax as an example:

const App = props => {
  return (
    <div style={{color: "red"}}>123</div>
  )
}

Compared with the implementation of naming conventions, Inline Styling provides a very direct and simple solution to respond to state changes in components. At the same time, we can also implement style reuse or extension by extracting some styles as variables.

但是这种方式如果在央视属性过多的情况下会让代码显得非常混乱,因此 Inline Styling 往往用于元素部分属性调整的情况。

3、CSS Modules

CSS Module 是目前使用的比较多的解决方案,它不是将 CSS 改造成编程语言,而是在 CSS 的基础上通过构建工具对其进行了扩展,针对前面我们提到的 CSS 面对现代前端组件化开发能力的不足给出了自己的解决方案。

3.1、组件级的隔离:局部作用域

CSS Modules 实现局部作用域的核心是使用独一无二的 class,以下面 React 组件为例:

// App.jsx
import React from 'react';
import style from './App.css';

export default () => {
  return (
    <h1 className={style.title}>
      Hello World
    </h1>
  );
};
/*  App.css */
.title {
  color: red;
}

构建工具会将类名 style.title 编译成一个哈希字符串。

<h1 class="_3zyde4l1yATCOkgn-DBWEL">
  Hello World
</h1>

App.css 也会同时被编译。

._3zyde4l1yATCOkgn-DBWEL {
  color: red;
}

这样一来,这个类名就变成独一无二了,只对App组件有效。

大家可以看到 CSS Modules 实现的核心就在构建工具处理的这个阶段,我们以 webpack 为例,使用 webpack 的 css-loader 可以在打包项目的时候指定该样式的 scope,例如

// webpack config
module.exports = {
  module: {
    loaders: [
      { 
        test: /.css$/, 
        loader: 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 
      },
    ]
  },
  ...
}

这里的 css-loader 会将类名 style.title 转变成 title__app__hash 的格式。

3.2、更灵活的限制:全局作用域

CSS Modules 允许使用 :global(.className) 的语法,声明一个全局规则。凡是这样声明的 class,都不会被编译成哈希字符串。举个例子:

/* App.css */
.title {
  color: red;
}

:global(.title) {
  color: green;
}
// APP.jsx
import React from 'react';
import styles from './App.css';

export default () => {
  return (
    // 注意这里的类名写法是普通写法
    <h1 className="title">
      Hello World
    </h1>
  );
};

3.3、样式的复用:class 的继承

在 CSS Modules 中,一个选择器可以继承另一个选择器的规则,这称为"组合(composition)"。

/* App.css */
.className {
  background-color: blue;
}

.title {
  composes: className;
  color: red;
}
import React from 'react';
import style from './App.css';

export default () => {
  return (
    <h1 className={style.title}>
      Hello World
    </h1>
  );
};

这种情况下 App.css 文件会被编译成如下内容:

._2DHwuiHWMnKTOYG45T0x34 {
  color: red;
}

._10B-buq6_BEOTOl9urIjf8 {
  background-color: blue;
}

而 h1 元素会被编译成 <h1 class="_2DHwuiHWMnKTOYG45T0x34 _10B-buq6_BEOTOl9urIjf8">

3.4、变量的引入

CSS Modules 支持使用变量,不过需要安装 postcss-loaderpostcss-modules-values

// webpack config
const values = require('postcss-modules-values');

module.exports = {
  module: {
    loaders: [
      { 
        test: /.css$/, 
        loader: 'css-loader?modules!postcss-loader
      },
    ]
  },
  postcss: [
    values
  ]
  ...
}

接着,在 colors.css 里面定义变量。

@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;

App.css 可以引用这些变量。

3.5 总结

CSS Modules 的这种做法非常类似 Angular 与 Vue 对样式的封装方案,其核心是以 CSS 文件模块为单元,将模块内的选择器附上特殊的哈希字符串,以实现样式的局部作用域。在 React 项目中我们通过引入相关打包工具配置同样可以实现相同的效果,对于大部分应用开发场景已经可以完全支持。

二、CSS-in-JS

CSS-in-JS 顾名思义是在 JS 中直接编写 CSS 的技术,也是 React 官方推荐的编写 CSS 的方案,在 github.com/MicheleBert… 这个代码仓库中我们可以看到 CSS-in-JS 相关的 package 已经有60多个了。这里我们主要介绍 emotion ,这个框架比起其他框架更注重开发者体验(Developer Experience),功能相对完整,也比其他一些专注于用 JS、TS 语法写样式的框架更“CSS”一些。

1、基本使用

import { css } from '@emotion/react'

const color = 'white'

render(
  <div
    css={css`
      padding: 32px;
      background-color: hotpink;
      font-size: 24px;
      border-radius: 4px;
      &:hover {
        color: ${color};
      }
    `}
  >
    Hover to change color.
  </div>
)

可以看到 emotion 对于组件内样式隔离、数据变量都有着比较好的支持,而且对于伪类选择器等 CSS 属性也有着比较好的支持,除此之外,比较常用还有子元素选择器:

import { css } from '@emotion/react'

render(
  <div
    css={css`
      padding: 32px;
      background-color: hotpink;
      & > span {
       background-color: blue;
      }
    `}
  >
    Hover to change color.
  </div>
)

子选择器 > 对于 KanbanColumn 组件是必要的。如果去掉 > ,仅保留空格,上面三个子选择器就变成了后代选择器,无论在 DOM 树中的深度如何,只要是子孙元素中的 span 元素就会被应用上面的样式,这就会污染传入的 children 子组件的样式,偏离了我们样式隔离的目标。

2、样式的组合与复用

最简单直接的样式复用方式当然是声明一个值为 css 函数执行结果的常量,然后可以在不同组件中赋给 HTML 元素的 css 属性:

import { css } from '@emotion/react'

const commonStyles = css`
  padding: 32px;
  background-color: hotpink;
  & > span {
   background-color: blue;
  }
`}

render(
  <div
    css={commonStyles}
  >
    Hover to change color.
  </div>
)

在此基础上我们也可以选择更加灵活的样式组合:

import { css } from '@emotion/react'

const commonStyles = css`
  padding: 32px;
  background-color: hotpink;
  & > span {
   background-color: blue;
  }
`}

const color = 'white'

render(
  <div
    css={
      css`
        ${commonStyles}
        &:hover {
          color: ${color};
        }
      `
    }
  >
    Hover to change color.
  </div>
)

除此之外如果要组合两个或更多 css 函数返回值的变量,还可以用数组的写法,如果其中有重复的 CSS 属性(如 color: redcolor: blue),那么后面的会覆盖前面的:

<div css={[style1, style2, style3]}>...</div>

3、基本原理

为了说明 emotion 在背后做了一些什么我们先用 emotion 写一个基本组件:

import React, { useState } from 'react'
import { css } from '@emotion/react'

const KanbanBoard = ({ children }) => (
  <main css={css`
    flex: 10;
    display: flex;
    flex-direction: row;
    gap: 1rem;
    margin: 0 1rem 1rem;
 `}>{children}</main>
)

把开发者工具切换到检查器页签,可以看到标签的 class 属性值变成了一个貌似没有意义的类名 css-130tiw0-KanbanBoard,而这个 CSS 类是在 HTML 文档的里动态插入的

类名中的 130tiw0 是个哈希值,用来保证类名在不同组件间的唯一性,这自然就避免了一个组件的样式污染另一个组件。你不妨将类样式代码格式化,会得到如下片段:

.css-130tiw0-KanbanBoard {
  -webkit-flex: 10; 
  -ms-flex: 10;
  flex: 10; 
  display: -webkit-box; 
  display: -webkit-flex; 
  display: -ms-flexbox; 
  display: flex; 
  -webkit-flex-direction: row; 
  -ms-flex-direction: row; 
  flex-direction: row; 
  gap: 1rem; 
  margin: 0 1rem 1rem;
}

貌似比一开始手写的代码增加了几行?是的,增加的这几行中,-webkit-、 -ms- 这样的前缀称作Vendor Prefix 浏览器引擎前缀,浏览器厂商用这种方式来引入尚未标准化的、实验性的 CSS 属性或属性值。为了提高浏览器兼容性,emotion 框架会自动为较新的 CSS 标准加入带有前缀的副本,不认识这些前缀的浏览器会忽略这些副本,而老版本浏览器会各取所需,这样只需按最新标准编写一次 CSS,就可以自动支持新老浏览器。

这儿 emotion 实际上是做了以下三个事情:

  1. 将样式写入模板字符串,并将其作为参数传入 css 方法。
  2. 根据模板字符串生成 class 名,并填入组件的 class="xxxx" 中。
  3. 将生成的 class 名以及 class 内容放到 style 标签中,然后放到 html 文件的 head 中。

参考资料

CSS Modules 用法教程 - 阮一峰的网络日志

github.com/camsong/blo…

juejin.cn/post/707120…

おすすめ

転載: juejin.im/post/7248168880573235260