CSS Modules入門教程

来源:https://www.cnblogs.com/rynxiao/archive/2018/08/27/9538058.html
-Advertisement-
Play Games

為什麼引入CSS Modules 或者可以這麼說,CSS Modules為我們解決了什麼痛點。針對以往我寫網頁樣式的經驗,具體來說可以歸納為以下幾點: 全局樣式衝突 過程是這樣的:你現在有兩個模塊,分別為A、B,你可能會單獨針對這兩個模塊編寫自己的樣式,例如a.css、b.css,看一下代碼 下麵是 ...


為什麼引入CSS Modules

或者可以這麼說,CSS Modules為我們解決了什麼痛點。針對以往我寫網頁樣式的經驗,具體來說可以歸納為以下幾點:

全局樣式衝突

過程是這樣的:你現在有兩個模塊,分別為A、B,你可能會單獨針對這兩個模塊編寫自己的樣式,例如a.css、b.css,看一下代碼

// A.js
import './a.css'
const html = '<h1 class="text">module A</h1>'

// B.js
import './b.css'
const html = '<h1 class="text">module B</h1>'

下麵是樣式:

/* a.css */
.text {
    color: red;
}

/* b.css */
.text {
    color: blue;
}

導入到入口APP中

// App.js
import A from './A.js'
import B from './B.js'

element.innerTHML = 'xxx'

由於樣式是統一載入到入口中,因此實際上的樣式合在一起(這裡暫定為mix.css)顯示順序為:

/* mix.css */

/* a.css */
.text {
    color: red;
}

/* b.css */
.text {
    color: blue;
}

根據CSS的Layout規則,因此後面的樣式會覆蓋掉前面的樣式聲明,最終有效的就是text的顏色為blue的那條規則,這就是全局樣式覆蓋,同理,這在js中也同樣存在,因此就引入了模塊化,在js中可以用立即執行函數表達式來隔離出不同的模塊

var moduleA = (function(document, undefined){
    // your module code
})(document)

var moduleB = (function(document, undefined){
    // your module code
})(document)

而在css中要想引入模塊化,那麼就只能通過namespace來實現,而這個又會帶來新的問題,這個下麵會講到

嵌套層次過深的選擇器

為瞭解決全局樣式的衝突問題,就不得不引入一些特地命名namespace來區分scope,但是往往有些namespace命名得不夠清晰,就會造成要想下一個樣式不會覆蓋,就要再加一個新的namespace來進行區分,最終可能一個元素最終的顯示樣式類似如以下:

.widget .table .row .cell .content .header .title {
  padding: 10px 20px;
  font-weight: bold;
  font-size: 2rem;
}

在上一個元素的顯示上使用了7個選擇器,總結起來會有以下問題:

  • 根據CSS選擇器的解析規則可以知道,層級越深,比較的次數也就越多。當然在更多的情況下,可能嵌套的層次還會更深,另外,這裡單單用了類選擇器,而採用類選擇器的時候,可能對整個網頁的渲染影響更重。
  • 增加了不必要的位元組開銷
  • 語義混亂,當文檔中出現過多的contenttitle以及item這些通用的類名時,你可能要花上老半天才知道它們到底是用在哪個元素上
  • 可擴展性不好,約束越多,擴展性越差

【註】CSS的渲染規則可以參看這篇文章探究 CSS 解析原理

會帶來代碼的冗餘

由於CSS不能使用類似於js的模塊化的功能,可能你在一個css文件中寫了一個公共的樣式類,而你在另外一個css也需要這樣一個樣式,這時候,你可能會多寫一次,類似於這樣的

/* a.css */

.modal {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1;
    background-color: rgba(0, 0, 0, 0.7);
}
.text {
    color: red;
}

/* b.css */
.modal {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1;
    background-color: rgba(0, 0, 0, 0.7);
}
.text {
    color: blue;
}

那麼在合併成app.css的時候,就會被編寫兩遍,雖然樣式不會被影響,但是這樣實際上也是一種位元組浪費,當然,上述的這種情況完全是可以通過公用全局樣式來達到目的,但是,這種代碼重覆通常是在不知情的情況下發生的。

一些解決方案

針對上述的一些問題,也有一些解決方案,具體如下:

CSS預處理器(Sass/Less等)

Sass,Less的用法這裡不再贅述,如果不清楚,可以自己查閱相關資料去瞭解一下。

CSS預處理器最大的好處就是可以支持模塊引入,用js的方式來編寫CSS,解決了部分scope混亂以及代碼冗餘的問題,但是也不能完全避免。同時,也沒有解決全局樣式的衝突問題

一個SASS的的文件是這樣的:

/* app.sass */

@import './reset'
@import './color'
@import './font'

可以實際上編譯之後,終究還是一個文件,因此不可避免的會出現衝突樣式

BEM(Block Element Modifier)

There are only two hard problems in Computer Science: cache invalidation and naming things — Phil Karlton

BEM就是為瞭解決命名衝突以及更好的語義化而生的。

BEM名詞解釋

  • Block:邏輯和頁面功能都獨立的頁面組件,是一個可復用單元,特點如下:

    • 可以隨意嵌套組合
    • 可以放在任意頁面的任何位置,不影響功能和外觀
    • 可復用,界面可以有任意多個相同Block的實例
  • Element:Block的組成部分,依賴Block存在(出了Block就不能用)
  • [可選]定義Block和Element的外觀及行為,就像HTML屬性一樣,能讓同一種Block看起來不一樣

命名規則

Block作為最小的可復用單元,任意嵌套不會影響功能和外觀,命名可以為headermenu等等

<style>
    .header { color: #042; }
</style>

<div class="header">...</div>

Element依附Block存在,沒有單獨的含義,命名上語義儘量接近於Block,比如titleitem之類

<style>
    .header { color: #042; }
    .header__title { color: #042; }
</style>

<div class="header">
    <h1 class="header__title">Header</h1>
</div>

Modifier是一個元素的狀態顯示,例如activecurrentselected

<style>
    .header--color-black { color: #000; }
    .header__title--color-red { color: #f00; }
</style>

<div class="header header--color-black">
    <h1 class="header__title">
        <span class="header__title--color-red">Header</span>
    </h1>
</div>

【說明】

  • Block完全獨立,可以嵌套,一個header是一個Block,header下的搜索框也可以是一個Block
  • 不可能出現Block__Element-father__Element-son_Modifer這種類名的寫法,BEM只有三級
  • Modifier可以加在Block和Element上面
  • Modifier作為一個額外的類名載入Block和Element上面,只是為了改變狀態,需要保留原本的class

一個完整的示例

<form class="form form--theme-xmas form--simple">
  <input class="form__input" type="text" />
  <input
    class="form__submit form__submit--disabled"
    type="submit" />
</form>
.form { }
.form--theme-xmas { }
.form--simple { }
.form__input { }
.form__submit { }
.form__submit--disabled { }

參考鏈接:

BEM解決了模塊復用、全局命名衝突等問題,配合預處理CSS使用時,也能得到一定程度的擴展,但是它依然有它的問題:

  • 對於嵌套過深的層次在命名上會給需要語義化體現的元素造成很大的困難
  • 對於多人協作上,需要統一命名規範,這同樣也會造成額外的effort

CSS Modules

說了這麼多,終於要到正文了

什麼是CSS Modules

根據CSS Modules的repo上的話來說是這樣的:

CSS files in which all class names and animation names are scoped locally by default.

所以CSS Modules並不是一個正式的聲明或者是瀏覽器的一個實現,而是通過構建工具(webpack or Browserify)來使所有的class達到scope的一個過程。

CSS Modules 解決了什麼問題

  • 全局命名衝突,因為CSS Modules只關心組件本身,只要保證組件本身命名不衝突,就不會有這樣的問題,一個組件被編譯之後的類名可能是這樣的:
/* App.css */
.text {
    color: red;
}

/* 編譯之後可能是這樣的 */
.App__text___3lRY_ {
    color: red;
}

命名唯一,因此保證了全局不會衝突。

  • 模塊化

可以使用composes來引入自身模塊中的樣式以及另一個模塊的樣式:

.serif-font {
  font-family: Georgia, serif;
}

.display {
  composes: serif-font;
  font-size: 30px;
  line-height: 35px;
}

應用到元素上可以這樣使用:

import type from "./type.css";

element.innerHTML = 
  `<h1 class="${type.display}">
    This is a heading
  </h1>`;

之後編譯出來的模板可能是這樣的:

<h1 class="Type__display__0980340 Type__serif__404840">
  Heading title
</h1>

從另一個模塊中引入,可以這樣寫:

.element {
  composes: dark-red from "./colors.css";
  font-size: 30px;
  line-height: 1.2;
}
  • 解決嵌套層次過深的問題

因為CSS Modules只關註與組件本身,組件本身基本都可以使用扁平的類名來寫,類似於這樣的:

.root {
  composes: box from "shared/styles/layout.css";
  border-style: dotted;
  border-color: green;
}

.text {
  composes: heading from "shared/styles/typography.css";
  font-weight: 200;
  color: green;
}

CSS Modules 怎麼用

CSS Modules不局限於你使用哪個前端庫,無論是React、Vue還是Angular,只要你能使用構建工具進行編譯打包就可以使用。

下麵我使用webpack為例,一步一步引入CSS Modules.

構建最初始的應用

.
├── build
│   └── bundle.js
├── index.html
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── index.js
│   └── styles
└── webpack.config.js

index.js作為程式入口,styles文件夾存放樣式文件,配合webpack.config.js作為webpack配置文件。

// index.js
var html = `<div class="header">
    <h2 class="title">CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

樣式文件:

/* global.css */
* {
    margin: 0;
    padding: 0;
}

.container {
    padding: 20px;
}

/* index.css */
.header {
    font-size: 32px;
}

.title {
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}

模板文件:

<!-- index.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>css modules</title>
</head>
<body>
    <div id="container" class="container"></div>
    <script src="./build/bundle.js"></script>
</body>
</html>

全局安裝依賴,配置執行腳本:

npm install webpack webpack-cli --save-dev

package.json

"scripts": {
    "build": "npx webpack && open index.html"
}

在控制台執行npm run build, 得到的結果為:

> [email protected] build /Users/yhhu/Documents/coding/css-modules-demo
> npx webpack && open index.html

Hash: 5810d2ecd760c08cc078
Version: webpack 4.17.1
Time: 78ms
Built at: 2018-08-26 15:09:31
    Asset      Size  Chunks             Chunk Names
bundle.js  3.97 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/index.js] 196 bytes {main} [built]

加入樣式以及loaders

package.json中加入能夠處理css的loader

  module: {
    rules: [
      {
        test: /\.js/,
        loader: 'babel-loader',
        include: __dirname + '/src',
        exclude: __dirname + '/src/styles'
        },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            options: {         
            }
          }
        ]
      }
    ]
  }

index.js中引入兩個CSS文件

// index.js
import './styles/global.css'
import './styles/index.css'

const html = `<div class="header">
    <h2 class="title">CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

編譯之後的執行結果為:
build

在瀏覽器中顯示為:
css-loader

提取公有樣式

可以看到打包之後的build目錄下只有一個bundle.js,我們現在要把樣式文件提取出來

./build/
└── bundle.js
  • 安裝依賴
npm install --save-dev mini-css-extract-plugin
  • 修改webpack.config.js
var MiniCssExtractPlugin = require("mini-css-extract-plugin");

modules: {
    rules: [
        // {
        //   test: /\.css$/,
        //   use: [
        //      { loader: "style-loader" },
        //     {
        //      loader: "css-loader",
        //      options: {
        
        //      }
        //     }
        //   ]
        // },
        {
            test: /\.css$/,
            use: [
              {
                loader: MiniCssExtractPlugin.loader,
                options: {
                  publicPath: './build/styles'
                }
              },
              { 
                loader: "css-loader",
                options: {
                    
                }
              }
            ]
        }        
    ]
},
plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
],
  • 在模板中引入樣式文件
<!-- index.html -->

<!DOCTYPE html>
<head>
    <link rel="stylesheet" href="./build/main.css">
</head>
<body>
    <div id="container" class="container"></div>
    <script src="./build/bundle.js"></script>
</body>
  • 編譯打包
    extract

可以看到有main.css生成

開啟css modules功能

預設在css-loader中是不開啟css modules功能的,要開啟可以設置modules: true即可,更多可以參看官方css-loader使用方法修改webpack.config.js,如下:

{
    test: /\.css$/,
    use: [
      {
        loader: MiniCssExtractPlugin.loader,
        options: {
          publicPath: './build/styles'
        }
      },
      { 
        loader: "css-loader",
        options: {
            modules: true
        }
      }
    ]
}        

修改index.js文件中的引用方式:

import './styles/global.css'
import Index from './styles/index.css'

const html = `<div class=${Index.header}>
    <h2 class=${Index.title}>CSS Modules</h2>
</div>`

document.getElementById('container').innerHTML = html;

可以看到,之前都是直接import一個css文件,而現在改成了導出一個對象的形式,我們可以把Index對象列印出來,看看具體是些什麼東西:

object

直接對應我們引用的方式,然後我們再看看生成出來的main.css中具體有哪些東西:

* {
    margin: 0;
    padding: 0;
}

._2BQ9qrIFipNbLIGEytIz5Q {
    padding: 20px;
}
._3Ukt9LHwDhphmidalfey-S {
    font-size: 32px;
}

._3XpLkKvmw0hNfJyl8yU3i4 {
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}

合成一個文件之後,所有的類名都經過了哈希轉換,因此確保了類名的唯一性,我們再看看瀏覽器中inspector中的樣式應用,如下:

no-transform

事實上,container樣式我們是不需要轉換的,因為我是把它固定寫死在了容器上,那我們應該怎麼做呢?

全局作用域

要想一個類名不需要被裝換,那麼可以使用:global(className)來進行包裝,這樣的類不會被轉換,會被原樣輸出,下麵我們修改global.css

/* global.css */
* {
    margin: 0;
    padding: 0;
}

:global(.container) {
    padding: 20px;
}

我們再來看看main.css

global

就可以發現.container類沒有被轉換

定義哈希類名

CSS Modules預設是以[hash:base64]來進行類名轉換的,可辨識度不高,因此我們需要自定義

開啟自定義,可以使用一個配置參數localIdentName,具體配置如下:

{ 
  loader: "css-loader",
  options: {
    modules: true,
    localIdentName: '[path][name]__[local]--[hash:base64:5]'
  }
}

localIdentName

類名組合

如果我們實現類似於Sass的繼承功能,我們需要怎麼做呢?CSS Modules中提供了composes關鍵字讓我們來繼承另外一個類,修改index.css如下:

.red {
    color: red;
}

.header {
    font-size: 32px;
}

.title {
    composes: red;
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}

我們增加了一個red的類名,在title中實現繼承,編譯之後的結果為:

composes-inner

發現多了一個src-styles-index__red--1ihPk的類名,正是我們上面繼承的那個類

除了在自身模塊中繼承,我們還可以繼承其他文件中的CSS規則,具體如下:

我們再styles文件夾下新建一個color.css

/* color.css */
.red {
    color: red;
}

.blue {
    color: blue;
}

然後在index.css文件中導入

/* index.css */
.red {
    color: red;
}

.header {
    font-size: 32px;
}

.title {
    color: green;
    composes: blue from './color.css';
    composes: red;
    border-bottom: 1px solid #ccc;
    padding-bottom: 20px;
}

最終我們會發現文字的顏色為綠色,可見自身模塊聲明優先順序最高,如果把自身申明的color去掉,那麼自身引入和從其他文件引入的相同申明又該如何顯示呢?

答案是自身引入的聲明的優先順序會比較高。

override

總結

至此,所有的CSS Modules用法就已經介紹完畢了,至於後續的還有如何應用於ReactVue以及Angular中,相信掌握了上面的內容之後就可以知道怎麼寫了,如何與預處理器一起使用相信問題也不大。

最後,本文代碼倉庫庫為:https://github.com/Rynxiao/css-modules-demo

參考鏈接


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 集群,為解決某個特定問題將多台電腦組合起來形成的單個系統lvs-nat:本質是多目標IP的DNAT,通過將請求報文中的目標地址和目標埠修改為某挑出的RS的RIP和PORT實現轉發lvs集群類型中的術語:VS:Virtual Server,Director Server(DS)Dispatcher... ...
  • 將mssql資料庫高版本遷移到低版本 ...
  • 作者:灬花兒灬 出處:http://www.cnblogs.com/flower1990/ 本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。 本文在排版和內容上做了點小修改。 一、安裝JAVA JDK 1、下載安裝包 ...
  • redis集群是有很多個redis一起工作,那麼就需要這個集群不是那麼容易掛掉,所以呢,理論上就應該給集群中的每個節點至少一個備用的redis服務。這個備用的redis稱為從節點(slave)。 1、集群是如何判斷是否有某個節點掛掉 首先要說的是,每一個節點都存有這個集群所有主節點以及從節點的信息。 ...
  • 1、前言 從接觸Redis也有兩年,平時就使用它來做緩存層,它給我的印象就是很強大,內置的數據結構很齊全,加上Redis5.0的到來,新增了很多特色功能。而Redis5.0最大的新特性就是多出了一個數據結構Stream,它是一個新的強大的支持多播的可持久化的消息隊列,可以去瞭解學習一下喲。言歸正傳, ...
  • 0.sql的執行順序 手寫順序 機讀順序 總結 ①From:對from左邊的表和右邊的表計算笛卡爾積,產生虛擬表c1 ②On:對c1中的數據進行on過濾,只有符合過濾條件的數據記錄才會記錄在虛擬表c2中 ③Join:若指定了連接條件(left、right),主表中的未匹配的行就會作為外部行添加到c2 ...
  • 新如何學習大數據技術?大數據怎麼入門?怎麼做大數據分析?數據科學需要學習那些技術?大數據的應用前景等等問題,已成為熱門大數據領域熱門問題,以下是對新手如何學習大數據技術問題的解答! 大數據開發學習可以按照以下內容進行學習: 第一階段:JavaSE+MySql+Linux 學習內容:Java 語言入門 ...
  • 一、 子查詢的定義 出現在其他語句中的select語句,稱為子查詢或者內查詢,外部的查詢語句稱為主查詢或者外查詢,子查詢可以包含普通select可以包含的任何語句。 外部查詢:select、insert、update、delete、set等,主要就是在select的應用。 二、 子查詢的分類 1.按 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...