CSS Solutions

Background

随着前端项目日益复杂,如何构建可维护、可复用、可配置的CSS代码,成了每一个前端工程师都须要思考的问题。问题的本质:CSS最初是为了描述网页样式而被提出的,并不具有编程语言的特性,因而在前端走向工程化的道路上,CSS暴露出一些问题拖了后腿:javascript

  • 全局做用域,没有模块的概念,在复杂的系统及多人协做时容易产生样式冲突,难以维护;
  • 缺少变量、函数等编程语言的特性,不利于经常使用属性、样式的抽象及复用;
  • 各浏览器及其不一样版本对CSS语法支持程度,支持方式不一致,具体表如今是否支持某些功能,同一属性在不一样浏览器中属性名不一样;
  • 在根据不一样的状态渲染样式时(这里称之为State Styling)须要定义多个class,可读性差;

.....css

针对这些问题,爱折腾的前端程序员们探索出了各类技术及解决方案。本文简单介绍经常使用的CSS技术,而后分享两种常见的CSS工程化解决方案,但愿能够帮助那些和我同样对这些概念比较模糊的同窗对此有个系统的认知。html

BEM

BEM(Block__Element--Modifier),是一种CSS命名规范,看个例子:前端

<body class="scenery">
  <section class="scenery__sky scenery__sky--dusk"></section>
  <section class="scenery__ground"></section>
  <section class="scenery__people"></section>
</body>

scenery对应Block,sky、ground、people对应Element,dusk对应modifier。不难看出BEM的本质实际上是把HTML元素的层级关系及元素自己的状态组合起来,造成元素独有的className,旨在解决CSS全局做用域引起的样式冲突的问题。vue

但BEM毕竟是一种规范,不是框架。Block,Element,Modifier的命名都须要开发者思考,引用某位大牛说过的话:“命名和缓存失效是计算机领域最难的两件事情”,可见BEM会增长开发者的工做量。另外,在HTML结构复杂时,BEM形式的className会很长,可读性不好,且增长了代码文件的体积。java

CSS Preprocessor

CSS Preprocessor(CSS预处理器)是一类旨在加强CSS语言功能,从而帮助开发者写出可复用,可维护的样式代码的CSS框架。主流的CSS Preprocessor有:Sass,Less,Stylus,都是以DSL(Sass: .scss/.sass, Less: .less, Stylus: .styl)的形式为开发者提供更强大的语言特性,再编译为浏览器能看懂的.css文件。python

Sass

Sass(Syntactically Awesome Style sheets)号称世界上最成熟,功能最强大的CSS Preprocessor。无可厚非,Sass有着庞大的用户群体,活跃的社区和详细的文档是它的优点之一。最初基于Ruby,后来衍生了libSass,DartSass,使得Sass编译速度更快。Sass功能强大,为CSS扩充了变量,Mixin,继承,数学运算等编程语言功能,优化了CSS自己的语法,好比适当地使用嵌套可使样式结构更清晰;提供不额外产生http请求的import,还提供了一系列功能强大的内置函数。react

Less

Less(Leaner Style Sheets)基于JS,它的设计理念是尽量相似CSS的语法以及函数式编程。Less甚至是向后兼容CSS的,这意味着在迁移老项目到Less时能够直接把CSS代码复制到.less文件中,固然仍是要利用Less提供的功能作出改动,但这无疑减小了工做量。因此Less上手快,但相对地Less的功能较弱,好比不提供相似Sass中的@function功能,Mixin在须要返回值的情景下并不适用;又好比Less的extend功能其实是把被Extend对象的样式复制到目标对象中,而不是像Sass那样为多个class定义同一个样式,致使产生冗余代码。如:jquery

/* Less Code */
.header {
  padding: 2px;
  font-weight: bold;
}

h1 {
  .header; /* Extends .header styles */
  font-size: 42px;
}
h2 {
  .header; /* Extends .header styles */
  font-size: 36px;
}

编译结果:webpack

.header{
  padding: 2px;
  font-weight: bold;
}
h1 {
  padding: 2px;
  font-weight: bold;
  font-size: 42px;
}
h2 {
  padding: 2px;
  font-weight: bold;
  font-size: 36px;
}

Stylus

Stylus基于NodeJS, 在适当贴近CSS语法的同时提供更增强大的功能,看上去像是Sass和Less的结合体。Stylus的语法是python风格,提倡简洁,因此推荐不写大括号,固然,这是可选的。看一段Stylus的代码:

border-radius()
  -webkit-border-radius: arguments
  -moz-border-radius: arguments
  border-radius: arguments

body
  font: 12px Helvetica, Arial, sans-serif

a.button {
  border-radius: 5px
}

能够看到Stylus在定义函数,变量或者mixin的时候甚至不须要像sass那样加上$,@等符号,语法十分简洁。

总得来讲,Sass有详细的文档,成熟的社区以及相对强大的功能和编译速度;Less向后兼容CSS,学习曲线平缓,旧项目迁移难度低,可是功能没有Sass和Stylus强大;Stylus功能最强大,语法最简洁,但文档可读性较差。

PostCSS

另外再说一下PostCSS,PostCSS本质上是一个平台,平台自己并无对CSS作任何加强,只是将CSS解析成AST提供给插件,全部须要的功能均可以经过插件灵活地订制(babel也是这种思想),好比Autoprefixer,相似于babel-preset-env的PostCSS Preset Env,CSS Modules,stylelint等等,甚至能够本身写插件。

因此用PostCSS替代以上三者也是能够的,即须要哪些语法功能就去找到对应的PostCSS插件,如:

  • postcss-partial-import
  • postcss-advanced-variables
  • postcss-nested

...

CSS Modules

CSS Modules是一种CSS模块化规范:经过为CSS Rule生成独一无二的class name,使得每个CSS Module下的CSS Rule默认都是locally,固然也能够声明global的rule。CSS Module export出local class name与global class name的map:

/* style.css */
.className {
  color: green;
}
import styles from "./style.css";
// import { className } from "./style.css";

element.innerHTML = '<div class="' + styles.className + '">';

另外还支持同一module或不一样module中的CSS Rule之间的composite,提高了样式可复用性。

经常使用的实现有webpack的css-loader,以及针对React优化的HOC版本react-css-modules。

// css-loader
{
  test: /\.css$/,
  loader: 'style!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 
}
// react-css-modules

import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './table.css';

class Table extends React.Component {
    render () {
        return <div styleName='table'>
            <div styleName='row'>
                <div styleName='cell'>A0</div>
                <div styleName='cell'>B0</div>
            </div>
        </div>;
    }
}

export default CSSModules(Table, styles);

CSS-IN-JS

CSS-IN-JS也是一种CSS工程化解决方案,核心思想在于彻底由JS托管CSS,借助JS的模块,变量,函数等概念来提高CSS代码的可维护性,可复用性。经常使用的实现有:styled-components,glamorou,emotion等。

// styled-components

const Container = styled.div`
  text-align: center;
  color: ${props => props.color};
  
`
render(
  <Container>
    Test Container
  </Container>
);
// emotion

import { css, jsx } from '@emotion/core'

const color = 'white'

render(
  <div
    css={css`
      padding: 32px;
      background-color: hotpink;
      font-size: 24px;
      border-radius: 4px;
      &:hover {
        color: ${color};
      }
    `}
  >
    Hover to change color.
  </div>
)

能够看到styled-components和emotion都使用了ES6的Tagged Templates语法分别调用styled、css函数,拿styled函数举例,上述代码会被编译成相似下面的代码:

const Container = styled(
  'div',
  ['css-Container-duiy4a'], // generated class names
  [props => props.color], // dynamic values
  function createStyledRules (x0) {
    return [`.css-Container-duiy4a { text-align: center; color:${x0} }`]
  }
)

render时,styled将执行dynamic values中的函数,赋予其最新的props。而后调用createStyledRules并传入dynamic values的结果,最后把createStyledRules生成的样式插入stylesheet中,再将generated class names赋给div的className属性。

CSS-IN-JS库的trade-off在于runtime性能,由于可能要在runtime作解析模板字符串,根据props动态生成样式,调用hash算法生成独特的css classname等操做。不一样的库性能差别就体如今对这些操做的优化措施,以及尽量地把这些操做提早到build time作。

Solutions

实际项目中的CSS解决方案是“因地制宜”的,由于怎么处理CSS,是由实际需求和项目中其余技术决定的。好比React项目会用到react-css-modules;结合React HOC的形式使代码更简洁;Vue项目会用到vue-loader、vue-style-loader;选用不一样的CSS-IN-JS库,如styled-components,emotion等。

尽管存在差别,但CSS解决方案大体能够分为两种:传统的CSS,CSS-IN-JS。

Traditional

考虑到可维护性和可复用性,咱们须要引入一种CSS Preprocessor,具体的选择能够参考上文对Sass、Less、Stylus的概述,这里以Sass为例。而后利用sass的partial功能合理地组织样式代码目录结构,好比:

sass/ 
| 
|– base/ 
|   |– _reset.scss       # Reset/normalize 
|   |– _typography.scss  # Typography rules 
|   ...                  # Etc… 
| 
|– components/ 
|   |– _buttons.scss     # Buttons 
|   |– _carousel.scss    # Carousel 
|   |– _cover.scss       # Cover 
|   |– _dropdown.scss    # Dropdown 
|   |– _navigation.scss  # Navigation 
|   ...                  # Etc… 
| 
|– helpers/ 
|   |– _variables.scss   # Sass Variables 
|   |– _functions.scss   # Sass Functions 
|   |– _mixins.scss      # Sass Mixins 
|   |– _helpers.scss     # Class & placeholders helpers 
|   ...                  # Etc… 
| 
|– layout/ 
|   |– _grid.scss        # Grid system 
|   |– _header.scss      # Header 
|   |– _footer.scss      # Footer 
|   |– _sidebar.scss     # Sidebar 
|   |– _forms.scss       # Forms 
|   ...                  # Etc… 
| 
|– pages/ 
|   |– _home.scss        # Home specific styles 
|   |– _contact.scss     # Contact specific styles 
|   ...                  # Etc… 
| 
|– themes/ 
|   |– _theme.scss       # Default theme 
|   |– _admin.scss       # Admin theme 
|   ...                  # Etc… 
| 
|– vendors/ 
|   |– _bootstrap.scss   # Bootstrap 
|   |– _jquery-ui.scss   # jQuery UI 
|   ...                  # Etc… 
| 
| 
`– main.scss             # primary Sass file

并在main.scss中import这些partial。

此外再考虑样式代码的build过程,结合webpack使用的话须要使用sass-loader,css-loader,style-loader等,如下配置仅供参考:

module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        include: path.resolve(__dirname, 'src'),
        use: [
          'style-loader',
          'css-loader',
          'sass-loader'
        ]
      }
      // ...other rules
    ]
  }

考虑到样式代码的code split及缓存策略,在生产模式下通常会把style-loader替换成MiniCssExtractPlugin,这样能够将css代码单独build成文件,而不是在runtime时以<style></style>的形式insert到document中去。

module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        include: path.resolve(__dirname, 'src'),
        use: [
          process.env.NODE_ENV !== 'production'
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader'
        ]
      }
      // ...other rules
    ]
  }

最后,可能你的项目须要一些额外的功能,好比使用了一些浏览器兼容程度较差的CSS语法须要转译成兼容的语法,这种状况下你还须要引入postcss及相关插件,如:

module: {
    rules: [
      {
        test: /\.s[ac]ss$/i,
        include: path.resolve(__dirname, 'src'),
        use: [
          process.env.NODE_ENV !== 'production'
            ? 'style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              ident: 'postcss',
              sourceMap: true,
              plugins: [
                postcssPresetEnv({
                  browsers: BROWSERSLIST,
                }),
              ],
            }
          },
          'sass-loader'
        ]
      }
      // ...other rules
    ]
  }

CSS-IN-JS

css-in-js方案最重要的莫过于选择一个合适的库。styled-components、emotion、glamorou、JSS......,根据自身项目的业务场景,选用的MVVM框架种类(React or Vue or Angular),开发团队水平等因素选择最适合团队的css-in-js库,既能提高开发效率又能减少迁移风险。

Package As Object As Tagged Templates SSR RN Support Agnostic Dynamic Babel plugins Bindings
emotion react-emotion, preact-emotion
fela react-fela native-fela preact-fela inferno-fela
jss react-jss styled-jss
rockey rockey-react
styled-components
aphrodite
csx
glam
glamor
glamorous
styletron styletron-react
aesthetic
j2c

目前css-in-js仍是有必定局限的:对于React应用较为友好,虽然不少库有Agnostic的版本,另外还有针对vue的styled-components-vue,emotion-vue等,但在功能和写法上都不如结合React使用。

另外,组织好目录结构、抽象可复用代码对CSS-IN-JS一样适用,可参考上文Traditional方案中的目录结构和粒度。CSS-IN-JS在这方面能够作得更好,由于复用的粒度能够上升到组件级别。

不管对传统方案仍是CSS-IN-JS方案,均可以经过服务端渲染提取critical css以提高首屏渲染速度。大体思路是根据用户访问的路由加载对应的页面,经过React的context api获取页面对应的样式并以style标签的形式插入到html文档的head中去,系统内跳转时交给client端控制,具体能够参考isomorphic-style-loader的实现。

一些感想

CSS Solutions是会随着各类新技术的出现而不断变化的,不少技术每每都是源自于某位开发者的灵光一如今社区提出了某个思想,一些赞同的人可能就会尝试给出具体的实现。因此当咱们哪天灵光一现时,千万不要就只是想一想,勇敢地去与他人分享或者尝试去实现,即便失败了也能学到不少东西。

扩展阅读

相关文章
相关标签/搜索