Webpack源碼分析 - css是怎樣被處理的

Webpack處理css

好久好久之前咱們在寫頁面時,一般將css單獨寫成文件引入,有時也直接在html裏寫css很是方便,這時頁面也很少動效也不須要,寫幾個頁面一把梭就能應付。css

漸漸地網頁成了大衆獲取信息的主要方式,這時的網站信息也愈來愈豐富,對網頁的質量要求愈來愈高,這一時期一些前端自動化構建工具慢慢嶄露頭角,css預處理器也進入前端的視線。這時的前端已經不是之前的單兵做戰的時代了,而隨之帶來的複雜性也挺讓人頭疼,寫個網站前要糾結用sass仍是less,選好了還要配置一番才能用,可是還好css的語法沒有太大改變。html

得益於移動端的發展,前端項目的複雜性日益增加,單頁網站慢慢作成了像APP同樣複雜。項目複雜了工具得要跟的上啊,因而前端涌現出了各類各樣的框架。React等解決了大型項目的組織和複用問題,Webpack等提供了項目從開發到發佈的配套環境,有了這些工具支持,慢慢地前端發展了本身的一套完整工做體系。這一階段咱們的思惟模式發生了很大轉變,慢慢把css也帶跑偏:前端

  • React的組件化模式使得css-in-js逐漸走上舞臺;
  • Webpack一切皆模塊的中心思想改變了咱們傳統的開發流程,從入口文件開始構建出一套可在瀏覽器運行的網站,直接抹去了前端複雜的多樣性,甚至促進了CSS Modules的發展;

我的感受css-in-js使用起來仍是感受有點彆扭,可是CSS Modules就太方便了,藉助Webpack咱們並不須要去使用style標籤引入css,仍是一樣的寫css文件,js中直接引入css看成變量使用。那麼Webpack是怎麼引入css文件並解析成變量呢?css最後又是如何做用在元素上呢?node

環境準備

  • 初始化項目:yarn init -y
  • 安裝依賴:yarn add webpack webpack-cli html-webpack-plugin
  • 建立目錄:srcloader
  • 建立Webpack配置文件殼子:
/* ./webpack.config.js */
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    // 方便查看輸出內容
    mode: 'development',
    // 方便查看輸出內容
    devtool: false,
    // 入口文件
    entry: './src/index.js',
    // 讓webpack優先使用/loader目錄下的loader
    resolveLoader: {
      modules: [path.resolve(__dirname, "loader"), "node_modules"]
    },
    // loader解析規則
    module: {
        rules: [ { test: /\.css$/, use: 'css-loader' } ]
    },
    // 輸出一個html
    plugins: [ new HtmlWebpackPlugin() ],
};
複製代碼
  • 新建一個css文件,咱們將會把這個文件打包成能夠在瀏覽器運行
/* ./src/foo.css */
body {
    background-color: yellow;
}
複製代碼
  • 新建一個js文件導入css
/* ./src/index.css */
require('./foo.css')
複製代碼

將css文件注入網頁

咱們知道webpack自己是不支持解析css文件的,因此若是咱們在js中使用require('./foo.css)會返回語法解析錯誤。咱們須要告訴webpack如何去解析css文件內容,就須要一個loader來將css轉換爲webpack能識別的js代碼進行解析。webpack

瀏覽器加載css一共有三種方法(內聯樣式/內部樣式表/外部樣式表),因此咱們最終的代碼中css必定也是以這三種方式加載,其中最簡單的方法就是把css文件內容直接轉成內部樣式表,咱們新建一個loader來試試看,既然是處理css那麼咱們就先取名爲css-loader吧:git

/* ./loader/css-loader */
module.exports = function loader(source) {
    // 將css文件特殊字符轉碼
    let cssCode = JSON.stringify(source);
    var source = `var style = document.createElement("style");`
        + `style.type = "text/css";`
        + `style.innerHTML = ${cssCode};`
        + `document.head.appendChild(style);`
    return source
}
複製代碼

運行打包後,會輸出一個html文件,打開就能夠看到樣式已經被插入到<head>中了,這段代碼進過webpack翻譯後大體變成下面的樣子:github

// 通過loader轉換後的foo.css輸出
function fooCss() {
    var style = document.createElement("style");
    style.type = "text/css";
    style.innerHTML = "body {background-color: yellow; }"
    document.head.appendChild(style);
}
// 通過編譯後的index.js輸出
fooCss()
複製代碼

咱們已經用最簡單方式完成了css文件打包輸出的功能。固然官方的loader確定沒那麼簡單,下面咱們來分析源碼學習一下。web

css-loader 編譯CSS

css-loader用於解析css文件,輸出爲一段js代碼,咱們能夠看到上面的例子幾個缺點:api

  • 若是遇到css中加載圖片文件就搞不了,由於上面只是簡單的將css賦值給標籤,若是webpack沒有解析圖片路徑,那麼圖片不會輸出到打包目錄,css也就找不到圖片文件。
  • 直接寫css,就像寫js沒有用babel同樣,功能弱兼容性還差。

因而css-loader使用了css屆的babel - postcss,不只能幫咱們支持css模塊化,搭配相應的插件想怎麼處理css都行,沒有命名衝突的煩惱簡直是命名潔癖患者的福音,下面咱們看看它到底作了什麼:瀏覽器

Step1: 插件加載

首先第一步作了參數解析,用於加載postcss插件,插件按順序執行處理css文件:

module.exports = function loader(content, map, meta) {
    const options = getOptions(this) || {};
    const callback = this.async();
    const plugins = []
    // 開啓postcss-modules
    if (options.modules) {
        // 用於支持@value語法
        plugins.push(modulesValues);
        // 用於支持composes import語法
        plugins.push(extractImports())
        // 用於標記局部css
        plugins.push(localByDefault())
        // 用於導出局部css爲變量
        plugins.push(modulesScope())
    }
    // 將icss語法轉爲普通css語法,就是解析:import,:export等標籤
    plugins.push(icssParser());
    // 用於解析@import語法
    if (options.import !== false) {
        plugins.push(importParser({}));
    }
    // 用於解析url語法
    if (options.url !== false) {
        plugins.push(urlParser({}));
    }

    // Step2 ...
}
複製代碼

postcss-modules-values

modulesValues是用於支持css變量,還會導出這個變量:

/* from */
@value primary: #BF4040;
.text-primary {
  color: primary;
}

/* to */
.text-primary {
  color: #BF4040;
}
:export {
  primary: #BF4040;
}
複製代碼

postcss-modules-extract-imports

解析composes import語法:

/* from */
.foo {
    composes: my_red from "./colors.css";
}

/* to */
:import("./colors.css") {
  i__imported_my_red_0: my_red;
}
.foo {
    composes: i__imported_my_red_0;
}
複製代碼

postcss-modules-local-by-default

使用CSS Modules後,默認咱們的css都是全局惟一的,localByDefault會把咱們的css選擇器加上:local標籤,若是須要將某些CSS標記爲全局時,須要咱們給選擇器手動加上:global標籤

/* from */
:global(.foo) {}
.bar {}

/* to */
.foo {}
:local(.bar) {}
複製代碼

postcss-modules-scope

用於解析:local標籤,將其重命名爲全局惟一,而後導出這個選擇器:

/* from */
.foo {
    color: red;
}
/* to */
._Users_demo_src_a__foo {
    color: red;
}
:export {
  foo: _Users_demo_src_a__foo;
}
複製代碼

icssParser

其中icss語法是一種中間語法,提供了兩個語法:import:export用於支持CSS Modules依賴解析,一般這對咱們來講是透明的。這裏的icssParser將解析這兩個標籤,輸出不帶這兩個標籤的css和解析後的import/export數據:

/* from */
._Users_demo_src_a__foo {
    color: red;
}
:export {
  foo: _Users_demo_src_a__foo;
}

/* to */
._Users_demo_src_a__foo {
    color: red;
}
/* 額外數據: const exports = [{foo: _Users_demo_src_a__foo}] */
複製代碼

Step2: 執行postCss

執行postCss就是按順序執行上面的一堆插件,輸出爲標準css字符串及依賴解析結果。這裏會對依賴結果進行分析,轉換成一串js字符串輸出給下一個loader:

module.exports = function loader(content, map, meta) {
    // Step1 ...

    postcss(plugins)
        .process(content, {
            from: this.remainingRequest.split('!').pop(),
            to: this.currentRequest.split('!').pop(),
            map: false,
        })
        .then((result) => {
            const imports = result.messages.filter(m => m.type === 'import').map(m => m.value);
            const exports = result.messages.filter(m => m.type === 'export').map(m => m.value);
            const replacers = result.messages.filter(m => m.type === 'replacer').map(m => m.value);
            // 轉換成js代碼,給webpack處理依賴
            const importCode = getImportCode(this, imports, 'full', false, undefined, false);
            const moduleCode = getModuleCode(this, result, 'full', false, replacers);
            const exportCode = getExportCode(this, exports, 'full', replacers, '', false);
            const jsCode = [importCode, moduleCode, exportCode].join('')
            callback(null, jsCode)
        })
}
複製代碼

舉個例子

@value my_red: * from './colors.css';
.foo {
    color: my_red;
}
複製代碼

轉換後的js以下,至關於css文件在這裏轉成了js文件,最後Webpack拿到下面的js繼續解析依賴,這樣Webpack就能正確解析到依賴的文件了。

// Imports
var ___CSS_LOADER_API_IMPORT___ = require("../loader/runtime/api.js");
var ___CSS_LOADER_ICSS_IMPORT_0___ = require("-!../loader/css-loader.js??ref--4-0!./colors.css");
exports = ___CSS_LOADER_API_IMPORT___(false);
exports.i(___CSS_LOADER_ICSS_IMPORT_0___, "", true);
// Module
exports.push([module.id, "._Users_demo_src_a__foo {\n color: " + ___CSS_LOADER_ICSS_IMPORT_0___.locals["my_red"] + ";\n}\n", ""]);
// Exports
exports.locals = {
        "my_red": "" + ___CSS_LOADER_ICSS_IMPORT_0___.locals["my_red"] + "",
        "foo": "_Users_demo_src_a__foo"
};
module.exports = exports;
複製代碼

css依賴

在上面的例子中咱們能夠到最終依賴的./color.css文件被轉換成了require("-!../loader/css-loader.js??ref--4-0!./colors.css"),咱們能夠獲得如下信息:

  • 輸出的js文件將會被Webpack繼續解析require的文件,因此Webpack將會繼續解析./colors.css
  • -!前綴說明解析時忽略normalLoaderpreLoader,因此將使用../loader/css-loader.jspostLoader解析該文件。
  • ??ref--4-0後綴說明要用全局定義的某個配置做爲css-loader的選項,這裏的ref--4-0配置就是全局css-loader的配置

css依賴的解析函數以下,importLoaders是咱們配置的值,表示css被css-loader處理前的loader數量,通過以下處理後,依賴的css便只須要被css-loader及前面的loader處理:

function getImportPrefix(loaderContext, importLoaders) {
    if (importLoaders === false) {
      return '';
    }
    // 除了css-loader外,解析還須要的loader數量
    const numberImportedLoaders = parseInt(importLoaders, 10) || 0;
    // loaderContext.loaders: 解析css的全部loader數量
    // loaderContext.loaderIndex: 當前css-loader是第幾個解析的
    const loadersRequest = loaderContext.loaders
      .slice(
        loaderContext.loaderIndex,
        loaderContext.loaderIndex + 1 + numberImportedLoaders
      )
      .map((x) => x.request)
      .join('!');
    return `-!${loadersRequest}!`;
}
複製代碼

style-loader 輸出CSS

通過css-loader處理後,咱們就須要把處理好的css文件輸出到html上了。

直接導出到html

由上面分析知道,這裏拿到的source是一個js字符串,而這串js中導出了一個exports.toString()函數能夠獲取到完整的css,那咱們就直接把這串css輸出到html。

另外這個loader的返回值會導出給require這個css的文件使用,而exports.locals裏則放了css導出的全部變量,因此咱們能夠在js中使用這些變量:

module.exports = function (source) {
    return `${source} ${` var style = document.createElement("style"); style.type = "text/css"; style.innerHTML = exports.toString() document.head.appendChild(style); `} module.exports = exports.locals; `
};
複製代碼

因而咱們能夠在js中寫以下代碼,這裏將使用上面導出的css變量,變量的值表明選擇器的值,這就是css能在js中使用的CSS Modules原理了:

const styles = require('./a.foo')
const div = document.createElement('div')
div.innerHTML = `<span class='${styles.foo}'>ME</span><div class='${styles.bar}'>YOU</div>`
document.body.appendChild(div)
複製代碼

更好的導出方法 - 巧用pitch

固然上面的方法是比較直接的,官方使用了pitch來更優雅地解決了這個問題。使用pitch的攔截功能直接結束本次文件解析,並將css以require的方式從新引入,使用!!配合參數,使得下一次解析不須要通過style-loader

module.exports.pitch = function (request) {
    // Webpack會繼續解析返回的js,此次將只使用css-loader去解析css
    // require("!!../loader/css-loader.js??ref--4-0!./colors.css")
    const req = `${`var content = require(${loaderUtils.stringifyRequest(this, `!!${request}`)});`}`
    
    // 在這裏能夠拿到css-loader解析後的內容,直接輸出到html
    const styles = `${` var style = document.createElement("style"); style.type = "text/css"; style.innerHTML = content.toString() document.head.appendChild(style); `}`

    // 導出css選擇器的變量給js使用
    const exp = `module.exports = content.locals ? content.locals : {};`
    return req + styles + exp;
};
複製代碼

less-loader 編譯less

除了使用上面的postcss,咱們還能夠無縫對接less等解析器:

module.exports = function(source) {
    const callback = this.async()
    const options = {
      // less解析@import時的參考路徑
      filename = this.resource;
    }
    // 調用less解析
    less.render(source, options).then(({ css, map, imports }) => {
        // 因爲less的依賴不是webpack解析的,因此要告訴webpack監聽這些文件
        imports.forEach(this.addDependency, this);
        // 把解析好的css傳給下一個loader
        callback(null, css)
    })
}
複製代碼

解析依賴時,不像css-loader是將import轉成了require給Webpack去解析,less解析器是本身解析依賴。就是說若是使用了@import './colors.css,less解析器輸出的結果已經不含依賴了。

參考資料

loader及其優化

less-loader

css-loader

css-modules

icss

相關文章
相關標籤/搜索