深刻淺出 CSS Modules

CSS Modules 是什麼?

官方文檔的介紹以下:javascript

A CSS Modules is a CSS file in which all class names and animation names are scoped locally by default.

全部的類名和動畫名稱默認都有各自的做用域的 CSS 文件。CSS Modules 並非 CSS 官方的標準,也不是瀏覽器的特性,而是使用一些構建工具,好比 webpack,對 CSS 類名和選擇器限定做用域的一種方式(相似命名空間)css

本文來介紹一下 CSS Modules 的簡單使用,以及 CSS Modules 的實現原理(CSS-loader 中的實現)html

CSS Modules 的簡單使用

項目搭建以及配置

新建一個項目,本文的 Demojava

npx create-react-app learn-css-modules-react
cd learn-css-modules-react
# 顯示 webpack 的配置
yarn eject

看到 config/webpack.config.js,默認狀況下,React 腳手架搭建出來的項目,只有 .module.css 支持模塊化,若是是本身搭建的話,能夠支持 .css 文件的後綴等node

// Adds support for CSS Modules (https://github.com/css-modules/css-modules)
// using the extension .module.css
{
  test: cssModuleRegex,
  use: getStyleLoaders({
    importLoaders: 1,
    sourceMap: isEnvProduction
      ? shouldUseSourceMap
      : isEnvDevelopment,
    modules: {
      getLocalIdent: getCSSModuleLocalIdent,
    },
  }),
}

其中 getStyleLoaders 函數,能夠看到 css-loader 的配置react

const getStyleLoaders = (cssOptions, preProcessor) => {
  const loaders = [
    // ...
    {
      loader: require.resolve('css-loader'),
      options: cssOptions,
    },
    // ...
  ];
  // ...
  return loaders;
};

咱們就基於這個環境當作示例進行演示webpack

局部做用域

以前的樣式git

首先,咱們將 App.css 修改爲 App.module.css,而後導入 css,並設置(這裏有個小知識點,實際上 CSS Modules 推薦的命名是駝峯式,主要是這樣的話,使用對象 style.className 就能夠訪問。若是是以下,就須要 styles['***-***'])github

import styles from './App.module.css';

// ...
<header className={styles['App-header']}></header>

就會根據特定的規則生成相應的類名web

這個命名規則是能夠經過 CSS-loader 進行配置,相似以下的配置:

module: {
  loaders: [
    // ...
    {
      test: /\.css$/,
      loader: "style-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]"
    },
  ]
}

全局做用域

默認狀況下,咱們發現,在 css modules 中定義的類名必須得經過相似設置變量的方式給 HTML 設置(如上示例所示)

那麼我能像其餘的 CSS 文件同樣直接使用類名(也就是普通的設置方法),而不是編譯後的哈希字符串麼?

使用 :global 的語法,就能夠聲明一個全局的規則

:global(.App-link) {
  color: #61dafb;
}

這樣就能夠在 HTML 中直接跟使用普通的 CSS 同樣了

但這裏感受就是 CSS Modules 給開發者留的一個後門,咱們這樣的 CSS,仍是直接放在普通 .css 文件中比較好,我理解這就是 React 爲何對待 .css 和 .module.css 不一樣後綴進行不一樣的處理的緣由

Class 的組合

在 CSS Modules 中,一個選擇器能夠繼承另外一個選擇器的規則,這稱爲 "組合"("composition"

好比,咱們定義一個 font-red,而後在 .App-header 中使用 composes: font-red; 繼承

.font-red {
  color: red;
}

.App-header {
  composes: font-red;
  /* ... */
}

輸入其餘的模塊

不只僅能夠同一個文件中的,還能夠繼承其餘文件中的 CSS 規則

定義一個 another.module.css

.font-blue {
  color: blue;
}

在 App.module.css 中

.App-header {
  /* ... */
  composes: font-blue from './another.module.css';
  /* ... */
}

使用變量

咱們還可使用變量,定義一個 colors.module.css

@value blue: #0c77f8;

在 App.module.css 中

@value colors: "./colors.module.css";
@value blue from colors;

.App-header {
  /* ... */
  color: blue;
}

使用小結

整體而言,CSS Modules 的使用偏簡單,上手很是的快,接下來咱們看看 Webpack 中 CSS-loader 是怎麼實現 CSS Modules

CSS Modules 的實現原理

從 CSS Loader 開始講起

lib/processCss.js

var pipeline = postcss([
    ...
    modulesValues,
    modulesScope({
    // 根據規則生成特定的名字
        generateScopedName: function(exportName) {
            return getLocalIdent(options.loaderContext, localIdentName, exportName, {
                regExp: localIdentRegExp,
                hashPrefix: query.hashPrefix || "",
                context: context
            });
        }
    }),
    parserPlugin(parserOptions)
]);

主要看 modulesValuesmodulesScope 方法,實際上這兩個方法又是來自其餘兩個包

var modulesScope = require("postcss-modules-scope");
var modulesValues = require("postcss-modules-values");

postcss-modules-scope

這個包主要是實現了 CSS Modules 的樣式隔離(Scope Local)以及繼承(Extend)

它的代碼比較簡單,基本一個文件完成,源碼能夠看這裏,這裏會用到 postcss 處理 AST 相關,咱們大體瞭解它的思想便可

默認的命名規則

實際上,假如你沒有設置任何的規則時候會根據以下進行命名

// 生成 Scoped name 的方法(沒有傳入的時候的默認規則)
processor.generateScopedName = function (exportedName, path) {
  var sanitisedPath = path.replace(/\.[^\.\/\\]+$/, '').replace(/[\W_]+/g, '_').replace(/^_|_$/g, '');
  return '_' + sanitisedPath + '__' + exportedName;
};

這種寫法在不少的源碼中咱們均可以看到,之後寫代碼的時候也能夠採用

var processor = _postcss2['default'].plugin('postcss-modules-scope', function (options) {
  // ...
  return function (css) {
    // 若是有傳入,則採用傳入的命名規則
       // 不然,採用默認定義的 processor.generateScopedName
    var generateScopedName = options && options.generateScopedName || processor.generateScopedName;
  }
  // ...
})

前置知識—— postcss 遍歷樣式的方法

css ast 主要有 3 種父類型

  • AtRule: @xxx 的這種類型,如 @screen,由於下面會提到變量的使用 @value
  • Comment: 註釋
  • Rule: 普通的 css 規則

還有幾個個比較重要的子類型:

  • decl: 指的是每條具體的 css 規則
  • rule:做用於某個選擇器上的 css 規則集合

不一樣的類型進行不一樣的遍歷

  • walk: 遍歷全部節點信息,不管是 atRule、rule、comment 的父類型,仍是 ruledecl 的子類型
  • walkAtRules:遍歷全部的 atRule
  • walkComments:遍歷註釋
  • walkDecls
  • walkRules

做用域樣式的實現

// Find any :local classes
// 找到全部的含有 :local 的 classes
css.walkRules(function (rule) {
  var selector = _cssSelectorTokenizer2['default'].parse(rule.selector);
  // 獲取 selector
  var newSelector = traverseNode(selector);
  rule.selector = _cssSelectorTokenizer2['default'].stringify(newSelector);
  // 遍歷每一條規則,假如匹配到則將類名等轉換成做用域名稱
  rule.walkDecls(function (decl) {
    var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/);
    tokens = tokens.map(function (token, idx) {
      if (idx === 0 || tokens[idx - 1] === ',') {
        var localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token);
        if (localMatch) {
          // 獲取做用域名稱
          return localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length);
        } else {
          return token;
        }
      } else {
        return token;
      }
    });
    decl.value = tokens.join('');
  });
});

css.walkRules 遍歷全部節點信息,不管是 atRule、rule、comment 的父類型,仍是 ruledecl 的子類型,獲取 selector

// 遞歸遍歷節點,找到目標節點
function traverseNode(node) {
  switch (node.type) {
    case 'nested-pseudo-class':
      if (node.name === 'local') {
        if (node.nodes.length !== 1) {
          throw new Error('Unexpected comma (",") in :local block');
        }
        return localizeNode(node.nodes[0]);
      }
      /* falls through */
    case 'selectors':
    case 'selector':
      var newNode = Object.create(node);
      newNode.nodes = node.nodes.map(traverseNode);
      return newNode;
  }
  return node;
}

walkDecls 遍歷每一條規則,生成相應的 Scoped Name

// 生成一個 Scoped Name
function exportScopedName(name) {
  var scopedName = generateScopedName(name, css.source.input.from, css.source.input.css);
  exports[name] = exports[name] || [];
  if (exports[name].indexOf(scopedName) < 0) {
    exports[name].push(scopedName);
  }
  return scopedName;
}

關於實現 composes 的組合語法,有點相似,再也不贅述

postcss-modules-values

這個庫的主要做用是在模塊文件之間傳遞任意值,主要是爲了實如今 CSS Modules 中可以使用變量

它的實現也是隻有一個文件,具體查看這裏

查看全部的 @value 語句,並將它們視爲局部變量或導入的,最後保存到 definitions 對象中

/* Look at all the @value statements and treat them as locals or as imports */
// 查看全部的 @value 語句,並將它們視爲局部變量仍是導入的
css.walkAtRules('value', atRule => {
  // 相似以下的寫法
  // @value primary, secondary from colors 
  if (matchImports.exec(atRule.params)) {
    addImport(atRule)
  } else {
    // 處理定義在文件中的 相似以下
    // @value primary: #BF4040;
        // @value secondary: #1F4F7F;
    if (atRule.params.indexOf('@value') !== -1) {
      result.warn('Invalid value definition: ' + atRule.params)
    }

    addDefinition(atRule)
  }
})

假如是導入的,調用的 addImport 方法

const addImport = atRule => {
  // 若是有 import 的語法
  let matches = matchImports.exec(atRule.params)
  if (matches) {
    let [/*match*/, aliases, path] = matches
    // We can use constants for path names
    if (definitions[path]) path = definitions[path]
    let imports = aliases.replace(/^\(\s*([\s\S]+)\s*\)$/, '$1').split(/\s*,\s*/).map(alias => {
      let tokens = matchImport.exec(alias)
      if (tokens) {
        let [/*match*/, theirName, myName = theirName] = tokens
        let importedName = createImportedName(myName)
        definitions[myName] = importedName
        return { theirName, importedName }
      } else {
        throw new Error(`@import statement "${alias}" is invalid!`)
      }
    })
    // 最後會根據這個生成 import 的語法
    importAliases.push({ path, imports })
    atRule.remove()
  }
}

不然則直接 addDefinition,兩個的思想大體我理解都是找到響應的變量,而後替換

// 添加定義
const addDefinition = atRule => {
  let matches
  while (matches = matchValueDefinition.exec(atRule.params)) {
    let [/*match*/, key, value] = matches
    // Add to the definitions, knowing that values can refer to each other
    definitions[key] = replaceAll(definitions, value)
    atRule.remove()
  }
}

總結

CSS Modules 並非 CSS 官方的標準,也不是瀏覽器的特性,而是使用一些構建工具,好比 webpack,對 CSS 類名和選擇器限定做用域的一種方式(相似命名空間)。經過 CSS Modules,咱們能夠實現 CSS 的局部做用域,Class 的組合等功能。最後咱們知道 CSS Loader 其實是經過兩個庫進行實現的。其中, postcss-modules-scope —— 實現CSS Modules 的樣式隔離(Scope Local)以及繼承(Extend)和 postcss-modules-values ——在模塊文件之間傳遞任意值

參考

相關文章
相關標籤/搜索