Webpack Loader原始碼導讀之css-loader

hihl發表於2018-02-02

原文地址:Webpack Loader原始碼導讀之css-loader

在上一篇Webpack Loader原始碼導讀之less-loader我們介紹了less-loader

本篇是Webpack Loader原始碼導讀系列中關於css-loader的解讀,主要闡述loader的工作,及部份配置項作用。

原始碼結構

原始碼 v0.28.8,lib目錄如下:

lib
|____compile-exports.js
|____createResolver.js
|____css-base.js
|____getImportPrefix.js
|____getLocalIdent.js
|____loader.js
|____localsLoader.js
|____processCss.js
複製程式碼

入口檔案

css-loader有兩個入口檔案lib/loader.jslib/localsLoader.js

配置項概覽

名稱 型別 預設值 描述
root String / 解析 URL 的路徑,以 / 開頭的 URL 不會被轉譯
url Boolean true 啟用/禁用 url() 處理
alias Object {} 建立別名更容易匯入一些模組
import Boolean true 啟用/禁用 @import 處理
modules或module Boolean false 啟用/禁用 CSS 模組
sourceMap Boolean false 啟用/禁用 Sourcemap
camelCase Boolean或String false 以駝峰化式命名匯出類名
importLoaders Number 0 在 css-loader 前應用的 loader 的數量
localIdentName String [hash:base64] 配置生成的識別符號(ident)

各個配置項的作用,在下面走讀程式碼的過程我們會舉例說明去作用;

processCss

無論是loader.js還是localsLoader.js,都會先解析loader選項,然後執行processCss編譯css檔案,他們的區別在於對編譯結果的處理不通,首先我們先看看processCss做了什麼處理。

先跑個示例 b.css

@value colorYellow: yellow;

:local(.className) {
    background: red;
    color: colorYellow;
}

:local(.subClass) {
    composes: className;
    background: blue;
}
複製程式碼

a.css

@value colorYellow from './b.css';

:local(.aClass) {
    composes: className from './b.css';
    background: colorYellow;
}

.app {
    font-size: 14px;
}
複製程式碼

loader配置,為了便於看編譯結果,我們配置了extract-text-webpack-plugin

{
  test: /\.css$/,
  loader: ExtractTextPlugin.extract({
    fallback: 'style-loader',
    use: [
      {
        loader: "css-loader",
        options: {
          minimize: true,
          sourceMap: true,
          modules: true,
          localIdentName: '[hash:base64]'
        }
      }
    ]
  })
}
複製程式碼

首先是經過一波postcss(v5.2.17)的處理, 先將輸入內容做一次parse(程式碼在postcss/lib/parser.js中),提取出一些關鍵字,轉成指定物件用於後面解析,轉成如下格式的物件:

{
  "raws": {
    "semicolon": false,
    "after": "\n"
  },
  "type": "root",
  "nodes": [
    {
      "raws": {
        "before": "",
        "between": "",
        "afterName": " "
      },
      "type": "atrule",
      "name": "value",
      "source": {
        "start": {
          "line": 1,
          "column": 1
        },
        "input": {
          "css": "@value colorYellow from './b.css';\n\n:local(.aClass) {\n    composes: className from './b.css';\n    background: colorYellow;\n}\n\n.app {\n    font-size: 14px;\n}\n",
          "file": "/css-loader!/Users/yzf/webpack-tuition/loaders/babel/src/a.css"
        },
        "end": {
          "line": 1,
          "column": 34
        }
      },
      "params": "colorYellow from './b.css'"
    },
    ...
  ],
  "source": {
    "input": {
      "css": "@value colorYellow from './b.css';\n\n:local(.aClass) {\n    composes: className from './b.css';\n    background: colorYellow;\n}\n\n.app {\n    font-size: 14px;\n}\n",
      "file": "/css-loader!/Users/yzf/webpack-tuition/loaders/babel/src/a.css"
    },
    "start": {
      "line": 1,
      "column": 1
    }
  }
}
複製程式碼

其中nodes的型別包括root(跟節點)、atrule(@規則)、decl(宣告)、comment(註釋)和rule(普通規則)幾種型別 然後這個nodes會經過一系列外掛處理,在外掛處理過程中會經常見到walkAtRules、walkRules、walkDecls和walkComments幾個方法,這幾個方法程式碼在postcss/lib/container.js中, 顧名思義,這幾個方法分別是用來解析這幾種不同規則的,如walkAtRules('value',callback)意思就是解析@value規則 在css-loader中使用到了如下幾個外掛

var pipeline = postcss([
    modulesValues,
    localByDefault({
        mode: options.mode,
        rewriteUrl: function(global, url) {
            if(parserOptions.url){
                url = url.trim();

                if(!url.replace(/\s/g, '').length || !loaderUtils.isUrlRequest(url, root)) {
                    return url;
                }
                if(global) {
                    return loaderUtils.urlToRequest(url, root);
                }
            }
            return url;
        }
    }),
    extractImports(),
    modulesScope({
        generateScopedName: function generateScopedName (exportName) {
            return customGetLocalIdent(options.loaderContext, localIdentName, exportName, {
                regExp: localIdentRegExp,
                hashPrefix: query.hashPrefix || "",
                context: context
            });
        }
    }),
    parserPlugin(parserOptions)
]);
複製程式碼

接下來我們來了解下這些外掛都做了什麼事情

第一個外掛是modulesValues(postcss-modules-values v1.3.0),其作用是解析變數@value,如b.css中 定義了@value colorYellow: yellow; 在後面就可以使用color: colorYellow;,效果等同color: yellow;,在a.css中也可以從b.css匯入該值@value colorYellow from './b.css';;

第二個外掛是localByDefault(postcss-modules-local-by-default v1.2.0),該外掛的作用與css-loader的配置項modules有關; 如果modules配置為true,則該外掛會給每個類名前加:local,這樣在js中import s from './a.css'時得到的s值為{ colorYellow: 'yellow', aClass: '_3RfWl8Fjg9j10HraIxvVwo _2WlYzvzC-urSx4y6mIOOFM', app: '_2fkqRy5LeEcw20RyY_eLpM' }, 否則為{ colorYellow: 'yellow', aClass: '_3RfWl8Fjg9j10HraIxvVwo _2WlYzvzC-urSx4y6mIOOFM' };區別在於a.css中app這個class,在示例程式碼中.app前面沒加:local則匯出的物件中不包含app, 但是modules設定為true時本外掛會預設給app加上local,所以匯出的物件中就有app。

第三個外掛是extractImports(postcss-modules-extract-imports v1.1.0),看a.css中的程式碼,該外掛的作用是將

:local(.aClass) {
    composes: className from './b.css';
    background: colorYellow;
}
複製程式碼

轉成

:import("./b.css"){
  className: i__imported_className_0;
}
:local(.aClass) {
    composes: i__imported_className_0;
    background: colorYellow;
}
複製程式碼

第四個外掛是modulesScope(postcss-modules-scope v1.1.0),該外掛的作用就是export出js中能夠引入的物件,會將

:local(.aClass) {
    composes: i__imported_className_0;
    background: colorYellow;
}
複製程式碼

轉成

:export {
  aClass: _3RfWl8Fjg9j10HraIxvVwo
}
._3RfWl8Fjg9j10HraIxvVwo {
    composes: i__imported_className_0;
    background: colorYellow;
}
複製程式碼

這裡暫時不會處理composes。其中轉換出來的類名,如_3RfWl8Fjg9j10HraIxvVwo是根據配置項localIdentName: '[hash:base64]'決定的,如果配置的是 localIdentName: '[local]',則類名不會變,即還是aClass

最後一個外掛是parserPlugin,這個程式碼就在css-loader/src/processCss.js中,是css-loader對前面編譯結果做的最後處理。 我們給demo增加一個c.css,然後在a.css中匯入@import "./c.css",這個外掛做了以下事情:

  • 如果配置了import: true(預設為true),則解析@import規則,根據options.root的配置提取出匯入模組的url路徑,並暫存到importItems中;
  • 通過var icss = icssUtils.extractICSS(css);從nodes中提取出每個檔案的:import與:export資訊,:import的內容暫存到imports和importItems中
// imports
{
  "$i__const_colorYellow_0": 1, // 值為importItems中的索引
  "$i__imported_className_0": 2
}
// importItems
[
  {
    "url": "./c.css",
    "mediaQuery": ""
  },
  {
    "url": "./b.css",
    "export": "colorYellow"
  },
  {
    "url": "./b.css",
    "export": "className"
  }
]
複製程式碼

然後根據imports和importItems將exports從

{
  "colorYellow": "i__const_colorYellow_0",
  "aClass": "_3RfWl8Fjg9j10HraIxvVwo i__imported_className_0",
  "app": "_2fkqRy5LeEcw20RyY_eLpM"
}
複製程式碼

轉換成

{
  "colorYellow": "___CSS_LOADER_IMPORT___1___", // 1,2即為importItems中的索引
  "aClass": "_3RfWl8Fjg9j10HraIxvVwo ___CSS_LOADER_IMPORT___2___",
  "app": "_2fkqRy5LeEcw20RyY_eLpM"
}
複製程式碼
  • 將nodes中宣告節點的值i__const_colorYellow_0都替換成___CSS_LOADER_IMPORT___1___形式的;

經過所有外掛處理以後結果是這樣的(當然中間還有個minimize配置為true時會走cssnano壓縮,這裡略過了):

/* a.css */
._3RfWl8Fjg9j10HraIxvVwo{background:___CSS_LOADER_IMPORT___1___}._2fkqRy5LeEcw20RyY_eLpM{font-size:14px}
/* b.css */
._2WlYzvzC-urSx4y6mIOOFM{background:red;color:#ff0}._2ZjxOCWmD5GtQv4c-EHJ1g{background:blue}
/* c.css */
._2W2YIQ3PA5I9QGXroo7b2m{display:block}
複製程式碼

loader最後處理

對於上面的處理結果,還存在著___CSS_LOADER_IMPORT___1___這樣的內容,顯然還不是最終結果,回到我們的入口檔案loader.js看看最後的處理, 當然,如果你使用的loader是css-loader/locals,則入口檔案是localsLoader.js。 loader.js最後要做的就是拼出最後module.exports要匯出去的模組,將依賴的模組通過正規表示式/___CSS_LOADER_IMPORT___([0-9]+)___/g及前面解析出來的importItems 替換成require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./b.css").locals["colorYellow"]格式的 最終匯出模組,在js中可以直接import進來得到一個物件

/* a.css */
exports = module.exports = require("../node_modules/css-loader/lib/css-base.js")(true);
// imports
exports.i(require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./c.css"), "");
exports.i(require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./b.css"), undefined);

// module
exports.push([module.id, "._3RfWl8Fjg9j10HraIxvVwo{background:" 
  + require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./b.css").locals["colorYellow"] 
  + "}._2fkqRy5LeEcw20RyY_eLpM{font-size:14px}", "", {"version":3,"sources":["/Users/yzf/webpack-tuition/loaders/babel/src/a.css"],"names":[],"mappings":"AAGA,yBAEI,sCAAwB,CAC3B,AAED,yBACI,cAAgB,CACnB","file":"a.css","sourcesContent":["@import \"./c.css\";\n@value colorYellow from './b.css';\n\n:local(.aClass) {\n    composes: className from './b.css';\n    background: colorYellow;\n}\n\n.app {\n    font-size: 14px;\n}\n"],"sourceRoot":""}]);

// exports
exports.locals = {
  "colorYellow": "" + require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./b.css").locals["colorYellow"] + "",
  "aClass": "_3RfWl8Fjg9j10HraIxvVwo " + require("-!../node_modules/css-loader/index.js?{\"minimize\":true,\"sourceMap\":true,\"modules\":true,\"localIdentName\":\"[hash:base64]\"}!./b.css").locals["className"] + "",
  "app": "_2fkqRy5LeEcw20RyY_eLpM"
};

/* b.css */
exports = module.exports = require("../node_modules/css-loader/lib/css-base.js")(true);
// imports

// module
exports.push([module.id, "._2WlYzvzC-urSx4y6mIOOFM{background:red;color:#ff0}._2ZjxOCWmD5GtQv4c-EHJ1g{background:blue}", "", {"version":3,"sources":["/Users/yzf/webpack-tuition/loaders/babel/src/b.css"],"names":[],"mappings":"AAEA,yBACI,eAAgB,AAChB,UAAmB,CACtB,AAED,yBAEI,eAAiB,CACpB","file":"b.css","sourcesContent":["@value colorYellow: yellow;\n\n:local(.className) {\n    background: red;\n    color: colorYellow;\n}\n\n:local(.subClass) {\n    composes: className;\n    background: blue;\n}\n"],"sourceRoot":""}]);

// exports
exports.locals = {
  "colorYellow": "yellow",
  "className": "_2WlYzvzC-urSx4y6mIOOFM",
  "subClass": "_2ZjxOCWmD5GtQv4c-EHJ1g _2WlYzvzC-urSx4y6mIOOFM"
};
/* c.css */
exports = module.exports = require("../node_modules/css-loader/lib/css-base.js")(true);
// imports

// module
exports.push([module.id, "._2W2YIQ3PA5I9QGXroo7b2m{display:block}", "", {"version":3,"sources":["/Users/yzf/webpack-tuition/loaders/babel/src/c.css"],"names":[],"mappings":"AAAA,yBACI,aAAe,CAClB","file":"c.css","sourcesContent":[".test {\n    display: block;\n}\n"],"sourceRoot":""}]);

// exports
exports.locals = {
  "test": "_2W2YIQ3PA5I9QGXroo7b2m"
};
複製程式碼
/* 合併後main.css */
._2W2YIQ3PA5I9QGXroo7b2m{display:block}._2WlYzvzC-urSx4y6mIOOFM{background:red;color:#ff0}._2ZjxOCWmD5GtQv4c-EHJ1g{background:blue}._3RfWl8Fjg9j10HraIxvVwo{background:yellow}._2fkqRy5LeEcw20RyY_eLpM{font-size:14px}
/*# sourceMappingURL=main.css.map*/
複製程式碼

如果是在服務端使用css-loader/locals則不搭配ExtractTextPlugin,處理結果為

/* a.css */
module.exports = {
	"colorYellow": "" + require("-!../node_modules/css-loader/locals.js??ref--1-0!./b.css")["colorYellow"] + "",
	"aClass": "_3RfWl8Fjg9j10HraIxvVwo " + require("-!../node_modules/css-loader/locals.js??ref--1-0!./b.css")["className"] + "",
	"app": "_2fkqRy5LeEcw20RyY_eLpM"
};
/* b.css */
module.exports = {
	"colorYellow": "yellow",
	"className": "_2WlYzvzC-urSx4y6mIOOFM",
	"subClass": "_2ZjxOCWmD5GtQv4c-EHJ1g _2WlYzvzC-urSx4y6mIOOFM"
};
複製程式碼

c.css沒有模組匯出

小結

這個解析過程有點長,但是css-loader對css處理的主要過程基本都提到了,我們也能夠知道經過這個loader以後樣式變成什麼樣,匯出了什麼模組;

當然,上面提到的@import @value composes等特性在使用less或者sass等其他css預編譯時是用不到的,因為他們有自己的語法,我們一般不會去使用這些特性;

css-loader處理完以後,在實際使用時我們在最後都會再經過style-loader處理,有時搭配ExtractTextPlugin,那麼這兩個loader或外掛又做了什麼呢?我們下篇見。

如果喜歡請點贊,歡迎關注我的部落格hiihl

相關文章