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是會隨着各類新技術的出現而不斷變化的,不少技術每每都是源自於某位開發者的靈光一如今社區提出了某個思想,一些贊同的人可能就會嘗試給出具體的實現。因此當咱們哪天靈光一現時,千萬不要就只是想一想,勇敢地去與他人分享或者嘗試去實現,即便失敗了也能學到不少東西。

擴展閱讀

相關文章
相關標籤/搜索