啥是CSS in JS?
以前,网页开发有一个原则,叫做"关注点分离"(separation of concerns)
各个技术各司其职,只负责自己的领域,不要混合在一起,对于我们日常开发来说,主要是三种技术分离:
- HTML:负责网页的结构,又称语义层
- CSS:负责网页样式,又称视觉层
- JS:负责网页的逻辑和交互,又称逻辑层或交互层
简而言之,不要写"行内样式"(inline style)和"行内脚本"(inline script)
// bad
<h1 style="color:red;font-size:46px;" onclick="alert('Hi')">
Hello World
</h1>
但是随着React为首的现代前端开发框架兴起,这个原则不再适用了。因为React是以组件为最小颗粒度划分的,强制我们将HTML
、CSS
、JS
写在一起。在JS中维护CSS(css in js)的方案成为了当代前端社区的新趋势
先看一段不使用CSS in JS方案的React代码:
const style = {
'color': 'red',
'fontSize': '46px'
};
const clickHandler = () => alert('hi');
ReactDOM.render(
<h1 style={style} onclick={clickHandler}>
Hello, world!
</h1>,
document.getElementById('example')
);
上面这段代码在一个文件(组件)里,封装了结构、样式和逻辑。完全背离了“关注点分离”的原则
但是不得不说,确实也有一定的优点,比如,组件的隔离,每个组件包含了所有需要的代码,不依赖外部,组件之间无耦合,方便复用。
关注点混合React在JS里实现了对HTML和CSS的封装,通过方法去操作网页样式和结构。
比如,React对HTML的封装是JSX。
而对CSS的封装非常简单,它沿用了DOM的style属性对象
const style = {
'color': 'red',
'fontSize': '46px'
};
主要使用驼峰写法,这是JS操作CSS的约定。
显而易见的,很难写,虽然说在一定程度上实现了样式的组件化封装,但是由于内联样式缺少CSS所能提供的许多特性,比如伪选择器、动画与渐变、媒体选择器等,下面具体来看。
传统CSS缺陷
缺乏模块组织
传统CSS和JS都没有模块的概念,后来在JS界陆续有了CommonJS和ES Module,CSS in JS可以用模块化的方式组织CSS,依托于JS的模块化方案:
// button1.ts
import styled from '@emotion/styled'
// ES module组织方式
export const Button = styled.button`
font-size: 16px;
`
// button2.ts
import styled from '@emotion/styled'
export const Button = styled.button`
font-size: 16px;
`
上面代码都是Button,但是可以用于不同的文件,不同模块,不会互相冲突,解决了模块组织问题
缺乏作用域
传统CSS只有一个全局作用域,比如说一个class可以匹配全局的任意元素。随着项目越来越大,CSS会变得难以组织,容易失控。CSS in JS可以通过生成独特的选择符,来实现作用域效果:
/* css in js自动生成的classname */
.css-1c4ktv6 >* {
margin-top: 20px;
}
这样保证了整个rule不会被应用到全局,只会应用在我们指定的作用域
隐式依赖,让样式难以追踪
.target .name h1 {
color: red;
}
body #container h1 {
color: blue;
}
<!doctype html>
<html lang="en">
<body>
<div id='container'>
<div class='target'>
<div class='name'>
<h1>我是啥颜色?</h1>
</div>
</div>
</div>
</body>
</html>
好,即使通过选择器优先级可以判断,但还是没有很直观,因为h1上面没有附带任何样式,如果想要追踪,需要通过全局搜索或着挨个检查才能找到影响h1样式的代码片段
export const Title = styled.h1`
color: blue;
`
// 直接把样式通过普通组件来使用
<Title>
我是啥颜色?
</Title>
么有变量
传统CSS规则里面没有变量,但是在CSS in JS中可以方便的控制变量,可以进行条件判断、变量的计算都会非常方便
const Container = styled.div(props => {
display: 'flex';
flexDirection: props.column && 'column'
})
CSS选择器与HTML元素耦合
.target .name h1 {
color: red;
}
body #container h1 {
color: blue;
}
<!doctype html>
<html lang="en">
<body>
<div id='container'>
<div class='target'>
<div class='name'>
<h1>我是啥颜色?</h1>
</div>
</div>
</div>
</body>
</html>
如果想改标签的名字,比如h1改成h2,就必须要同时改动CSS和HTML。
可以成为React好搭档的,CSS替代方案
对于Angular和Vue来说,这两个都有框架原生提供的CSS封装方案,比如Vue的<style scoped>
标签和Angular组件的viewEncapsukation
属性。React本身的设计原则决定了其不会提供原生CSS封装方案,或者说CSS封装并不是React框架本身的关注点。
由于CSS的封装非常弱,React社区从很早的时候就开始寻找相关替代办法,一系列的第三方库,用来加强React对CSS的操作。统称为CSS in JS
CSS in JS在2014年由Facebook的员工Vjeux在《NationJS会议》上提出。可以借用JS解决许多CSS本身的一些缺陷,比如全局作用域、生效顺序依赖于样式加载顺序、常量共享等等问题。
前端社区也在各个方向上探索着CSS in JS。甚至,Chrome v85为CSS in JS的需求修复了一个问题,这也可以从侧面看出CSS in JS已经得到了浏览器厂商的重视
image.png
使用JS语言写CSS
根据不完全统计,各种CSS in JS的库至少有47种
这么多库里面代表库有styled-component和emotion,通过几年间的竞争,为了满足开发者的需求,同时结合社区的使用反馈,在不断的更新过程中,几乎在接口上使用同样的接口设计,以Emotion为例:
-
CSS prop
可以算是内联样式的升级版,用户定义的内联样式以 JSX 标签属性的方式与组件紧密结合 -
样式组件
更像是 CSS 的组件化封装,将样式抽象为语义化的标签,把样式从组件实现中分离出来,让 JSX 结构更“干净整洁”,复用性更高
这两种方案在内部实现中都会享受当代前端工程化的福利,如语法检查、自动增加浏览器属性前缀、帮助开发者增强样式的浏览器兼容性等等
同时利用vscode-styled-components
等代码编辑器插件,我们可以在 JS 代码中增加对于 CSS 的语法高亮支持
这次分享重点介绍emotion,它对React做了很好的适应,在github中有12.9k的star,官方文档
Emotion
Emotion支持的两种样式定义方法
- String Style
- Object Style
String Style
需结合css函数使用,该函数返回一个对象(包含样式名,样式字符串)给Emotion底层使用
import { css, jsx } from '@emotion/react'
const color = 'darkgreen'
render(
<div
css={css`
background-color: hotpink;
&:hover {
color: ${color};
}
`}
>
This has a hotpink background.
</div>
)
另外,关于css函数的写法
// 标签模板字符串
const style = css`
color: "black";
&:hover {
color: "white";
}
`;
// 等同于
const style = css(`
color: "black";
&:hover {
color: "white";
}
`);
Object Styles
一个JS对象,使用驼峰式命名,可用在css prop
,Styled Components
,css函数
中
import { css, cx } from '@emotion/css'
const objectStyle = 'white'
render(
<div
css={{
backgroundColor: 'hotpink',
'&:hover': {
color: ${objectStyle};
}
}}
>
Hover to change color.
</div>
)
这种写法比起 React 自带的 style 的写法功能更强大,比如可以处理级联、伪类等 style 处理的不了的情况
Style Precedance
props 对象中的 css 属性优先级⾼于组件内部的 css 属性
/** @jsx jsx */
import { jsx } from '@emotion/react'
const P = props => (
<p
css={{
margin: 0,
fontSize: 12
}}
{...props} // <- props contains the `className` prop
/>
)
const ArticleText = props => (
<P
css={{
fontSize: 14,
fontFamily: 'Georgia, serif'
}}
{...props} // <- props contains the `className` prop
/>
)
<ArticleText style={{fontSize: 16px}}>
结果
.css-result {
- font-size: 12px;
- font-size: 14px;
+ font-size: 16px;
}
Style Components
启发于另一个CSS-in-JS库styled-components,能够样式化任何接收className的组件
静态样式
// String Style
const Button = styled.button`
color: turquoise;
`
// Object Style
const Button = styled.button({color: turquoise;})
// <button>This my button component.</button>
render(<Button>This my button component.</Button>)
动态样式
// 动态定义某个属性
const Button = styled.button`
color: ${props =>
props.primary ? 'hotpink' : 'turquoise'};
`
render(<Button primary>This my button component.</Button>)
// 动态定义Object Style
const H1 = styled.h1(
{
fontSize: 20
},
props => ({ color: props.color })
)
render(<H1 color="lightgreen">This is lightgreen.</H1>)
// 动态多个属性
const dynamicStyle = props =>
css`
color: ${props.color};
`
const Container = styled.div`
${dynamicStyle};
`
render(
<Container color="lightgreen">
This is lightgreen.
</Container>
)
修改标签
使用withComponent
生成新的自定义组件
const Section = styled.section`
background: #333;
color: #fff;
`
const Aside = Section.withComponent('aside')
render(
<div>
<Section>This is a section</Section>
<Aside>This is an aside</Aside>
</div>
)
将样式组件当作选择器使用
import styled from '@emotion/styled'
const Child = styled.div`
color: red;
`
const Parent = styled.div`
${Child} {
color: green;
}
`
render(
<div>
<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>
</div>
)
组件选择器也可以使用Object Style的写法
import styled from '@emotion/styled'
const Child = styled.div({
color: 'red'
})
const Parent = styled.div({
[Child]: {
color: 'green'
}
})
render(
<div>
<Parent>
<Child>green</Child>
</Parent>
<Child>red</Child>
</div>
)
依然可以使用&
/* @jsx jsx */
import { jsx } from '@emotion/react'
render(
<div
css={{
color: 'darkorchid',
'& .name': {
color: 'orange'
}
'& :hover': {
color: 'red'
}
}}
>
This is darkorchid.
<div className="name">This is orange</div>
</div>
)
scss怎么写,Emotion就能怎么写
/** @jsx jsx */
import { jsx, css } from '@emotion/react'
const paragraph = css`
color: turquoise;
a {
border-bottom: 1px solid currentColor;
cursor: pointer;
}
`
render(
// 可以自定义样式名,后加上自定义字符串,比如下面这行paragraph
<p css={paragraph}>
Some text.
<a>A link with a bottom border.</a>
</p>
)
样式组合
/** @jsx jsx */
import { jsx, css } from '@emotion/core'
const base = css`
color: hotpink;
`
render(
<div
css={css`
${base};
background-color: #eee;
`}
>
This is hotpink.
</div>
)
在传统css中,两个className组合的优先级由样式表中定义的先后决定。在应用时修改优先级需要使用!important
。而Emotion则可以根据应用时的顺序决定:
/** @jsx jsx */
import { css, jsx } from '@emotion/core'
const danger = css`
color: red;
`
const base = css`
color: blue;
`
render(
<div>
<div css={base}>This will be blue</div>
<div css={[danger, base]}>
This will be also be blue since the base styles
overwrite the danger styles.
</div>
<div css={[base, danger]}>This will be red</div>
</div>
附加props
/** @jsx jsx */
import { jsx, css } from '@emotion/core'
const PasswordInput = ({size, ...restProps}) => (
<input
type="password"
css={css`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
/* here we use the dynamically computed prop */
margin: ${props => props.size};
padding: ${props => props.size};
`}
size={props.size || "1em"}
{...restProps}
/>
)
render(
<div>
<PasswordInput placeholder="red" />
<PasswordInput placeholder="pink" css={pinkInput} />
</div>
);
媒体查询
/** @jsx jsx */
import { jsx } from '@emotion/react'
render(
<div
css={{
color: 'darkorchid',
'@media(min-width: 420px)': {
color: 'orange'
}
}}
>
This is orange on a big screen and darkorchid on a small
screen.
</div>
)
参考:
网友评论