在上一篇 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.js 和 lib/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 .
来源: http://www.jianshu.com/p/98404525793c