好久好久之前咱們在寫頁面時,一般將css單獨寫成文件引入,有時也直接在html裏寫css很是方便,這時頁面也很少動效也不須要,寫幾個頁面一把梭就能應付。css
漸漸地網頁成了大衆獲取信息的主要方式,這時的網站信息也愈來愈豐富,對網頁的質量要求愈來愈高,這一時期一些前端自動化構建工具慢慢嶄露頭角,css預處理器也進入前端的視線。這時的前端已經不是之前的單兵做戰的時代了,而隨之帶來的複雜性也挺讓人頭疼,寫個網站前要糾結用sass
仍是less
,選好了還要配置一番才能用,可是還好css的語法沒有太大改變。html
得益於移動端的發展,前端項目的複雜性日益增加,單頁網站慢慢作成了像APP同樣複雜。項目複雜了工具得要跟的上啊,因而前端涌現出了各類各樣的框架。React
等解決了大型項目的組織和複用問題,Webpack
等提供了項目從開發到發佈的配套環境,有了這些工具支持,慢慢地前端發展了本身的一套完整工做體系。這一階段咱們的思惟模式發生了很大轉變,慢慢把css也帶跑偏:前端
css-in-js
逐漸走上舞臺;一切皆模塊
的中心思想改變了咱們傳統的開發流程,從入口文件開始構建出一套可在瀏覽器運行的網站,直接抹去了前端複雜的多樣性,甚至促進了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
src
,loader
/* ./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() ],
};
複製代碼
/* ./src/foo.css */
body {
background-color: yellow;
}
複製代碼
/* ./src/index.css */
require('./foo.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文件,輸出爲一段js代碼,咱們能夠看到上面的例子幾個缺點:api
因而css-loader
使用了css屆的babel - postcss
,不只能幫咱們支持css模塊化,搭配相應的插件想怎麼處理css都行,沒有命名衝突的煩惱簡直是命名潔癖患者的福音,下面咱們看看它到底作了什麼:瀏覽器
首先第一步作了參數解析,用於加載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 ...
}
複製代碼
modulesValues
是用於支持css變量,還會導出這個變量:
/* from */
@value primary: #BF4040;
.text-primary {
color: primary;
}
/* to */
.text-primary {
color: #BF4040;
}
:export {
primary: #BF4040;
}
複製代碼
解析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;
}
複製代碼
使用CSS Modules
後,默認咱們的css都是全局惟一的,localByDefault
會把咱們的css選擇器加上:local
標籤,若是須要將某些CSS標記爲全局時,須要咱們給選擇器手動加上:global
標籤
/* from */
:global(.foo) {}
.bar {}
/* to */
.foo {}
:local(.bar) {}
複製代碼
用於解析:local
標籤,將其重命名爲全局惟一,而後導出這個選擇器:
/* from */
.foo {
color: red;
}
/* to */
._Users_demo_src_a__foo {
color: red;
}
:export {
foo: _Users_demo_src_a__foo;
}
複製代碼
其中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}] */
複製代碼
執行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;
複製代碼
在上面的例子中咱們能夠到最終依賴的./color.css
文件被轉換成了require("-!../loader/css-loader.js??ref--4-0!./colors.css")
,咱們能夠獲得如下信息:
require
的文件,因此Webpack將會繼續解析./colors.css
-!
前綴說明解析時忽略normalLoader
和preLoader
,因此將使用../loader/css-loader.js
及postLoader
解析該文件。??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}!`;
}
複製代碼
通過css-loader
處理後,咱們就須要把處理好的css文件輸出到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
的攔截功能直接結束本次文件解析,並將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;
};
複製代碼
除了使用上面的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解析器輸出的結果已經不含依賴了。