好久好久之前咱们在写页面时,一般将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解析器输出的结果已经不含依赖了。