CSS in JS

啥是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是以组件为最小颗粒度划分的,强制我们将HTMLCSSJS写在一起。在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 propStyled Componentscss函数

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>
)

参考:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容