Compiler.js
中会为将用户配置与默认配置合并,其中就包括了loader
部分NormalModuleFactory
,它可以用来创建NormalModule
build
方法来进行模块的构建。构建模块的第一步就是使用loader
来加载并处理模块内容。而loader-runner
这个库就是webpack
中loade
r的运行器属性 | 值 |
---|---|
this.request | /loaders/babel-loader.js!/src/index.js |
this.userRequest | /src/index.js |
this.rawRequest | ./src/index.js |
this.resourcePath | /src/index.js |
$ cnpm i @babel/preset-env @babel/core -D
const babel = require("@babel/core");
function loader(source,inputSourceMap) {
//C:\webpack-analysis2\loaders\babel-loader.js!C:\webpack-analysis2\src\index.js
const options = {
presets: ['@babel/preset-env'],
inputSourceMap:inputSourceMap,
sourceMaps: true,//ourceMaps: true 是告诉 babel 要生成 sourcemap
filename:this.request.split('!')[1].split('/').pop()
}
//在webpack.config.js中 增加devtool: 'eval-source-map'
let {code,map,ast}=babel.transform(source,options);
return this.callback(null,code,map,ast);
}
module.exports = loader;
resolveLoader: {
alias: {//可以配置别名
"babel-loader": resolve('./build/babel-loader.js')
},//也可以配置loaders加载目录
modules: [path.resolve('./loaders'), 'node_modules']
},
{
test: /\.js$/,
use:['babel-loader']
}
pitch与loader本身方法的执行顺序图
|- a-loader `pitch`
|- b-loader `pitch`
|- c-loader `pitch`
|- requested module is picked up as a dependency
|- c-loader normal execution
|- b-loader normal execution
|- a-loader normal execution
loaders\loader1.js
function loader(source) {
console.log('loader1',this.data);
return source+"//loader1";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
data.name = 'pitch1';
console.log('pitch1');
}
module.exports = loader;
loaders\loader2.js
function loader(source) {
console.log('loader2');
return source+"//loader2";
}
loader.pitch = function (remainingRequest,previousRequest,data) {
console.log('remainingRequest=',remainingRequest);
console.log('previousRequest=',previousRequest);
console.log('pitch2');
//return 'console.log("pitch2")';
}
module.exports = loader;
loaders\loader3.js
function loader(source) {
console.log('loader3');
return source+"//loader3";
}
loader.pitch = function () {
console.log('pitch3');
}
module.exports = loader;
{
test: /\.js$/,
use: ['loader1', 'loader2', 'loader3']
}
符号 | 变量 | 含义 | |
---|---|---|---|
-! |
noPreAutoLoaders | 不要前置和普通loader | Prefixing with -! will disable all configured preLoaders and loaders but not postLoaders |
! |
noAutoLoaders | 不要普通loader | Prefixing with ! will disable all configured normal loaders |
!! |
noPrePostAutoLoaders | 不要前后置和普通loader,只要内联loader | Prefixing with !! will disable all configured loaders (preLoaders, loaders, postLoaders) |
let path = require("path");
let nodeModules = path.resolve(__dirname, "node_modules");
let request = "-!inline-loader1!inline-loader2!./styles.css";
//首先解析出所需要的 loader,这种 loader 为内联的 loader
let inlineLoaders = request
.replace(/^-?!+/, "")
.replace(/!!+/g, "!")
.split("!");
let resource = inlineLoaders.pop();//// 获取资源的路径
let resolveLoader = loader => path.resolve(nodeModules, loader);
//从相对路径变成绝对路径
inlineLoaders = inlineLoaders.map(resolveLoader);
let rules = [
{
enforce: "pre",
test: /\.css?$/,
use: ["pre-loader1", "pre-loader2"]
},
{
test: /\.css?$/,
use: ["normal-loader1", "normal-loader2"]
},
{
enforce: "post",
test: /\.css?$/,
use: ["post-loader1", "post-loader2"]
}
];
let preLoaders = [];
let postLoaders = [];
let normalLoaders = [];
for(let i=0;i<rules.length;i++){
let rule = rules[i];
if(rule.test.test(resource)){
if(rule.enforce=='pre'){
preLoaders.push(...rule.use);
}else if(rule.enforce=='post'){
postLoaders.push(...rule.use);
}else{
normalLoaders.push(...rule.use);
}
}
}
preLoaders = preLoaders.map(resolveLoader);
postLoaders= postLoaders.map(resolveLoader);
normalLoaders = normalLoaders.map(resolveLoader);
let loaders = [];
//noPrePostAutoLoaders 忽略所有的 preLoader / normalLoader / postLoader
if(request.startsWith('!!')){
loaders = inlineLoaders;//只保留inline
//noPreAutoLoaders 是否忽略 preLoader 以及 normalLoader
}else if(request.startsWith('-!')){
loaders = [...postLoaders,...inlineLoaders];//只保留post和inline
//是否忽略 normalLoader
}else if(request.startsWith('!')){
loaders = [...postLoaders,...inlineLoaders,...preLoaders];//保留post inline pre
}else{
loaders = [...postLoaders,...inlineLoaders,...normalLoaders,...preLoaders];
}
console.log(loaders);
let readFile = require("fs");
let path = require("path");
function createLoaderObject(loader) {
let obj = { data: {} };
obj.request = loader;
obj.normal = require(loader);
obj.pitch = obj.normal.pitch;
return obj;
}
function runLoaders(options, callback) {
let loaderContext = {};
let resource = options.resource;
let loaders = options.loaders;
loaders = loaders.map(createLoaderObject);
loaderContext.loaderIndex = 0;
loaderContext.readResource = readFile;
loaderContext.resource = resource;
loaderContext.loaders = loaders;
let isSync = true;
var innerCallback = (loaderContext.callback = function(err, args) {
loaderContext.loaderIndex--;
iterateNormalLoaders(loaderContext, args, callback);
});
loaderContext.async = function async() {
isSync = false;
return innerCallback;
};
Object.defineProperty(loaderContext, "request", {
get: function() {
return loaderContext.loaders
.map(function(o) {
return o.request;
})
.concat(loaderContext.resource)
.join("!");
}
});
Object.defineProperty(loaderContext, "remainingRequest", {
get: function() {
return loaderContext.loaders
.slice(loaderContext.loaderIndex + 1)
.map(function(o) {
return o.request;
})
.concat(loaderContext.resource || "")
.join("!");
}
});
Object.defineProperty(loaderContext, "currentRequest", {
enumerable: true,
get: function() {
return loaderContext.loaders
.slice(loaderContext.loaderIndex)
.map(function(o) {
return o.request;
})
.concat(loaderContext.resource || "")
.join("!");
}
});
Object.defineProperty(loaderContext, "previousRequest", {
get: function() {
return loaderContext.loaders
.slice(0, loaderContext.loaderIndex)
.map(function(o) {
return o.request;
})
.join("!");
}
});
Object.defineProperty(loaderContext, "data", {
get: function() {
return loaderContext.loaders[loaderContext.loaderIndex].data;
}
});
iteratePitchingLoaders(loaderContext, callback);
function iteratePitchingLoaders(loaderContext, callback) {
if (loaderContext.loaderIndex >= loaderContext.loaders.length) {
loaderContext.loaderIndex--;
return processResource(loaderContext, callback);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
let fn = currentLoaderObject.pitch;
if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);
let args = fn.apply(loaderContext, [
loaderContext.remainingRequest,
loaderContext.previousRequest,
currentLoaderObject.data
]);
if (args) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(loaderContext, args, callback);
} else {
loaderContext.loaderIndex++;
iteratePitchingLoaders(loaderContext, callback);
}
function processResource(loaderContext, callback) {
let buffer = loaderContext.readResource.readFileSync(
loaderContext.resource,
"utf8"
);
iterateNormalLoaders(loaderContext, buffer, callback);
}
}
function iterateNormalLoaders(loaderContext, args, callback) {
if (loaderContext.loaderIndex < 0) return callback(null, args);
var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
var fn = currentLoaderObject.normal;
if (!fn) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(loaderContext, args, callback);
}
args = fn.apply(loaderContext, [args]);
if (isSync) {
loaderContext.loaderIndex--;
iterateNormalLoaders(loaderContext, args, callback);
}
}
}
let entry = "./src/world.js";
let options = {
resource: path.join(__dirname, entry),
loaders: [
path.join(__dirname, "loaders/loader1.js"),
path.join(__dirname, "loaders/loader2.js"),
path.join(__dirname, "loaders/loader3.js")
]
};
runLoaders(options, (err, result) => {
console.log(result);
});
file-loader
并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成一个唯一的文件名。const { getOptions, interpolateName } = require('loader-utils');
function loader(content) {
let options=getOptions(this)||{};
let url = interpolateName(this, options.filename || "[hash].[ext]", {content});
this.emitFile(url, content);
return `module.exports = ${JSON.stringify(url)}`;
}
loader.raw = true;
module.exports = loader;
loaderUtils.interpolateName
方法可以根据 options.name 以及文件内容生成一个唯一的文件名 url(一般配置都会带上hash,否则很可能由于文件重名而冲突)this.emitFile(url, content)
告诉 webpack 我需要创建一个文件,webpack会根据参数创建对应的文件,放在 public path
目录下module.exports = ${JSON.stringify(url)}
,这样就会把原来的文件路径替换为编译后的路径let { getOptions } = require('loader-utils');
var mime = require('mime');
function loader(source) {
let options=getOptions(this)||{};
let { limit, fallback='file-loader' } = options;
if (limit) {
limit = parseInt(limit, 10);
}
const mimetype=mime.getType(this.resourcePath);
if (!limit || source.length < limit) {
let base64 = `data:${mimetype};base64,${source.toString('base64')}`;
return `module.exports = ${JSON.stringify(base64)}`;
} else {
let fileLoader = require(fallback || 'file-loader');
return fileLoader.call(this, source);
}
}
loader.raw = true;
module.exports = loader;
$ cnpm i less postcss css-selector-tokenizer -D
src\index.js
import './index.less';
src\index.less
@color:red;
#root{
color:@color;
}
src\index.html
<div id="root">hello</div>
<div class="avatar"></div>
webpack.config.js
{
test: /\.less$/,
use: [
'style-loader',
'less-loader'
]
}
let less = require('less');
function loader(source) {
let callback = this.async();
less.render(source, { filename: this.resource }, (err, output) => {
callback(err, output.css);
});
}
module.exports = loader;
function loader(source) {
let script=(`
let style = document.createElement("style");
style.innerHTML = ${JSON.stringify(source)};
document.head.appendChild(style);
module.exports = "";
`);
return script;
}
module.exports = loader;
let less = require('less');
function loader(source) {
let callback = this.async();
less.render(source, { filename: this.resource }, (err, output) => {
callback(err, `module.exports = ${JSON.stringify(output.css)}`);
});
}
module.exports = loader;
let loaderUtils = require("loader-utils");
function loader(source) {
}
//https://github.com/webpack/webpack/blob/v4.39.3/lib/NormalModuleFactory.js#L339
loader.pitch = function (remainingRequest, previousRequest, data) {
//C:\webpack-analysis2\loaders\less-loader.js!C:\webpack-analysis2\src\index.less
console.log('previousRequest', previousRequest);//之前的路径
//console.log('currentRequest', currentRequest);//当前的路径
console.log('remainingRequest', remainingRequest);//剩下的路径
console.log('data', data);
// !! noPrePostAutoLoaders 不要前后置和普通loader
//__webpack_require__(/*! !../loaders/less-loader.js!./index.less */ "./loaders/less-loader.js!./src/index.less");
let style = `
var style = document.createElement("style");
style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!" + remainingRequest)});
document.head.appendChild(style);
`;
return style;
}
module.exports = loader;
css-loader
的作用是处理css中的 @import
和 url
这样的外部资源src\index.js
require('./style.css');
@import './global.css';
.avatar {
width: 100px;
height: 100px;
background-image: url('./baidu.png');
background-size: cover;
}
div{
color:red;
}
body {
background-color: green;
}
+ {
+ test: /\.css$/,
+ use: [
+ 'style-loader',
+ 'css-loader'
+ ]
+ },
+ {
+ test: /\.png$/,
+ use: [
+ 'file-loader'
+ ]
+ }
loaders\css-loader.js
var postcss = require("postcss");
var loaderUtils = require("loader-utils");
var Tokenizer = require("css-selector-tokenizer");
const cssLoader = function (inputSource) {
const cssPlugin = (options) => {
return (root) => {
root.walkAtRules(/^import$/i, (rule) => {
rule.remove();
options.imports.push(rule.params.slice(1, -1));
});
root.walkDecls((decl) => {
var values = Tokenizer.parseValues(decl.value);
values.nodes.forEach(function (value) {
value.nodes.forEach(item => {
if (item.type === "url") {
item.url = "`+require(" + loaderUtils.stringifyRequest(this, item.url) + ")+`";
}
});
});
decl.value = Tokenizer.stringifyValues(values);
console.log(decl);
});
};
}
let callback = this.async();
let options = { imports: [] };
let pipeline = postcss([cssPlugin(options)]);
pipeline.process(inputSource).then((result) => {
let importCss = options.imports.map(url => "`+require(" + loaderUtils.stringifyRequest(this, "!!css-loader!" + url) + ")+`").join('\r\n');
callback(
null,
'module.exports=`' + importCss + '\n' + result.css + '`'
);
});
};
module.exports = cssLoader;