CSS in JS的好与坏

是什么

CSS-in-JS是一种技术(technique),而不是一个具体的库实现(library)。简单来讲CSS-in-JS就是将应用的CSS样式写在JavaScript文件里面,而不是独立为一些.css.scss或者less之类的文件,这样你就能够在CSS中使用一些属于JS的诸如模块声明,变量定义,函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。值得一提的是,虽然CSS-in-JS不是一种很新的技术,但是它在国内普及度好像并非很高,它当初的出现是由于一些component-based的Web框架(例如React,Vue和Angular)的逐渐流行,使得开发者也想将组件的CSS样式也一块封装到组件中去以解决原生CSS写法的一系列问题。还有就是CSS-in-JS在React社区的热度是最高的,这是由于React自己不会管用户怎么去为组件定义样式的问题,而Vue和Angular都有属于框架本身的一套定义样式的方案。javascript

本文将经过分析CSS-in-JS这项技术带来的好处以及它存在的一些问题来帮助你们判断本身是否是要在项目中使用CSS-in-JS。css

不一样的实现

实现了CSS-in-JS的库有不少,据统计如今已经超过了61种。虽然每一个库解决的问题都差很少,但是它们的实现方法和语法却截然不同。从实现方法上区分大致分为两种:惟一CSS选择器和内联样式(Unique Selector VS Inline Styles)。接下来咱们会分别看一下对应于这两种实现方式的两个比较有表明性的实现:styled-componentsradiumhtml

Styled-components

Styled-components 应该是CSS-in-JS最热门的一个库了,到目前为止github的star数已经超过了27k。经过styled-components,你可使用ES6的标签模板字符串语法(Tagged Templates)为须要styled的Component定义一系列CSS属性,当该组件的JS代码被解析执行的时候,styled-components会动态生成一个CSS选择器,并把对应的CSS样式经过style标签的形式插入到head标签里面。动态生成的CSS选择器会有一小段哈希值来保证全局惟一性来避免样式发生冲突。前端

CSS-in-JS Playground是一个能够快速尝试不一样CSS-in-JS实现的网站,上面有一个简单的用styled-components实现表单的例子:java

从上面的例子能够看出,styled-components不须要你为须要设置样式的DOM节点设置一个样式名,使用完标签模板字符串定义后你会获得一个styled好的Component,直接在JSX中使用这个Component就能够了。接着让咱们打开DevTools查看一下生成的CSS:react

从上面DevTools能够看出styled的Component样式存在于style标签内,并且选择器名字是一串随机的哈希字符串,这样其实实现了局部CSS做用域的效果(scoping styles),各个组件的样式不会发生冲突。除了styled-components,采用惟一CSS选择器作法的实现还有:jssemotionglamorous等。git

Radium

Radium是由FormidableLabs建立的在github上有超过7.2k star的CSS-in-JS库。Radium和styled-components的最大区别是它生成的是标签内联样式(inline styles)。因为标签内联样式在处理诸如media query以及:hover:focus:active等和浏览器状态相关的样式的时候很是不方便,因此radium为这些样式封装了一些标准的接口以及抽象。github

再来看一下radium在CSS-in-JS Playground的例子:web

从上面的例子能够看出radium定义样式的语法和styled-components有很大的区别,它要求你使用style属性为DOM添加相应的样式。打开DevTools查看一下radium生成的CSS:npm

从DevTools上面inspect的结果能够看出,radium会直接在标签内生成内联样式。内联样式相比于CSS选择器的方法有如下的优势:

  • 自带局部样式做用域的效果,无需额外的操做
  • 内联样式的权重(specificity)是最高的,能够避免权重冲突的烦恼
  • 因为样式直接写在HTML中,十分方便开发者调试

其余区别

不一样的CSS-in-JS实现除了生成的CSS样式和编写语法有所区别外,它们实现的功能也不尽相同,除了一些最基本的诸如CSS局部做用域的功能,下面这些功能有的实现会包含而有的却不支持:

  • 自动生成浏览器引擎前缀 - built-in vendor prefix
  • 支持抽取独立的CSS样式表 - extract css file
  • 自带支持动画 - built-in support for animations
  • 伪类 - pseudo classes
  • 媒体查询 - media query
  • 其余

想了解更多关于不一样CSS-in-JS的对比,能够看一下Michele Bertoli整理的不一样实现的对比图

好处

看完了一些不一样的实现,你们应该对CSS-in-JS一些基本的概念和用法有了大概的理解,接着咱们能够来聊一下CSS-in-JS都有什么好处和坏处了。

局部样式 - Scoping Styles

CSS有一个被你们诟病的问题就是没有本地做用域,全部声明的样式都是全局的(global styles)。换句话来讲页面上任意元素只要匹配上某个选择器的规则,这个规则就会被应用上,并且规则和规则之间能够叠加做用(cascading)。SPA应用流行了以后这个问题变得更加突出了,由于对于SPA应用来讲全部页面的样式代码都会加载到同一个环境中,样式冲突的几率会大大加大。因为这个问题的存在,咱们在平常开发中会遇到如下这些问题:

  • 很难为选择器起名字。为了不和页面上其余元素的样式发生冲突,咱们在起选择器名的时候必定要深思熟虑,起的名字必定不能太普通。举个例子,假如你为页面上某个做为标题的DOM节点定义一个叫作.title的样式名,这个类名很大几率已经或者将会和页面上的其余选择器发生冲突,因此你不得不手动为这个类名添加一些前缀,例如.home-page-title来避免这个问题。
  • 团队多人合做困难。当多我的一块儿开发同一个项目的时候,特别是多个分支同时开发的时候,你们各自取的选择器名字可能有会冲突,但是在本地独立开发的时候这个问题几乎发现不了。当你们的代码合并到同一个分支的时候,一些样式的问题就会随之出现。

CSS-in-JS会提供自动局部CSS做用域的功能,你为组件定义的样式会被限制在这个组件,而不会对其余组件的样式产生影响。不一样的CSS-in-JS库实现局部做用域的方法可能有所不同,通常来讲它们会经过为组件的样式生成惟一的选择器来限制CSS样式的做用域。如下是一个简化了的CSS-in-JS库生成惟一选择器的示例代码:

const css = styleBlock => {
  const className = someHash(styleBlock);
  const styleEl = document.createElement('style');
  styleEl.textContent = ` .${className} { ${styleBlock} } `;
  document.head.appendChild(styleEl);
  return className;
};
const className = css(` color: red; padding: 20px; `); // 'c23j4'
复制代码

从上面的代码能够看出,CSS-in-JS的实现会根据定义的样式字符串生成一个惟一的CSS选择器,而后把对应的样式插入到页面头部的style标签中,styled-components使用的就是相似的方法。

避免无用的CSS样式堆积 - Dead Code Elimination

进行过大型Web项目开发的同窗应该都有经历过这个情景:在开发新的功能或者进行代码重构的时候,因为HTML代码和CSS样式之间没有显式的一一对应关系,咱们很难辨认出项目中哪些CSS样式代码是有用的哪些是无用的,这就致使了咱们不敢轻易删除代码中多是无用的样式。这样随着时间的推移,项目中的CSS样式只会增长而不会减小(append-only stylesheets)。无用的样式代码堆积会致使如下这些问题:

  • 项目变得愈来愈重量级,加载到浏览器的CSS样式会愈来愈多,会形成必定的性能影响。
  • 开发者发现他们很难理解项目中的样式代码,甚至可能被大量的样式代码吓到,这就致使了开发效率的下降以及一些奇奇怪怪的样式问题的出现。

CSS-in-JS的思路就能够很好地解决这个问题。咱们先来看一段styled-components的做者Max Stoiber说过的话:

“For three years, I have styled my web apps without any .css files. Instead, I have written all the CSS in JavaScript. ... I can add, change and delete CSS without any unexpected consequences. My changes to the styling of a component will not affect anything else. If I delete a component, I delete its CSS too. No more append-only stylesheets!” – Max Stoiber

Max Stoiber大致就是说因为CSS-in-JS会把样式和组件绑定在一块儿,当这个组件要被删除掉的时候,直接把这些代码删除掉就行了,不用担忧删掉的样式代码会对项目的其余组件样式产生影响。并且因为CSS是写在JavaScript里面的,咱们还能够利用JS显式的变量定义,模块引用等语言特性来追踪样式的使用状况,这大大方便了咱们对样式代码的更改或者重构。

Critical CSS

浏览器在将咱们的页面呈现给用户以前必定要先完成页面引用到的CSS文件的下载和解析(download and parse),因此link标签连接的CSS资源是渲染阻塞的(render-blocking)。若是CSS文件很是大或者网络的情况不好,渲染阻塞的CSS会严重影响用户体验。针对这个问题,社区有一种优化方案就是将一些重要的CSS代码(Critical CSS)直接放在头部的style标签内,其他的CSS代码再进行异步加载,这样浏览器在解析完HTML后就能够直接渲染页面了。具体作法相似于如下代码:

<html>
  <head>
    <style> /* critical CSS */ </style>
    <script>asyncLoadCSS("non-critical.css")</script>
  </head>
  <body>
    ...body goes here
  </body>
</html>
复制代码

那么如何定义Critical CSS呢?放在head标签内的CSS固然是越少越好,由于太多的内容会加大html的体积,因此咱们通常把用户须要在首屏看到的(above the fold)页面要用到的最少CSS提取为Critical CSS。如下是示意图:

上图中above the fold的CSS就是Critical CSS,由于它们须要当即展现在用户面前。因为页面在不一样的设备上展现的效果不一样,对应着的Critical CSS内容也会有所差异,所以Critical CSS的提取是一个十分复杂的过程,虽然社区有不少对应的工具但是效果都差强人意。CSS-in-JS却能够很好地支持Critical CSS的生成。在CSS-in-JS中,因为CSS是和组件绑定在一块儿的,只有当组件挂载到页面上的时候,它们的CSS样式才会被插入到页面的style标签内,因此很容易就能够知道哪些CSS样式须要在首屏渲染的时候发送给客户端,再配合打包工具的Code Splitting功能,能够将加载到页面的代码最小化,从而达到Critical CSS的效果。换句话来讲,CSS-in-JS经过增长一点加载的JS体积就能够避免另外发一次请求来获取其它的CSS文件。并且一些CSS-in-JS的实现(例如styled-components)对Critical CSS是自动支持的

基于状态的样式定义 - State-based styling

CSS-in-JS最吸引个人地方就是它能够根据组件的状态动态地生成样式。对于SPA应用来讲,特别是一些交互复杂的页面,页面的样式一般要根据组件的状态变化而发生变化。若是不使用CSS-in-JS,处理这些逻辑复杂的状况会比较麻烦。举个例子,假如你如今页面有一个圆点,它根据不一样的状态展现不一样的颜色,running的时候是绿色,stop的时候是红色,ready的时候是黄色。若是使用的是CSS modules方案你可能会写下面的代码:

style.css文件

.circle {
  ... circle base styles
}

.healthy {
  composes: circle;
  background-color: green;
}

.stop {
  composes: circle;
  background-color: red;
}

.ready {
  composes: circle;
  background-color: 
}
复制代码

index.js文件

import React from 'react'
import styles from './style.css'

const styleLookup = {
  healthy: styles.healthy,
  stop: styles.stop,
  ready: styles.ready
}

export default ({ status }) => (
  <div className={styleLookup[status]} /> ) 复制代码

在style.css中咱们使用了CSS modules的继承写法来在不一样状态的CSS类中共用circle基类的样式,代码看起来十分冗余和繁琐。因为CSS-in-JS会直接将CSS样式写在JS文件里面,因此样式复用以及逻辑判断都十分方便,若是上面的例子用styled-components来写是这样的:

import styled from 'styled-components'

const circleColorLookup = {
  healthy: 'green',
  stop: 'red',
  ready: 'yellow'
}

export default styled.div` ... circle base styles background-color: ${({ status }) => circleColorLookup[status]}; `
复制代码

对比起来,styled-components的逻辑更加清晰和简洁,若是后面须要增长一个状态,只须要为circleColorLookup添加一个键值对就好,而CSS modules的写法须要同时改动style.css和index.js文件,代码很差维护和扩展。

封装得更好的组件库

你们在平常开发的过程当中可能会封装一些组件在不一样的项目中使用,若是你的组件的样式使用的CSS预处理方案和另一个项目的预处理方案不同,例如组件使用的是less,项目使用的是css modules,组件复用会变得很麻烦。但是若是CSS是写在JS里面的,项目想要使用封装的组件库只须要进行简单的npm install就能够了,很是方便。

坏处

任何事物都有好的地方和坏的地方,只有对好处和坏处都了解清楚咱们才能更好地作出判断。接着咱们就来讲一下CSS-in-JS很差的地方吧。

陡峭的学习曲线 - Steep learning curve

这其实能够从两方面来讲明。首先CSS-in-JS是针对component-based的框架的,这就意味着要学习CSS-in-JS你必须得学习:component-based框架(例如React),JavaScript和CSS这三样技能。其次,即便你已经会用React,JavaScript和CSS来构建SPA应用,你还要学习某个CSS-in-JS实现(例如styled-components),以及学习一种全新的基于组件定义样式的思考问题方式。咱们团队在刚开始使用styled-components的时候,适应了好一段时间才学会如何用好这个库。由于学习成本比较高,在项目中引入CSS-in-JS可能会下降大家的开发效率。

运行时消耗 - Runtime cost

因为大多数的CSS-in-JS的库都是在动态生成CSS的。这会有两方面的影响。首先你发送到客户端的代码会包括使用到的CSS-in-JS运行时(runtime)代码,这些代码通常都不是很小,例如styled-components的runtime大小是12.42kB min + gzip,若是你但愿你首屏加载的代码很小,你得考虑这个问题。其次大多数CSS-in-JS实现都是在客户端动态生成CSS的,这就意味着会有必定的性能代价。不一样的CSS-in-JS实现因为具体的实现细节不同,因此它们的性能也会有很大的区别,你能够经过这个工具来查看和衡量各个实现的性能差别。

代码可读性差 - Unreadable class names

大多数CSS-in-JS实现会经过生成惟一的CSS选择器来达到CSS局部做用域的效果。这些自动生成的选择器会大大下降代码的可读性,给开发人员debug形成必定的影响。

没有统一的业界标准 - No interoperability

因为CSS-in-JS只是一种技术思路而没有一个社区统一遵循的标准和规范,因此不一样实现的语法和功能可能有很大的差别。这就意味着你不能从一个实现快速地切换到另一个实现。举个例子,假如你先在项目使用radium,但是随着项目规模的变大,你发现radium可能不适合你如今的业务,更好的解决方案应该是styled-components。但是因为写法差别巨大,这时候你要对代码进行脱胎换骨的改动才能将代码迁移到styled-components。不过使人欣慰的是,如今已经有人在制定相关的标准了,有兴趣的同窗能够看一下Interoperable Style Transfer Format

我的思考与总结

CSS-in-JS有好处也有坏处,咱们必定要根据本身的实际状况进行衡量和取舍来肯定是否是要在本身的项目中使用它。永远不要为了使用一个技术而用一个技术。例如在下面几种状况下你就不须要它:

  • 你是前端开发的初学者: 因为CSS-in-JS的学习坡度很陡,刚开始学习Web开发的同窗不必学习,可能会有挫败感。
  • 你只想制做一些功能简单的静态页面:逻辑交互不复杂的网站没有必要使用CSS-in-JS。
  • 你很注重样式名的可读性以及调试体验: CSS-in-JS动态生成的选择器很影响代码的可读性,可能会下降你的调试效率。

相反若是你的应用交互逻辑复杂的话,CSS-in-JS可能会给你带来很大的开发便利,没有使用过的人十分值得一试。

参考文献

我的技术动态

文章首发于个人我的博客

欢迎关注公众号进击的大葱一块儿学习成长