一、编译less文件,需要哪些loader?
{
test: /.css$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
从执行顺序上说,loader先执行后面的loader,再执行前面的loader。
也就是说,针对less文件的执行顺序为:less-loader、css-loader、style-loader。
二、loader的实现原理
A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the this
context provided to it.
loader是node中的一个模块,对外export的是一个函数。当一个文件资源需要被转化的时候,就会调用该loader。这个函数通过提供给它的this上下文,来访问loader的API。
1、这是一个什么都不做的loader.
module.exports = function(source) {
return source;
}
loader函数有一个参数,该参数为文件的内容。以字符串的方式,给到我们开发者。
上面的source。就是一个包含某个文件内容的字符串。
2、loader函数辅助包(loader-utils)
loader配置中,不仅仅有loader的名字,还有一些options的配置选项,该包的主要作用,提供一些额外的功能,比如获取配置的options。
npm下载到本地
npm install --save loader-utils
使用获取options
const loaderUtils = require('loader-utils');
nodule.exports = function(source) {
const options = loaderUtils.getOptions(this);
console.log(options);
return source;
}
3、loader函数校验包(schema-utils)
我们进行配置的options,是否符合loader的预期,这需要做一些校验,而schema-utils包,就是做相关的校验工作的。
npm下载到本地
npm install --save schema-utils
校验获取的options。
const loaderUtils = require('loader-utils');
const schemaUtils = require('schema-utils');
// 这个schema表示校验规则,options必须传递一个testProps的属性,其值的类型为number类型。
const schema = {
type: 'object',
properties: {
testProps: {
type: 'number',
},
},
};
module.exports = function (source) {
const options = loaderUtils.getOptions(this);
console.log(options)
// 校验函数:参数一,校验规则。参数二,获取的options。
schemaUtils.validate(schema, options, {
name: 'test-loader',
baseDataPath: 'options',
});
console.log(source)
return `/** my name is liwudi **/\n${source}`;
}
三、css编译部分的loader解析
1、各个loader的功能是什么
-
css-loader:会对 @import 和 url() 进行处理,就像js解析 import/require() 一样。
-
style-loader:把css插入到dom中。
-
less-loader:会把less编译为css的loader。
2、解析
less-loader解析:
import less from 'less';
const { css, imports } = less.render(source, lessOptions);
css就是最终得到的结果。也就是正式的把less编译成css字符串了。 less-loader的原理很简单,就是调用less库提供的方法,转译less语法后输出,如下:
// less-loader实现(经简化)
const less = require('less');
module.exports = function(content) {
const callback = this.async(); // 转译比较耗时,采用异步方式
const options = this.getOptions(); // 获取配置文件中less-loader的options
less.render(
content,
createOptions(options), // less转译的配置
(err, output) => {
callback(err, output.css); // 将生成的css代码传递给下一个loader
}//第三个参数是个callback,如果不传callback,则render方法会返回一个promise
);
};
编译结果的处理
function processResult(loaderContext, resultPromise) {
const { callback } = loaderContext;
resultPromise
.then(({ css, map, imports }) => {
imports.forEach(loaderContext.addDependency, loaderContext);
return {
// Removing the sourceMappingURL comment.
// See removeSourceMappingUrl.js for the reasoning behind this.
css: removeSourceMappingUrl(css),
map: typeof map === 'string' ? JSON.parse(map) : map,
};
}, (lessError) => {
throw formatLessError(lessError);
})
.then(({ css, map }) => {
callback(null, css, map);
}, callback);
}
编译后的结果包括css,map和imports三个,css是less编译成的css内容,map则是sourceMap相关信息,imports是编译过程中所有的依赖文件路径。 拿到编译结果后,首先是调用addDependency把所有imports中的文件添加到依赖里面,这个方法的作用时在watch模式时,依赖的这些文件变化时会出发编译更新。 然后是removeSourceMappingUrl(css),这个方法的作用是移除结果中的sourceMappingURL=,理由是less-loader无法知道最终的sourceMap会在哪里。 最后调用callback(null, css, map)把结果传给下一个loader去执行,less-loader的的工作就完成了
css-loader解析:
css-loader的作用主要是解析css文件中的@import和url语句,处理css-modules,并将结果作为一个js模块返回 假如我们有a.css、b.css、c.css:
// a.css
@import './b.css'; // 导入b.css
.a {
font-size: 16px;
}
// b.css
@import './c.css'; // 导入c.css
.b {
color: red;
}
// c.css
.c {
font-weight: bolder;
}
css-loader对a.css的编译输出:
// css-loader输出
exports = module.exports = require("../../../node_modules/css-loader/lib/css-base.js")(false);
// imports
// 文件需要的依赖js模块,这里为空
···
// module
exports.push([ // 模块导出内容
module.id,
".src-components-Home-index__c--3riXS {\n font-weight: bolder;\n}\n.src-components-Home-index__b--I-yI3 {\n color: red;\n}\n.src-components-Home-index__a--3EFPE {\n font-size: 16px;\n}\n",
""
]);
// exports
exports.locals = { // css-modules的类名映射
"c": "src-components-Home-index__c--3riXS",
"b": "src-components-Home-index__b--I-yI3",
"a": "src-components-Home-index__a--3EFPE"
};
可以理解为css-loader将a.css、b.css和c.css的样式内容以字符串的形式拼接在一起,并将其作为js模块的导出内容。
// css-loader源码(经简化)
// https://github.com/webpack-contrib/css-loader/blob/master/src/index.js
import postcss from 'postcss';
module.exports = async function (content, map, meta) {
const options = this.getOptions(); // 获取配置
const plugins = []; // 转译源码所需的postcss插件
shouldUseModulesPlugins(options, this) && plugins.push(modulesPlugins); // 处理css-modules
shouldUseImportPlugin(options, this) && plugins.push(importPlugin); // 处理@import语句
shouldUseURLPlugin(options, this) && plugins.push(urlPlugin); // 处理url()语句
shouldUseIcssPlugin(options, this) && plugins.push(icssPlugin); // 处理icss相关逻辑
if (meta && meta.ast) { // 复用前面loader生成的CSS AST(如postcss-loader)
content = meta.ast.root;
}
const result = await postcss(plugins).process(content); // 使用postcss转译源码
const importCode = getImportCode(); // 需要导入的依赖语句
const moduleCode = getModuleCode(result); // 模块导出内容
const exportCode = getExportCode(); // 其他需要导出的信息,如css-modules的类名映射等
const callback = this.async(); // 异步返回
callback(null, `${importCode}${moduleCode}${exportCode}`);
};
style-loader解析:
经过css-loader的转译,我们已经得到了完整的css样式代码,style-loader的作用就是将结果以style标签的方式插入DOM树中。
// style-loader
import loaderUtils from 'loader-utils';
module.exports = function (content) {
// do nothing
};
module.exports.pitch = function (remainingRequest) {
/*
* 用require语句获取css-loader返回的js模块的导出
* 用'!!'前缀跳过配置中的loader,避免重复执行
* 用remainingRequest参数获取loader链的剩余部分,在本例中是css-loader、less-loader
* 用loaderUtils的stringifyRequest方法将request语句中的绝对路径转为相对路径
*/
const requestPath = loaderUtils.stringifyRequest(this, '!!' + remainingRequest);
// 本例中requestPath为:
// '!!../node_modules/css-loader/index.js!../node_modules/less-loader/dist/cjs.js!src/styles/index.less'
return `
const content = require(${requestPath})
const style = document.createElement('style');
style.innerHTML = content;
document.head.appendChild(style);
`;
};
关于postcss:github.com/postcss/pos…