前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CSS Modules VS. styled-components,哪个才是解决 CSS 不足之处的更好方案?

CSS Modules VS. styled-components,哪个才是解决 CSS 不足之处的更好方案?

作者头像
玖柒的小窝
修改2021-11-01 11:11:10
7.8K1
修改2021-11-01 11:11:10
举报
文章被收录于专栏:各类技术文章~

CSS 是一门标记语言,用于元素布局及样式定义。它存在很多问题,例如书写效率和维护性低;缺乏模块机制、变量、函数等概念;容易出现全局样式污染和样式冲突等。目前前端社区存在很多解决上述问题的方案,主要包括 CSS Module以及 styled-components💅(CSS-in-JS 的代表)。

styled-components 在我的日常开发中用得很多,并且用得非常顺手。它的 CSS-in-JS 思想以及通过 props 来动态更改样式跟 React 的开发理念一脉相承,并且还基于 React Context API 还提供了自己的的主题切换能力。

但是,这周跟我同事讨论了一下 styled-components。我同事是 styled-components 的反对者,认为用 CSS Modules 就已经很足够了,因为CSS Modules提供了局部作用域和模块功能,配合 Sass / Less 使用完全能达到跟 styled-components 同样的效果。使用 styled-components 虽然 可能 能提升了开发体验,但是它运行时(runtime)的机制却对性能有损耗,可能对用户体验造成负面影响(这里用可能是因为我并没有量化地去比较,感觉这也很难量化)。我自己也并没有很认真地去比较过两者的原理和异同,因此很好奇:这么开发者都在用 styled-components,难道它真的是提升开发体验而降低用户体验的东西吗?

因此,这篇文章就对两者进行了较为详细的介绍和比较(也是为了是通过写文章,来加深自己对两者的理解)。其实比较完之后,发现两者在各自的领域都都已经是很成熟的方案了,因此用哪个方案,就是看开发者偏好以及团队选择了。

先从 CSS 说起

CSS 是一个用于布局和定义样式的语言:它非常容易理解和上手,但要精通它却很难。CSS 的属性互不正交,大量的依赖与耦合难以记忆,规则非常庞杂。如果只使用原生的 CSS 语言去写样式的话,主要可能会遇到以下两个问题:

  • CSS 缺乏没有变量、函数这些概念,也没有模块机制,导致书写效率以及代码的维护性都不高。注意,CSS 的 @import 机制并不算真正的模块机制,因为 @import 是在一个 CSS 文件里面引入另一个 CSS 文件,并且只有执行到 @import 语句的时候才会触发浏览器下载被引入的 CSS 文件。这会对网页加载速度产生不利影响。
  • 复用性低:CSS 缺少抽象的机制,选择器很容易出现重复,不利于维护和复用。
  • 全局污染:CSS 选择器的作用域是全局的,如果两个选择器名称相同,后定义的选择器会覆盖前定义的选择器。此外,不同种类的选择器,例如ID 选择器、类选择器、元素选择器等的权重也不一样,这很容易引起样式相互覆盖或冲突。虽然可以通过差异化类命名的方式来避免全局冲突,但这又会导致类命名的复杂度上升。

为了解决 CSS 存在的这些不足之处,前端社区出现了很多种解决方案。例如,Saas 或者 LessSassLess 都属于 CSS 预处理,即在 CSS 的基础上进行了扩展,增加了一些编程的特性,并且将 CSS 作为目标生成文件。具体来说,Sass / Less 增加了规则、变量、混入、选择器、继承等特性,还引入了模块系统。因此,相比与 CSS,Sass / Less 更像是一门编程语言,可以提升写 CSS 的效率,代码更易于组织和维护。Sass / Less 文件最终都会被编译为 CSS 文件,这样才能被浏览器正常识别。然而,Sass / Less 更多地解决的是上述不足的第一点以及第二点,即通过引入编程语言特性和模块机制提升编写效率,提高可维护性。

CSS Module方案以及 styled-components 方案是社区中比较著名的解决方案,可以较好地解决 CSS 的上述问题。这两者解决问题采用的是两种不同的思路:CSS Module 是通过工程化的方法,加入了局部作用域和模块机制来解决命名冲突的问题。CSS Module 通常会配合 Sass 或者 Less 一起使用。styld-components 是一种 CSS-in-JS 的优秀实践,通过 JS 来声明、抽象样式来提高组件的可维护性,在组件加载时动态地加载样式,并且动态地生成类名避免命名冲突和全局污染。

CSS Modules

CSS 用于描述网页样式,一个典型的网页包含许多元素或组件,例如菜单、按钮、输入框等,这些元素或组件的样式是由单个或多个 CSS 规则决定的,这些规则被包含在一个 CSS 文件当中,并且可供包含该文件的整个网页访问。也就是说。所有 CSS 样式都是全局的,任何一个组件的样式规则,都对整个页面有效。如果希望某些样式仅对页面的某个组件可见,应该怎么办呢?

基本用法

CSS Modules 就是为了解决这种场景而生的,它加入了局部作用域和模块依赖,可以保证某个组件的样式不会影响到其他组件。具体而言,CSS Modules 通过工程化的方法,可以将类名编译为哈希字符串,从而使得每个类名都是独一无二的,不会与其他的选择器重名,由此可以产生局部作用域。CSS Modules 提供各种插件,支持不同的构建工具,包括 Webpack, Browserify, NodeJS 等。其中,WebpackCSS Loader 插件提供了对 CSS Modules 的支持,可以很方便地打开 CSS Modules 功能。以如下 Demo 为例:

代码语言:javascript
复制
module.exports = {
  entry: './src/index.js',

  output: {
    filename: 'index.js',
    path: path.resolve('./dist'),
    libraryTarget: 'umd'
  },

  module: {
    loaders: [
      { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
      { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader') },
      { test: /\.svg$/, loader: "url-loader?limit=10000&mimetype=image/svg+xml" }
    ]
  },

  postcss: [
    require('autoprefixer-core'),
    require('postcss-color-rebeccapurple')
  ],

  resolve: {
    modulesDirectories: ['node_modules', 'components']
  },

  plugins: [
    new ExtractTextPlugin('style.css', { allChunks: true }),
    new ReactToHtmlPlugin('index.html', 'index.js', {
      static: true,
      template: ejs.compile(fs.readFileSync(__dirname + '/src/template.ejs', 'utf-8'))
    })
  ]
};
复制代码

其中,跟 CSS Modules 相关的关键代码是

代码语言:javascript
复制
{ test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]!postcss-loader') }
复制代码

其中,开启 CSS Modules 的关键就在于 modules: true,即给所有文件都开启 CSS Modules。modules还可能有以下的值,具体可以参照 css-loader 的 modules 配置项

  • undefined(默认值):所有符合正则表达式 /.module.\w+$/i.test(filename)或者 /.icss.\w+$/i.test(filename)的文件都会开启 CSS Modules;
  • true:所有文件都开启 CSS Modules;
  • false:所有文件都关闭 CSS Modules;
  • string(local, global, pure 或者 icss): 所有文件都关闭 CSS Modules 并且设置 mode 属性的值。
    • local(默认值): CSS Modules 会默认开启局部作用域,所有全局变量都要加上 :global前缀;
    • global: CSS Modules 会默认开启全局作用域,所有局部变量都要加上 :local前缀;
    • pure: 选择器必需包含至少一个局部 class 或者 id;
    • icssicss 只会编译低级别的Interoperable CSS 格式,用于声明 CSS 和其他语言之间的 :import :export依赖项。
  • object: 一个配置对象,默认所有文件都开启 CSS Modules,具体情况根据 modules.auto的值而定。

开启 CSS Modules 之后,所有的类名都会被编译成一个哈希字符串,以下面组件 App.jsx及其样式文件 App.css为例:

代码语言:javascript
复制
// App.css
.appTitle {
    color: red;
}
复制代码
代码语言:javascript
复制
// APP.jsx
import React from 'react';
import styles from './App.css';

export default () => {
  return (
    <h1 className={styles['appTitle']}>
      Hello World
    </h1>
  );
};
复制代码

注意,根据 CSS Modules 的官方规范,更推荐以驼峰式的命名方式定义类名,而非 kebab-casing。以上述例子为例,我们把 h1 的类名命名为 appTitle 而非 app-title,这是因为app-title这种命名方式不能用 . 访问法,即:

代码语言:javascript
复制
// 驼峰式
<h1 className={styles['appTitle']}> // 🉑️
<h1 className={styles.appTitle}> // 同样🉑️

// kebab-casing
<h1 className={styles['app-title']}> // 🉑️
<h1 className={styles.app-title}> // 不🉑️,会导致错误
复制代码

css-loader 默认的哈希算法是[hash:base64],但是可以通过设置 localIdentName的属性来更改哈希算法的规则。上面例子的 webpack 配置设置了localIdentName 的属性是[name]__[local]___[hash:base64:5],那么 h1的类名会被编译为:

代码语言:javascript
复制
<h1 class="App__appTitle__GyYTO">
  Hello World
</h1>
复制代码

并且 App.css也会被编译为

代码语言:javascript
复制
.App__appTitle__GyYTO {
  color: red;
}
复制代码

这样一来,类名app-title就被编译为了独一无二App__app-title__GyYTO,并且只对 App组件有效。

全局变量

CSS Modules 允许使用 :global(.className)的语法,声明一个全局规则。凡是这样声明的 class,都不会被编译成哈希字符串。例如,我们在 App.css中加入全局类名 globalTitle 。注意,CSS Modules 还提供一种显式的局部作用域语法 :local(.className),这在 css Loader 设置 modules = local 时等同于 .className

代码语言:javascript
复制
// App.css
.appTitle {
	color: red;
}

:local(.appTitle1) {
  color: yellow;
}


:global(.globalTitle){
	color: green;
}
复制代码

App.jsx中,就可以以普通 CSS 的写法去引用全局 class 了。此时,渲染的 Hello World 是红色,而 Hello World Again 是绿色,因为它用的是全局变量。

代码语言:javascript
复制
import React from 'react';
import styles from './App.css';

export default () => {
  return (
    <h1 className={styles['appTitle']}>
      Hello World
    </h1>
    <h1 className = "globalTitle">
       Hello World Again
    </h1>
  );
};
复制代码

此时 App.CSS 被编译为

代码语言:javascript
复制
.App__appTitle__GyYTO {
  color: red;
}

.App__appTitle1__NHgyT {
  color: yellow;
}

// 全局类名不会被编译为哈希类名
.globalTitle {
    color: green;
}
复制代码

组合 (Composition)

除了局部作用域,CSS Modules 的另一个很重要的特性是组合(Composition),一个选择器可以继承另一个选择器的规则。组合可以发生在同一个 CSS 文件的不同类之间,也可以发生在不同 CSS 文件的不同类之间。后者可以理解为在 CSS 中加入了模块机制

同 CSS 文件中 class 的组合

App.css中建立一个用于定义背景颜色的 class appBackground,并且新建一个继承 appBackground 以及 appTitle 的 class appStyle

代码语言:javascript
复制
// App.css
.appTitle {
    color: red;
}

.appBackground {
    color: blue;
}

.appStyle {
    composes: appTitle appBackground; // 不同类之间用空格隔开
    padding: 8px;
}
复制代码

注意,composes 仅对局部(local-scoped)且只包含单独的类名的选择器有效,并且可以一个选择器里面可以存在多条 composes 规则,但所有的 composes 规则都必须定义在其他规则的前面。此时,App.css 会被编译为:

代码语言:javascript
复制
// 编译后的 App.css
.App__appTitle__GyYTO {
  color: red;
}

.App__appBackground__NhvyT {
  color: yellow;
}

.App__appStyle__dahOP {
  border: 1px solid black;
}
复制代码

App.jsx 如下:

代码语言:javascript
复制
import React from 'react';
import styles from './App.css';

export default () => {
  return (
    <h1 className={styles['appStyle']}>
      Hello World
    </h1>
  );
};
复制代码

那么 h1 的 class 会被编译为

代码语言:javascript
复制
<h1 className="App__appTitle__GyYTO App__appBackground__NhvyT App__appStyle__dahOP">
复制代码

不同 CSS 文件间 class 的组合

假设除了 App.css 之外,还有一个 another.css,并且App.css继承 another.css其中的规则:

代码语言:javascript
复制
/* another.css */
.ohterBackground {
  background-color: blue;
}

/* App.css */
.appStyle {
  composes: ohterBackground from './another.css';
  color: red;
}
复制代码

这样,渲染出来的 h1 会有蓝色的背景颜色以及红色的字体颜色。注意,当一个类从不同文件中组合多个类时,被组合类的规则的应用顺序是不可预测的。因此,应该要避免来自不同文件的多个类名中为同一属性定义不同的值。 例如 App.css继承 another.cssanother1.css的两条规则:

代码语言:javascript
复制
/* another.css */
.ohterBackground {
  background-color: blue;
}

/* another1.css */
.ohter1Background {
  background-color: grey;
}

/* App.css */
.appStyle {
  composes: ohterBackground from './another.css';
  composes: ohter1Background from './another1.css'; // 不要这样做,会导致最终的 background-color 不可预测
  color: red;
}
复制代码

由于 ohterBackground 以及 ohter1Background background-color定义了不同的值。由于 appStyle同时组合了 ohterBackground 以及 ohterBackground1,由于后定义的属性值会覆盖前面定义的同属性的值,这会使得应用了 appStyleh1标签实际的背景颜色会变得不可预测,可能是 blue (如果是 ohterBackground 后应用)或者 red (如果是 ohterBackground1 后应用)。此外,还注意组合不应该形成循环依赖,这会使得 Css Modules 抛错。

代码语言:javascript
复制
// 循环依赖会导致错误,不要这样做!!
/* another.css */
.ohterBackground {
  composes: appStyle from './App.css';
  background-color: blue;
}

/* App.css */
.appStyle {
  composes: ohterBackground from './another.css'; // 形成了循环依赖
  color: red;
}
复制代码

此外,局部 class 中还可以组合全局 class,例如:

代码语言:javascript
复制
/* global.css */
:global(.globalBackground){
    color: green;
}

/* App.css */
.appStyle {
  composes: globalBackground from './global.css';
  color: red;
}
复制代码

在 CSS Modules 里面使用变量

在安装 PostCSS 以及 postcss-modules-values 之后,并且把 postcss-loader 加入 webpack 配置之后,在 CSS Modules 使用变量了。例如:

代码语言:javascript
复制
/* colors.css */
@value primary: #BF4040;
@value secondary: #1F4F7F;

.text-primary {
  color: primary;
}

.text-secondary {
  color: secondary;
}

/* breakpoints.css */
@value small: (max-width: 599px);
@value medium: (min-width: 600px) and (max-width: 959px);
@value large: (min-width: 960px);


/* App.css */
/* alias paths for other values or composition */
@value colors: "./colors.css";
/* import multiple from a single file */
@value primary, secondary from colors;
/* make local aliases to imported values */
@value small as bp-small, large as bp-large from "./breakpoints.css";
/* value as selector name */
@value selectorValue: secondary-color;

.selectorValue {
  color: secondary;
}

.header {
  composes: text-primary from colors; // colors 是 "./colors.css" 的别名
  box-shadow: 0 0 10px secondary;
}

@media bp-small {
  .header {
    box-shadow: 0 0 4px secondary;
  }
}
@media bp-large {
  .header {
    box-shadow: 0 0 20px secondary;
  }
}
复制代码

除了这种方式之外,可以将 CSS ModulesSass / Less 进行组合使用,从而既能拥有 Sass / Less 的 CSS 预处理器的能力(规则、变量、混入、选择器、继承等),又可以拥有 CSS Modules 提供的局部作用域的能力,避免全局污染。

styled-componnets 💅(这个 logo 有点魔性)

介绍完了 CSS Modules,终于轮到 styled-components 💅 了。styled-components 在我的日常开发中用的很多,并且个人感觉的确非常好用,这种 CSS-in-JS 的写法能让组件的样式定义变得很明了且带有语义特性(但也可能会让组件的 tsx 文件变得很长)。styled-components 的基本思想是通过删除样式和组件之间的映射来强制执行最佳实践,同时还拆分了容器组件和展示组件,确保开发人员只能构建小型且集中的组件。

但是,我有同事是 styled-components 的反对者,因为它在运行时引入了 PostCSS,应用启动时编译样式。应充分利用编译期能力,把 CSS 在编译期确定下来,这样才能享受浏览器内核自己的优化。后来我自己查阅相关文章才发现,前端社区早就有很多关于是否应该使用styled-components 的讨论。首先让我们了解什么是 styled-components:

styled-components 以组件的形式来声明样式,让样式也成为组件

Styled Components 的官方网站将其优点归结为:

  • Automatic critical CSSstyled-components 持续跟踪页面上渲染的组件,并自动注入样式。结合使用代码拆分, 可以实现仅加载所需的最少代码。
  • 解决了 class name 冲突styled-components 为样式生成唯一的 class name,开发者不必再担心 class name 重复、覆盖以及拼写的问题。(CSS Modules 通过哈希编码局部类名实现这一点)
  • CSS 更容易移除:使用 styled-components 可以很轻松地知道代码中某个 class 在哪儿用到,因为每个样式都有其关联的组件。如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除。
  • 简单的动态样式:可以很简单直观的实现根据组件的 props 或者全局主题适配样式,无需手动管理多个 classes。(这一点很赞)
  • 无痛维护:无需搜索不同的文件来查找影响组件的样式,无论代码多庞大,维护起来都是小菜一碟。
  • 自动提供前缀:按照当前标准写 CSS,其余的交给 styled-components 处理。

因为 styled-components 做的只是在 runtime 把 CSS 附加到对应的 HTML 元素或者组件上,它完美地支持所有 CSS。 媒体查询、伪选择器,甚至嵌套都可以工作。但是要注意,styled-componentsReact 下的 CSS-in-JS 的实践,因此下面的所有例子的技术栈都是 React

安装

代码语言:javascript
复制
# install with npm
npm install --save styled-components

# install with yarn
yarn add styled-components
复制代码

基本用法

下面是一个简单的 styled-components 例子。styled.h1 调用后会返回一个 React 组件。 styled-components 会自动生成一个附加到这个 React 组件的名称哈希化后的 class(默认以 sc- 开头),并且把定义的样式与这个 class 相关联。

代码语言:javascript
复制
import React from 'react';
import styled from 'styled-components';

const ScH1 = styled.h1`
    color: red;
    background-color: blue;
    text-align: center;
    padding: 10px;
`;

export default () => {
  return (
    <ScH1>
        Hello World
    </ScH1>
  );
};

复制代码

Styled-Components 使用了标记模板文字(tagged template literals)来为组件添加样式。当你定义你的样式时,实际上是在创建一个普通的 React 组件,该组件附加了你的样式。Styled-Components 使用了 stylis 自动为 Css 规则自动加上前缀。

注意,Styled-Components 定义的组件一定要放在组件函数定义之外(对于 Class 类型的组件,不要放在 render 方法内 )。因为在 react 组件的 render 方法中声明样式化的组件,会导致每次渲染都会创建一个新组建。 这意味着 React 将不得不在每个后续渲染中丢弃并重新计算 DOM 子树的那部分,而不是仅仅计算它们之间变化的差异,从而导致性能瓶颈和不可预测的行为。

代码语言:javascript
复制
// ❌ 绝对不要这样写
const Header = () => {
  const Title = styled.h1`
    font-size: 10px;
  `

  return (
    <div>
      <Title />
    </div>
  )
}

// ✅应该要这样写
const Title = styled.h1`
  font-size: 10px;
`

const Header = () => {
  return (
    <div>
      <Title />
    </div>
  )
}
复制代码

此外,如果 styled-components 的目标是一个简单的 HTML 元素(例如 styled.div),那么 styled-components 将传递所有原生的 HTML AttributesDOM。如果是自定义 React 组件(例如 styled(MyComponent)),则 styled-components 会传递所有的 props

styled-componnets 的动态样式

styled-components 支持通过 props 实现动态样式,并且可以与 TypeScript 配合使用。并且 VsCode 还有一款插件 vscode-styled-components 能识别 styled-components ,并能自动进行 CSS 高亮、补全、纠正等。

代码语言:javascript
复制
# 与 TypeScript 配合使用
$ npm install @types/styled-components -D
复制代码

下面例子展示了一个样式化的 Button 接收 primary 属性,并根据该属性调整背景颜色 background 以及 color

代码语言:javascript
复制
import React, {
  ButtonHTMLAttributes
} from 'react';
import styled from 'styled-components';

interface IScButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
    primary?: boolean;
}

const ScWrapper = styled.div`
    margin-top: 12px;
`;

const ScButton = styled.button<IScButtonProps> `
    background: ${props => props.primary ? "blue" : "white"};
    color: ${props => props.primary ? "white" : "blue"};
    border: 2px solid palevioletred;
    border-radius: 3px;
    padding: 0.25em 1em;
`;

export default () => {
  return (
   <ScWrapper>
       <ScButton>Normal</ScButton>
       <ScButton primary>Primary</ScButton>
  </ScWrapper>
  );
};
复制代码

扩展组件样式

styled-components 不仅能对原生的 element 进行样式定义,也能对组件的样式进行扩展。可以通过 styled()创建一个继承另一个组件样式的新组件。例如,我们想要创建一个继承了上述 ScButton 的新组件 ScExtendedButton

代码语言:javascript
复制
const ScButton = styled.button`
    color: white;
    background-color: blue;
    border: 2px solid palevioletred;
    border-radius: 3px;
    padding: 0.25em 1em;
`;

// 创建一个继承 ScButton 的新组件 ScExtendedButton
const ScExtendedButton = styled(ScButton)`
    color: blue;
    background-color: white;
    margin-top: 1em;
`;
复制代码

通过这种方式,ScExtendedButton 拥有跟 ScButton相同的 border, border-radius, padding属性,但是多了 margin-top属性,并且覆盖了 ScButton 中的 color, background-color属性。此外,如果我们想要创建一个继承 ScButton的所有样式的 a元素,可以使用 as属性来制定最终渲染的内容(可以是原生的元素或者是自定义组件),例如:

代码语言:javascript
复制
// 创建一个继承 ScButton 的新组件 ScExtendedButton,最终会渲染为 a 元素
const ScExtendedButton = styled(ScButton)`
    color: blue;
    background-color: white;
`;

const ReversedTextButton = (children, ...props) => <Button {...{
    children: children.spilit('').reverse().join(''),
    ...props
  }} />

export default () => {
    return (
        <ScExtendedButton as="a" href="#">
            Extends Link with Button styles
        </ScExtendedButton>
        {/* as 属性可以是自定义组件 */}
        <ScExtendedButton as="ReversedTextButton">
            Extends Component with Button styles
        </ScExtendedButton>
     )
}
复制代码

扩展第三方组件

只要将传递的 className 属性附加到 DOM 元素,styled-components 就可以在自己创建的或是第三方组件中运行。

代码语言:javascript
复制
// 在自己创建的组件上运行
const Link = ({ className, children }) => (
  // className 属性附加到 DOM 元素
  <a className={className}>
    {children}
  </a>
);

const StyledLink = styled(Link)`
  color: red;
  font-weight: bold;
`;

render(
  <div>
    <Link>Unstyled Link</Link>
    <br />
    <StyledLink>Styled Link</StyledLink>
  </div>
);
复制代码

我们同样可以扩展第三方组件,例如阿里的企业级中后台组件库 fusion 的 Button 组件,由于它同样把 className 属性附加到渲染的 Dom 元素,因此可以利用 styled()扩展

代码语言:javascript
复制
import {
    Button
} from '@alifd/next';

const ScButton = styled(Button)`
    margin-top: 12px;
    color: green;
`;

render(
    <div>
        <ScButton>Styled Fusion Button</ScButton>
    </div>
);
复制代码

对伪元素、伪选择器以及嵌套的支持

由于 styled-components 采用 stylis 作为预处理器,因此提供了对伪元素、伪选择器以及嵌套写法的支持(跟 Les 很类似)。其中,& 指向组件本身:

代码语言:javascript
复制
const ScDiv = styled.div`
   color: blue;

  &:hover {
    color: red; // 被 hover 时的样式
  }

  & ~ & {
    background: tomato; // ScDiv 作为 ScDiv 的 sibling
  }

  & + & {
    background: lime; // 与 ScDiv 相邻的 ScDiv
  }

  &.something {
    background: orange; // 带有 class .something 的 ScDiv
  }

  .something-child & {
    border: 1px solid; // 不带有 & 时指向子元素,因此这里表示在带有 class .something-child 之内的 ScDiv
`;

render(
  <React.Fragment>
    <ScDiv>Hello world!</ScDiv>
    <ScDiv>How ya doing?</ScDiv>
    <ScDiv className="something">The sun is shining...</ScDiv>
    <ScDiv>Pretty nice day today.</ScDiv>
    <ScDiv>Don't you think?</ScDiv>
    <div className="something-else">
      <ScDiv>Splendid.</ScDiv>
    </div>
  </React.Fragment>
)
复制代码

渲染的结果如图所示:

通过 .attrs 传递 props 或 attributes

.attrs允许传递静态或动态的 props,或者第三方的 props 给组件。attrs 一般接收函数作为参数,并且该函数的参数是组件接收到的 props,函数的返回值将会与 propsmerge,由此可以得到组件最终的 props。例如:

代码语言:javascript
复制
const ScInput = styled.input.attrs(props => ({
  // 定义静态的 prop
  type: "text",

 // 定义动态的 prop
  size: props.size || "1em"
}))`
  color: palevioletred;
  font-size: 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;

  /* 注意,最终组件的 props 是合并 attrs 返回值的 props 的结果 */
  margin: ${props => props.size};
  padding: ${props => props.size};
`;

render(
  <div>
    <ScInput placeholder="A small text input" />
    <br />
    <ScInput placeholder="A bigger text input" size="2em" />
  </div>
);
复制代码

注意,在对 styled-componnets 进行包装时,.attrs 的应用顺序是从最里面的样式化的组件到最外面的样式化的组件。因此外层的包装可以对内层的 .attrs 做覆盖。例如:

代码语言:javascript
复制
const ScInputInner = styled.input.attrs(props => ({
  type: "text",
  size: props.size || "1em"
}))`
  border: 2px solid palevioletred;
  margin: ${props => props.size};
  padding: ${props => props.size};
`;

// ScInputInner 的 attrs 将被先应用,然后是这个 ScInputOutter 的 attrs 被应用
const PasswordInput = styled(Input).attrs({
  // 这会覆盖 ScInputInner 的 type: text
  type: "password" 
})`
  /* 同样,这会覆盖 ScInputInner 的 border*/
  border: 2px solid aqua;
`;
复制代码

主题切换

styled-components 通过导出 <ThemeProvider> 组件从而能支持主题切换。 <ThemeProvider>是基于 React 的 Context API 实现的,可以为其下面的所有 React 组件提供一个主题。在渲染树中,任何层次的所有样式组件都可以访问提供的主题。例如:

代码语言:javascript
复制
// 通过使用 props.theme 可以访问到 ThemeProvider 传递下来的对象
const Button = styled.button`
  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border-radius: 3px;
  color: ${props => props.theme.main};
  border: 2px solid ${props => props.theme.main};
`;

// 为 Button 指定默认的主题
Button.defaultProps = {
  theme: {
    main: "palevioletred"
  }
}

const theme = {
  main: "mediumseagreen"
};

render(
  <div>
    <Button>Normal</Button>
    // 采用了 ThemeProvider 提供的主题的 Button
    <ThemeProvider theme={theme}>
      <Button>Themed</Button>
    </ThemeProvider>
  </div>
);
复制代码

ThemeProvidertheme除了可以接受对象之外,还可以接受函数。函数的参数是父级的 theme对象。此外,还可以通过使用 theme prop 来处理 ThemeProvider 未定义的情况(这跟上面的 defaultProps是一样的效果),或覆盖 ThemeProvider的 theme。例如:

代码语言:javascript
复制
const ScButton = styled.button`
  color: ${props => props.theme.fg};
  border: 2px solid ${props => props.theme.fg};
  background: ${props => props.theme.bg};
`;

const theme = {
  fg: "palevioletred",
  bg: "white"
};

const invertTheme = ({ fg, bg }) => ({
  fg: bg,
  bg: fg
});

render(
  // ThemeProvider 未定义的情况
  <ScButton theme={{
    	fg: 'red',
      bg: 'white'
    }}>Default Theme</ScButton>
  <ThemeProvider theme={theme}>
    <div>
      <ScButton>Default Theme</ScButton>
    	// theme 接收的是一个函数,函数的参数是父级的 theme
      <ThemeProvider theme={invertTheme}>
        <ScButton>Inverted Theme</ScButton>
      </ThemeProvider>
      // 覆盖 ThemeProvider的 theme
      <ScButton theme={{
        fg: 'red',
      	bg: 'white'
        }}>Override Theme</ScButton>
    </div>
  </ThemeProvider>
);
复制代码

CSS Prop

当不想创建额外的组件,而是只为了应用一些样式时,CSS Prop 可以实现这一点。它适用于普通的 HTML 标签和组件,并支持任何 styled-components 支持的特性,包括基于 props、主题和自定义组件的调整。注意,为了使 CSS Prop生效,需要用到 styled-components 提供的 babel-plugin

代码语言:javascript
复制
<div
  css={`
    background: papayawhip;
    color: ${props => props.theme.colors.text};
  `}
/>


<MyComponent
  css="padding: 0.5em 1em;"
/>
复制代码

除了上述用法之外,还有一种用法是提取多个 styled-components 组件会用到的共同样式,这样可以减少冗余代码

代码语言:javascript
复制
import styled, {
     css
} from 'styled-components';
import {
    Button as FusionButton
} from '@alifd/next';


const mixinCommonCSS = css`
    margin-top: 12px;
    border: 1px solid grey;
    borde-radius: 4px;
`;

const ScButton = styled.button`
    ${mixinCommonCSS}
    color: yellow;
`;

const ScFusionButton = styled(FusionButton)`
    ${mixinCommonCSS}
    color: blue;
`;
复制代码

从编译方法来看 styled-components

只创建样式化组件,但不实例化组件,不会产生额外开销

当在应用中第一次 import styled-components 时,它会创建一个内部计数器变量 counter来计算通过工厂函数(styled())创建的所有组件。当 styled-components 创建一个新组件时,它也会创建内部标识符 componentId。 以下是标识符的计算方式:

代码语言:javascript
复制
// 计算标识符
counter++;
const componentId = 'sc-' + hash('sc' + counter); // 这就是一开提到的附加到组件上的类名 sc- 的计算方式,因为使用了 hash 算法,因此可以确保唯一
复制代码

创建标识符后,styled-components将新的 HTML <style> 元素插入页面的 <head>(如果它是第一个组件并且该元素尚未插入),并添加带有 componentId 的特殊注释标记到 稍后将使用的元素。 假设生成的 componentIdsc-bdhhai

代码语言:javascript
复制
// 注意这个 data-styled-components
<style data-styled-components>
  /* sc-component-id: sc-bdhhai */
</style>
复制代码

实例化组件时,传递给 styled() 函数目标的目标组件的 componentId 保存在静态字段中:

代码语言:javascript
复制
StyledComponent.componentId = componentId;
StyledComponent.target = TargetComponent;
复制代码

因此,当只创建样式化的组件 styled-components时,没有任何性能开销,因为只有在创建组件的时候componentId才会保留在内存中。即使你定义了数百个样式化组件,但是并不使用它们,你得到的只是一个或多个带有几百条注释的 <style> 元素

继承自 BaseStyledComponent类的工厂函数 styled

假设我们用 styled-componnets 创建了一个样式化的 Button组件 ScButton,并实例化了组件

代码语言:javascript
复制
const ScButton = styled.button`
  font-size: ${({ sizeValue }) => sizeValue + 'px'};
  color: coral; 
  padding: 0.25rem 1rem; 
  border: solid 2px coral; 
  border-radius: 3px;
  margin: 0.5rem;
  &:hover {
    background-color: bisque;
  }
`;

ReactDOM.render(
  <ScButton sizeValue={24}>I'm a button</ScButton>,
  document.getElementById('root')
);
复制代码

BaseStyledComponent有自己的生命周期函数 componentWillMount, 。下面简单介绍下这几个生命周期函数的作用:

ComponnetWillMount

  • 解析 tagged templatestring 类型的 evaluatedStyles
代码语言:javascript
复制
font-size: 24px; // sizeValue 用 props 传递的 24 
color: coral; 
padding: 0.25rem 1rem; 
border: solid 2px coral; 
border-radius: 3px;
margin: 0.5rem;
&:hover {
  background-color: bisque;
}
复制代码
  • 生成 CSS 对应的类名:类名基于 componentIdevaluatedStyles ,通过 MurmurHash 算法生成,并以 generatedClassNam存在组件的 state中。
代码语言:javascript
复制
const className = hash(componentId + evaluatedStyles);
复制代码
  • CSS 预处理:styled-components利用了 stylis CSS 预处理器,从而得到有效的 CSS 样式字符串
代码语言:javascript
复制
const selector = '.' + className;
const cssStr = stylis(selector, evaluatedStyles);
复制代码

ScButton最终的 CSS 样式字符串如下所示:

代码语言:javascript
复制
.jsZVzX {
  font-size: 24px;
  color: coral;
  padding: 0.25rem 1rem;
  border: solid 2px coral;
  border-radius: 3px;
  margin: 0.5rem;
}

.jsZVzX:hover{
  background-color: bisque;
}
复制代码

注意,styled-componentsv1(现在已经是 v5) 中使用了 PostCSS,一种非常流行的 CSS-in-JS 和 CSS 工具和构建管道工具,用于转换 CSS。具体来说,使用了 postcss-safe-parser, postcss-nested, inline-style-prefixer,分别用于解析 CSS、取消嵌套(Unnesting)以及自动加上前缀。 PostCSS 也与 AST 一起工作,这意味着我们有一个抽象的 CSS 语法树结构,我们可以随意更改。 就像 Babel 改变 JavaScript 所做的那样。

由于 styled-components可以立即安装和使用,因此所有的 CSS 流程(Pipeline)都是在 runtime 包中。由于 PostCSS的体积过于大,导致 styled-components的 bundle 体积有 21kb(after minimized and gzipd), 并且还带有 AST。

因此从 v2开始,styled-components 的开发团队就用高度专业化,体积小,速度极快的 stylis 替换了 PostCSS,成功把包体积降到了 9kB,并在一次传递中转换 CSS。

  • 将 CSS 样式字符串注入页面: 将CSS 注入上面提到的页面 head<style> 元素,紧跟在组件的注释标记之后:
代码语言:javascript
复制
<style data-styled-components> 
/* sc-component-id: sc-bdhhai */ 
.sc-bdVaJa {} 
.jsZVzX{font-size:24px;color:coral; ... } 
.jsZVzX:hover{background-color:bisque;} 
</style>
复制代码
  • 渲染:CSS 注入到页面之后,styled-components要做的就是创建一个带有对应 className: sc-bdhhai 的元素了
代码语言:javascript
复制
const TargetComponent = this.constructor.target; // In our case just 'button' string.
const componentId = this.constructor.componentId;
const generatedClassName = this.state.generatedClassName;

return ( 
  <TargetComponent 
    {...this.props} 
    className={this.props.className + ' ' + componentId + ' ' + generatedClassName}
  />
);
复制代码

componentWillReceiveProps

如果在 button完成 mounted之后更改其 props,如下所示。每次单击按钮时,都会使用递增的 sizeValue 属性调用 componentWillReceiveProps(),并执行与 componentWillMount() 相同的操作:

代码语言:javascript
复制
let sizeValue = 24;

const updateButton = () => {
  ReactDOM.render(
    <ScButton sizeValue={sizeValue} onClick={updateButton}>
      Font size is {sizeValue}px
    </ScButton>,
    document.getElementById('root')
  );
  sizeValue++;
}

updateButton();
复制代码

点击几次之后,查看生成的 styles,大概是这个样子。可以看到,每个 CSS 类的唯一区别 font-size属性,并且不会删除未使用的 CSS 类。这是由于删除它们会增加性能开销,而保持它们不会。此外,在样式字符串中没有插值的组件被标记为 isStatic并且在 componentWillReceiveProps() 中检查这个标志以跳过相同样式的不必要计算。

代码语言:javascript
复制
<style data-styled-components>
  /* sc-component-id: sc-bdVaJa */
  .sc-bdhhai {} 
  .jsZVzX{font-size:24px;color:coral; ... } .jsZVzX:hover{background-color:bisque;}
  .kkRXUB{font-size:25px;color:coral; ... } .kkRXUB:hover{background-color:bisque;}
  .jvOYbh{font-size:26px;color:coral; ... } .jvOYbh:hover{background-color:bisque;}
  .ljDvEV{font-size:27px;color:coral; ... } .ljDvEV:hover{background-color:bisque;}
</style>
复制代码

采用 .attrs优化 styled-components

从上面可以看到,采用 font-size: ${({ sizeValue }) => sizeValue + 'px'};这样的插值方法,每次组件重新渲染都会产生新的类。可以将其替换为 attrs属性来提升性能。但是,styled-components的作者也不建议把这种方法用于所有的动态样式,而是所有结果数量减少的动态样式使用 .attrs属性。例如,如果有一个具有可自定义字体大小的组件,或从服务器加载的具有不同颜色的标签列表,则最好使用样式属性 attrs。但是,如果您在一个组件中有多种按钮,例如 default、primary、warn 等,则可以在样式字符串中使用带条件的插值。

代码语言:javascript
复制
const Button = styled.button.attrs({
  style: ({ sizeValue }) => ({ fontSize: sizeValue + 'px' })
})`
  color: coral;
  padding: 0.25rem 1rem; 
  border: solid 2px coral; 
  border-radius: 3px;
  margin: 0.5rem;
  &:hover {
    background-color: bisque;
  }
`;
复制代码

CSS Modules Vs. styled-components

为了解决 CSS 本身的不足之处,CSS Modules 是编译时的原生 CSS 解决方案,而 styled-components 是基于 CSS-in-JS 理念,运行时的解决方案。前端社区也一直都存在要不要用 CSS-in-JS(典型代表 styled-components)的讨论。styled-components 要解决的很多问题,CSS Modules 也可以解决,并且时机是在编译时而非运行时。反观 styled-components,它的执行时机是在运行时,虽然它的开发团队采取了很多优化措施,但运行时的开销导致的影响是不可避免的。 styled-components 的反对派们的主要观点包括

  • 虽然 styled-components 解决了全局命名空间和样式冲突, 但是 CSS Modules、Shadow DOM 和命名约定很久以前在社区中就解决了这个问题。这不是一个开始使用 styled-components 的充分理由;
  • styled-components 可以让使用样式组件使代码更简洁是一个误区。例如:
代码语言:javascript
复制
// styled-componnets
<TicketName></TicketName>

// CSS Modules
<div className={styles.ticketName}></div>
复制代码

虽然 TicketName看起来更语义化了,但是这个命名完全取决于写代码的人,如果起了不表意的样式化组件名,反而有副作用。并且这带来的收益很小

  • 虽然 styled-components 提供了扩展样式的能力,但通过 CSS Modules 的组合 (Composition)能力,或者 SASS 继承 mixin @extend都可以做到。这也不是一个开始使用 styled-components 的充分理由;
  • 虽然 styled-components 可以利用 props 对组件进行有条件的样式设置,这很符合 React 体系,并且利用了 JavaScript 的强大功能,然而,这也意味着风格更难解释,并且 CSS 同样也可以做到:
代码语言:javascript
复制
// styled-components
const ScButton = styled.button`
  background: ${props => props.primary ? '#f00' : props.secondary ? '#0f0' : '#00f'};
  color: ${props => props.primary ? '#fff' : props.secondary ? '#fff' : '#000'};
  opacity: ${props => props.active ? 1 : 0};
`;

<ScButton primary />
<ScButton secondary />
<ScButton primary active={true} />
复制代码
代码语言:javascript
复制
// & 基于 CSS 预处理器的能力
button {
  background: #00f;
  opacity: 0;
  color: #000;
  
  &.primary,
  &.seconary {
    color: #fff;
  }
  &.primary {
    background: #f00;
  }
  &.secondary {
    background: #0f0;
  }
  &.active {
    opacity: 1;
  }
}
复制代码
  • styled-components 允许在同一个文件中包含样式和 JavaScript。但是将样式和标记塞入一个文件中是一个可怕的解决方案,它不仅使版本控制难以跟踪,而且还很容易写出非常长的 JSX 代码。此外,如果必须在同一个文件中包含 CSS 和 JavaScript,请考虑使用 css-literal-loader,它在构建时使用 extract-text-webpack-plugin 提取 CSS,并使用标准 css loader 配置来处理 CSS。
  • styled-components 能提升开发体验也是一个误区:当样式出现问题时,整个应用程序将因长堆栈跟踪错误而崩溃。而使用 CSS 时,“样式错误”只会错误地呈现元素。此外,无效的样式会被简单地忽略,这可能导致比较难以调试的问题。
  • styled-components 是运行时的方案,这会对前端性能产生不利影响,包括
    • styled-components 无法提取到静态 CSS 文件中(例如使用 extract-text-webpack-plugin),这意味着在 styled-components 解析样式并将它们添加到 DOM 之后,浏览器才能开始解释样式。
    • 缺少单独的文件意味着您无法单独缓存 CSS 和 JavaScript。
    • 所有样式化的组件都被包装在一个额外的 HoC 中,会产生不必要的性能损失。注意,react-css-modules 也有这样的问题,请使用 babel-plugin-react-css-modules

上面这些观点主要想提醒开发者不要盲目去使用 styled-componentsstyled-components 本身是个很优秀的 CSS-in-JS 解决方案,并且有更好的跨平台支持能力。

References:

Less Sass

css-modules

CSS Modules github

styled-components 官方网站

# Styled Components: Enforcing Best Practices In Component-Based Systems

why-i-don-t-like-to-use-styled-components

css-evolution-from-css-sass-bem-css-modules-to-styled-components

styled-components-to-use-or-not-to-use

getting-the-most-out-of-styled-components-7-must-know-features

how-styled-components-works

with-styled-components-into-the-future

stop-using-css-in-javascript-for-web-development

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先从 CSS 说起
  • CSS Modules
    • 基本用法
      • 全局变量
        • 组合 (Composition)
          • 同 CSS 文件中 class 的组合
          • 不同 CSS 文件间 class 的组合
          • 在 CSS Modules 里面使用变量
      • styled-componnets 💅(这个 logo 有点魔性)
        • 安装
          • 基本用法
            • styled-componnets 的动态样式
              • 扩展组件样式
                • 扩展第三方组件
                  • 对伪元素、伪选择器以及嵌套的支持
                    • 通过 .attrs 传递 props 或 attributes
                      • 主题切换
                        • CSS Prop
                        • 从编译方法来看 styled-components
                          • 只创建样式化组件,但不实例化组件,不会产生额外开销
                            • 继承自 BaseStyledComponent类的工厂函数 styled
                              • ComponnetWillMount
                              • componentWillReceiveProps
                            • 采用 .attrs优化 styled-components
                            • CSS Modules Vs. styled-components
                            • References:
                            相关产品与服务
                            云服务器
                            云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档