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
语言去写样式的话,主要可能会遇到以下两个问题:
@import
机制并不算真正的模块机制,因为 @import
是在一个 CSS 文件里面引入另一个 CSS 文件,并且只有执行到 @import
语句的时候才会触发浏览器下载被引入的 CSS 文件。这会对网页加载速度产生不利影响。
为了解决 CSS 存在的这些不足之处,前端社区出现了很多种解决方案。例如,Saas 或者 Less。Sass
和 Less
都属于 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 用于描述网页样式,一个典型的网页包含许多元素或组件,例如菜单、按钮、输入框等,这些元素或组件的样式是由单个或多个 CSS 规则决定的,这些规则被包含在一个 CSS 文件当中,并且可供包含该文件的整个网页访问。也就是说。所有 CSS 样式都是全局的,任何一个组件的样式规则,都对整个页面有效。如果希望某些样式仅对页面的某个组件可见,应该怎么办呢?
CSS Modules
就是为了解决这种场景而生的,它加入了局部作用域和模块依赖,可以保证某个组件的样式不会影响到其他组件。具体而言,CSS Modules 通过工程化的方法,可以将类名编译为哈希字符串,从而使得每个类名都是独一无二的,不会与其他的选择器重名,由此可以产生局部作用域。CSS Modules
提供各种插件,支持不同的构建工具,包括 Webpack, Browserify, NodeJS
等。其中,Webpack
的 CSS Loader 插件提供了对 CSS Modules 的支持,可以很方便地打开 CSS Modules
功能。以如下 Demo 为例:
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
相关的关键代码是
{ 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;icss
:icss
只会编译低级别的Interoperable CSS
格式,用于声明 CSS 和其他语言之间的 :import
和 :export
依赖项。object
: 一个配置对象,默认所有文件都开启 CSS Modules,具体情况根据 modules.auto
的值而定。
开启 CSS Modules
之后,所有的类名都会被编译成一个哈希字符串,以下面组件 App.jsx
及其样式文件 App.css
为例:
// App.css
.appTitle {
color: red;
}
复制代码
// 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
这种命名方式不能用 .
访问法,即:
// 驼峰式
<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
的类名会被编译为:
<h1 class="App__appTitle__GyYTO">
Hello World
</h1>
复制代码
并且 App.css
也会被编译为
.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
。
// App.css
.appTitle {
color: red;
}
:local(.appTitle1) {
color: yellow;
}
:global(.globalTitle){
color: green;
}
复制代码
在 App.jsx
中,就可以以普通 CSS 的写法去引用全局 class 了。此时,渲染的 Hello World 是红色,而 Hello World Again 是绿色,因为它用的是全局变量。
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 被编译为
.App__appTitle__GyYTO {
color: red;
}
.App__appTitle1__NHgyT {
color: yellow;
}
// 全局类名不会被编译为哈希类名
.globalTitle {
color: green;
}
复制代码
除了局部作用域,CSS Modules
的另一个很重要的特性是组合(Composition),一个选择器可以继承另一个选择器的规则。组合可以发生在同一个 CSS 文件的不同类之间,也可以发生在不同 CSS 文件的不同类之间。后者可以理解为在 CSS 中加入了模块机制。
在 App.css
中建立一个用于定义背景颜色的 class appBackground
,并且新建一个继承 appBackground
以及 appTitle
的 class appStyle
。
// App.css
.appTitle {
color: red;
}
.appBackground {
color: blue;
}
.appStyle {
composes: appTitle appBackground; // 不同类之间用空格隔开
padding: 8px;
}
复制代码
注意,composes
仅对局部(local-scoped)且只包含单独的类名的选择器有效,并且可以一个选择器里面可以存在多条 composes
规则,但所有的 composes
规则都必须定义在其他规则的前面。此时,App.css 会被编译为:
// 编译后的 App.css
.App__appTitle__GyYTO {
color: red;
}
.App__appBackground__NhvyT {
color: yellow;
}
.App__appStyle__dahOP {
border: 1px solid black;
}
复制代码
App.jsx
如下:
import React from 'react';
import styles from './App.css';
export default () => {
return (
<h1 className={styles['appStyle']}>
Hello World
</h1>
);
};
复制代码
那么 h1
的 class 会被编译为
<h1 className="App__appTitle__GyYTO App__appBackground__NhvyT App__appStyle__dahOP">
复制代码
假设除了 App.css
之外,还有一个 another.css
,并且App.css
继承 another.css
其中的规则:
/* another.css */
.ohterBackground {
background-color: blue;
}
/* App.css */
.appStyle {
composes: ohterBackground from './another.css';
color: red;
}
复制代码
这样,渲染出来的 h1
会有蓝色的背景颜色以及红色的字体颜色。注意,当一个类从不同文件中组合多个类时,被组合类的规则的应用顺序是不可预测的。因此,应该要避免来自不同文件的多个类名中为同一属性定义不同的值。 例如 App.css
继承 another.css
和 another1.css
的两条规则:
/* 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
,由于后定义的属性值会覆盖前面定义的同属性的值,这会使得应用了 appStyle
的 h1
标签实际的背景颜色会变得不可预测,可能是 blue (如果是 ohterBackground
后应用)或者 red (如果是 ohterBackground1
后应用)。此外,还注意组合不应该形成循环依赖,这会使得 Css Modules 抛错。
// 循环依赖会导致错误,不要这样做!!
/* another.css */
.ohterBackground {
composes: appStyle from './App.css';
background-color: blue;
}
/* App.css */
.appStyle {
composes: ohterBackground from './another.css'; // 形成了循环依赖
color: red;
}
复制代码
此外,局部 class 中还可以组合全局 class,例如:
/* global.css */
:global(.globalBackground){
color: green;
}
/* App.css */
.appStyle {
composes: globalBackground from './global.css';
color: red;
}
复制代码
在安装 PostCSS
以及 postcss-modules-values 之后,并且把 postcss-loader 加入 webpack 配置之后,在 CSS Modules 使用变量了。例如:
/* 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 Modules
与 Sass / Less
进行组合使用,从而既能拥有 Sass / Less
的 CSS 预处理器的能力(规则、变量、混入、选择器、继承等),又可以拥有 CSS Modules
提供的局部作用域的能力,避免全局污染。
介绍完了 CSS Modules
,终于轮到 styled-components 💅
了。styled-components
在我的日常开发中用的很多,并且个人感觉的确非常好用,这种 CSS-in-JS
的写法能让组件的样式定义变得很明了且带有语义特性(但也可能会让组件的 tsx 文件变得很长)。styled-components
的基本思想是通过删除样式和组件之间的映射来强制执行最佳实践,同时还拆分了容器组件和展示组件,确保开发人员只能构建小型且集中的组件。
但是,我有同事是 styled-components
的反对者,因为它在运行时引入了 PostCSS,应用启动时编译样式。应充分利用编译期能力,把 CSS 在编译期确定下来,这样才能享受浏览器内核自己的优化。后来我自己查阅相关文章才发现,前端社区早就有很多关于是否应该使用styled-components
的讨论。首先让我们了解什么是 styled-components
:
styled-components 以组件的形式来声明样式,让样式也成为组件
Styled Components 的官方网站将其优点归结为:
styled-components
持续跟踪页面上渲染的组件,并自动注入样式。结合使用代码拆分, 可以实现仅加载所需的最少代码。
styled-components
为样式生成唯一的 class name,开发者不必再担心 class name 重复、覆盖以及拼写的问题。(CSS Modules
通过哈希编码局部类名实现这一点)
styled-components
可以很轻松地知道代码中某个 class 在哪儿用到,因为每个样式都有其关联的组件。如果检测到某个组件未使用并且被删除,则其所有的样式也都被删除。
props
或者全局主题适配样式,无需手动管理多个 classes。(这一点很赞)
styled-components
处理。
因为 styled-components
做的只是在 runtime 把 CSS 附加到对应的 HTML 元素或者组件上,它完美地支持所有 CSS。 媒体查询、伪选择器,甚至嵌套都可以工作。但是要注意,styled-components
是 React
下的 CSS-in-JS
的实践,因此下面的所有例子的技术栈都是 React
。
# 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 相关联。
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 子树的那部分,而不是仅仅计算它们之间变化的差异,从而导致性能瓶颈和不可预测的行为。
// ❌ 绝对不要这样写
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 Attributes
给 DOM
。如果是自定义 React
组件(例如 styled(MyComponent
)),则 styled-components
会传递所有的 props
。
styled-components
支持通过 props
实现动态样式,并且可以与 TypeScript
配合使用。并且 VsCode 还有一款插件 vscode-styled-components
能识别 styled-components
,并能自动进行 CSS 高亮、补全、纠正等。
# 与 TypeScript 配合使用
$ npm install @types/styled-components -D
复制代码
下面例子展示了一个样式化的 Button
接收 primary
属性,并根据该属性调整背景颜色 background
以及 color
。
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
:
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
属性来制定最终渲染的内容(可以是原生的元素或者是自定义组件),例如:
// 创建一个继承 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
就可以在自己创建的或是第三方组件中运行。
// 在自己创建的组件上运行
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()
扩展
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
很类似)。其中,&
指向组件本身:
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
,或者第三方的 props
给组件。attrs
一般接收函数作为参数,并且该函数的参数是组件接收到的 props
,函数的返回值将会与 props
做 merge
,由此可以得到组件最终的 props
。例如:
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
做覆盖。例如:
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 组件提供一个主题。在渲染树中,任何层次的所有样式组件都可以访问提供的主题。例如:
// 通过使用 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>
);
复制代码
ThemeProvider
的 theme
除了可以接受对象之外,还可以接受函数。函数的参数是父级的 theme
对象。此外,还可以通过使用 theme prop 来处理 ThemeProvider
未定义的情况(这跟上面的 defaultProps
是一样的效果),或覆盖 ThemeProvider
的 theme。例如:
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
可以实现这一点。它适用于普通的 HTML 标签和组件,并支持任何 styled-components 支持的特性
,包括基于 props
、主题和自定义组件的调整。注意,为了使 CSS Prop
生效,需要用到 styled-components
提供的 babel-plugin。
<div
css={`
background: papayawhip;
color: ${props => props.theme.colors.text};
`}
/>
<MyComponent
css="padding: 0.5em 1em;"
/>
复制代码
除了上述用法之外,还有一种用法是提取多个 styled-components
组件会用到的共同样式,这样可以减少冗余代码。
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
。 以下是标识符的计算方式:
// 计算标识符
counter++;
const componentId = 'sc-' + hash('sc' + counter); // 这就是一开提到的附加到组件上的类名 sc- 的计算方式,因为使用了 hash 算法,因此可以确保唯一
复制代码
创建标识符后,styled-components
将新的 HTML <style>
元素插入页面的 <head>
(如果它是第一个组件并且该元素尚未插入),并添加带有 componentId
的特殊注释标记到 稍后将使用的元素。 假设生成的 componentId
为sc-bdhhai
// 注意这个 data-styled-components
<style data-styled-components>
/* sc-component-id: sc-bdhhai */
</style>
复制代码
实例化组件时,传递给 styled()
函数目标的目标组件的 componentId
保存在静态字段中:
StyledComponent.componentId = componentId;
StyledComponent.target = TargetComponent;
复制代码
因此,当只创建样式化的组件 styled-components
时,没有任何性能开销,因为只有在创建组件的时候componentId
才会保留在内存中。即使你定义了数百个样式化组件,但是并不使用它们,你得到的只是一个或多个带有几百条注释的 <style>
元素。
BaseStyledComponent
类的工厂函数 styled
假设我们用 styled-componnets
创建了一个样式化的 Button
组件 ScButton
,并实例化了组件
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 template
到 string
类型的 evaluatedStyles
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;
}
复制代码
componentId
和 evaluatedStyles
,通过 MurmurHash 算法生成,并以 generatedClassNam
存在组件的 state
中。const className = hash(componentId + evaluatedStyles);
复制代码
styled-components
利用了 stylis
CSS 预处理器,从而得到有效的 CSS 样式字符串
。const selector = '.' + className;
const cssStr = stylis(selector, evaluatedStyles);
复制代码
ScButton
最终的 CSS 样式字符串如下所示:
.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-components
在 v1
(现在已经是 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。
head
的 <style>
元素,紧跟在组件的注释标记之后:<style data-styled-components>
/* sc-component-id: sc-bdhhai */
.sc-bdVaJa {}
.jsZVzX{font-size:24px;color:coral; ... }
.jsZVzX:hover{background-color:bisque;}
</style>
复制代码
styled-components
要做的就是创建一个带有对应 className: sc-bdhhai
的元素了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()
相同的操作:
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()
中检查这个标志以跳过相同样式的不必要计算。
<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
等,则可以在样式字符串中使用带条件的插值。
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
可以让使用样式组件使代码更简洁是一个误区。例如:
// 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 同样也可以做到:// 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} />
复制代码
// & 基于 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
。styled-components
本身是个很优秀的 CSS-in-JS
解决方案,并且有更好的跨平台支持能力。
# 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
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。