自定义loader的配置:
- 可以先在resolveLoader中指定路径,然后直接使用
- 也可以配置modules,默认在node_modules下找loader,如果找不到,可以指定查找路径, 这样也是可以的
{
resolveLoader: {
alias: {
loader1: path.resolve(__dirname, "loaders", "loader1.js")
},
modules: ["node_modules", path.resolve(__dirname, "loaders")]
},
module: {
rules: [
{
test: /\.js$/,
use: "loader1"
}
]
}
}
配置多个loader
- loader的执行顺序: 从右到左,从下到上
- loader的分类:enforce:"pre" 在前面的; enforce:"post" 在后面; normal
- loader的执行顺序:pre => normal => inline => post
- inline-loader: let str = require("loader4!./a.js")
- let str = require("-!loader4!./a.js")
- -!不会让文件再去通过pre+normal loader来处理
- 前面加! 没有normal
- 前面加 !! 什么都没有,只要行内loader来处理
- use 配置成数组
rules: [
{
test: /\.js$/,
use: ["loader3","loader2","loader1"]
}
]
rules: [
{
test: /\.js$/,
use: "loader3"
},
{
test: /\.js$/,
use: "loader2"
}
{
test: /\.js$/,
use: "loader1"
}
]
pitchLoader和normalLoader
- loader的特点
- 第一个loader要返回js脚本【(最后执行的loader), 因为要将返回的内容放到eval函数中执行】
- 每一个loader只做一件内容,为了使loader在更多场景链式调用
- 每一个loader都是一个模块
- 每个loader都是无状态的,确保loader在不同模块之间不保存状态
function loader1(resource) {
console.log("loader1");
return resource;
}
loader1.pitch = function() {
console.log("loader1-pitch");
}
module.exports = loader1;
use: ["loader3","loader2", "loader1"]
当pitchloader没有返回值的时候:执行的顺序是:
loader3-pitch => loader2-pitch => loader1-pitch => loader1 => loader2 => loader3
当pitchloader有返回值的时候,会跳过后面loader-pitch的执行和loader(包括自己的loader)的执行
假设loader2-pitch有返回值,执行顺序会变成:
loader3-pitch => lodaer2-pitch => loader3
babel-loader的实现
- 先安装babel相关插件:@babel/core @babel/preset-env
let babel = require("@babel/core");
let loaderUtils = require("loader-utils");
function babelLoader(resource) {
console.log("babel loader");
let options = loaderUtils.getOptions(this);
const callback = this.async();
babel.transform(resource, {
...options,
sourceMaps: true,
filename: this.resourcePath.split("/").pop(),
}, function(err, result) {
callback(err, result.code, result.map);
})
return resource;
}
module.exports = babelLoader;
banner-loader的实现
let loaderUtils = require("loader-utils");
let {validate :validateOptions} = require("schema-utils");
let fs = require("fs");
function bannerLoader(resource) {
let options = loaderUtils.getOptions(this);
let callback = this.async();
let schema = {
type: "object",
properties: {
text: {
type: "string"
},
filename: {
type: "string"
}
}
};
validateOptions(schema,options, "banner-loader");
if(options.filename) {
this.addDependency(options.filename);
fs.readFile(options.filename, "utf8", (err,data) => {
callback(err, `/**${data}**/${resource}`)
})
}else{
callback(null, `/**${options.text}**/${resource}`)
}
}
module.exports = bannerLoader;
file-loader的实现
let loaderUtils = require("loader-utils");
function fileLoader(resource) {
let filename = loaderUtils.interpolateName(this, '[hash].[ext]', {content: resource});
this.emitFile(filename, resource);
return `module.exports="${filename}"`;
}
fileLoader.raw = true;
module.exports = fileLoader;
url-loader的实现
let loaderUtils = require("loader-utils");
let mime = require("mime");
function urlLoader(resource) {
let options = loaderUtils.getOptions(this);
let limit = options.limit;
if(limit&& limit > resource.length) {
return `module.exports="data:${mime.getType(this.resourcePath)};base64,${resource.toString("base64")}"`
}else{
return require("./file-loader").call(this, resource);
}
}
urlLoader.raw = true;
module.exports = urlLoader;
less-loader的实现
let less = require("less");
function lessloader(source) {
let css = "";
less.render(source, function(err, c) {
css = c.css;
});
return css;
}
module.exports = lessloader;
style-loader的实现
function styleloader(source) {
let style = `
let style = document.createElement("style");
style.innerHTML=${JSON.stringify(source)};
document.head.appendChild(style);
`
return style;
}
module.exports = styleloader;
css-loader
function cssLoader(resource) {
let reg=/url\((.+?)\)/g;
let pos = 0;
let current;
let arr = ["let list = []"]
while(current = reg.exec(resource)) {
console.log(current, 123123123)
let [matchUrl, g] = current;
let last = reg.lastIndex - matchUrl.length;
arr.push(`list.push(${JSON.stringify(resource.slice(pos, last))})`)
pos = reg.lastIndex;
arr.push(`list.push('url('+require(${g})+')')`);
}
arr.push(`list.push(${JSON.stringify(resource.slice(pos))})`)
arr.push(`module.exports=list.join(" ")`)
return arr.join("\r\n");
}
module.exports = cssLoader;
let loaderUtils = require("loader-utils");
function loader(source) {
let style = `
let style = document.createElement("style");
style.innerHTML=${JSON.stringify(source)};
document.head.appendChild(style);
`
return style;
}
loader.pitch = function(remainingRequest) {
let style = `
let style = document.createElement("style");
style.innerHTML=require(${loaderUtils.stringifyRequest(this,"!!" + remainingRequest)});
document.head.appendChild(style);
`
return style;
}
module.exports = loader;
对于加载css文件,配置一般都是 style-loader css-loader
即css代码会先被css-loader处理一次,然后交给style-loader进行处理
css-loader的作用是处理css中的@import和url这样的外部资源
style-loader的作用是把样式插入到DOM中,方法是在head中插入一个style标签,并把样式写入到这个标签的innerHTML
pitch方法:
默认的loader都是从右向左执行,用pitching loader可以从左向右执行
为什么要使用pitching loader?
因为要把css文件的内容插入dom,所以要获取css样式,如果使用css-loader,他返回的结果是一段js字符串
这样就取不到css样式了。
为了获取css样式,我们会在style-loader中直接通过require来获取,这样返回的js就不是字符串而是一段代码,
同样的字符串,在默认模式下是当成字符串传入的,在pitching模式下是当代码运行的,这就是区别
也就是说,我们处理css的时候,其实是style-loader先执行了,他里面会调用css-loader来拿到css的内容,拿到的内容
当然是经过css-loader编译过的
在style-loader的pitch方法有返回值时,剩余的css-loader的pitch方法、css-loader的normal方法
以及style-loader的normal方法都不会执行了。而style-loader的pitch方法里面调用了require(!!.../x.css)
这就会把require的css文件当作新的入口文件,重新链式调用 剩余的loader来进行处理
值得注意的是 !!是一个标志,表示不会再重复递归调用style-loader,而只会调用css-loader处理了
loader-runner
- loader-runner 是一个执行loader链条的模块
- 是webpack为了执行loader开发的一个模块
const {runLoaders} = require("loader-runner");
const path = require("path");
const fs = require("fs");
const {runLoaders} = require("loader-runner");
const entry = path.resolve(__dirname, 'src', 'index.js');
let request = `inline1-loader!inline2-loader!${entry}`;
let rules = [
{
test: /\.js$/,
use: ['normal1-loader', 'normal2-loader']
},
{
enforce: "pre",
test: /\.js$/,
use: ['pre1-loader', 'pre2-loader']
},
{
enforce: "post",
test: /\.js$/,
use: ['post1-loader', 'post2-loader']
}
];
let parts = request.split("!");
let resource = parts.pop();
const resolveLoader = loader => path.resolve(__dirname, 'loaders', loader);
let inlineLoaders = parts;
let preLoaders = [];
let normalLoaders = [];
let postLoaders = [];
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);
}
}
}
let loaders = [];
if(request.startsWith("!!")) {
loaders = inlineLoaders;
}else if(request.startsWith("-!")) {
loaders = [...postLoaders, ...inlineLoaders];
}else if(request.startsWith("!")) {
loaders = [...postLoaders, ...inlineLoaders, ...preLoaders];
}else {
loaders = [...postLoaders, ...inlineLoaders, ...normalLoaders, ...preLoaders];
}
loaders = loaders.map(resolveLoader);
runLoaders({
resource,
loaders,
context: {name: "zhuzhu"},
readResource: fs.readFile.bind(fs),
}, (err, result) => {
console.log(err);
console.log(result);
console.log(result.resourceBuffer.toString("utf8"));
})
babel-loader
const babel = require("@babel/core");
const path = require("path");
function babelLoader(inputSource) {
const { getOptions } = require("loader-utils");
let options = getOptions(this) || {};
options.filename = path.basename(this.resourcePath);
let {code, map, ast} = babel.transform(inputSource, options);
return this.callback(null, code, map, ast);
}
module.exports = babelLoader;
file-loader
- file-loader并不会对文件内容进行任何转换,只是复制一份文件内容,并根据配置为他生成唯一的文件名
- 通过
loaderUtils.interpolateName方法可以根据options.name以及文件内容生成一个唯一的文件名url(一般配置都会带上hash,否则很可能由于文件重名而冲突)
- 通过
this.emitFile(url,content)告诉webpack需要创建一个文件,webpack会根据参数创建对应的文件,放在public path目录下
- 返回
module.exports=${JSON.stringify(url)}
const { getOptions, interpolateName} = require("loader-utils");
function fileLoader(content) {
let options = getOptions(this);
let url = interpolateName(this, options.filename || "[hash].[ext]",{content});
this.emitFile(url, content);
return `module.exports=${JSON.stringify(url)}`
}
fileLoader.raw = true;
module.exports = fileLoader;
url-loader
const { getOptions } = require("loader-utils");
const mime = require("mime");
function urlLoader(source) {
const 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)
}
}
urlLoader.raw = true;
module.exports = urlLoader;
less-loader
let less = require("less");
function loader(inputSource) {
let callback = this.async();
less.render(inputSource, { filename: this.resource}, (err, output) => {
callback(null, output.css);
})
}
module.exports = loader;
- less-loader处理完成后返回的是一段css脚本,说明less-loader不能放在最左侧。因为最左侧loader的返回值要给到webpack,webpack只能接收js,不是js时webpack处理不了
style-loader
function styleLoader(inputSource) {
let script = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(inputSource)};
document.head.appendChild(style);
`;
return script;
}
module.exports = styleLoader;
源码中less-loader的实现
- 返回
module.exports=${JSON.stringify(output.css)}
let less = require("less");
function loader(inputSource) {
let callback = this.async();
less.render(inputSource, { filename: this.resource}, (err, output) => {
callback(null, `module.exports=${JSON.stringify(output.css)}`);
})
}
module.exports = loader;
源码中style-loader的实现
- pitch取代了当前loader,并加载了之后的loader的返回值
let loaderUtils = require('loader-utils');
function styleLoader(inputSource) {
}
styleLoader.pitch = function(remainingRequest, previousRequest, data) {
let script = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this, "!!"+remainingRequest)});
document.head.appendChild(style);
`
return script;
}
module.exports = styleLoader;
loader-runner的实现
function createLoaderObject(loader) {
let loaderObject = {
path: loader,
normal: null,
pitch: null,
raw: false,
data: {},
pitchExecuted: false,
normalExecuted: false,
};
return loaderObject;
}
function processResource(options, loaderContext, runLoadersCallback){
options.readResource(loaderContext.resource, (err, buffer) => {
loaderContext.loaderIndex = loaderContext.loaders.length - 1;
options.resourceBuffer = buffer;
iterateNormalLoaders(options, loaderContext, [buffer], runLoadersCallback)
})
}
function convertArgs(args, raw) {
if(raw && !Buffer.isBuffer(args[0])) {
args[0] = Buffer.from(args[0]);
}else if(!raw && Buffer.isBuffer(args[0])) {
args[0] = args[0].toString('utf8')
}
}
function iterateNormalLoaders(options, loaderContext, [buffer], runLoadersCallback) {
if(loaderContext.loaderIndex <0) {
return runLoadersCallback(null, ...args);
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if(currentLoaderObject.normalExecuted) {
loaderContext.loaderIndex--;
return iterateNormalLoaders(options, loaderContext, runLoadersCallback)
}
loadLoader(currentLoaderObject, () => {
let fn = currentLoaderObject.normal;
currentLoaderObject.normalExecuted = true;
if(!fn) {
return iterateNormalLoaders(options, loaderContext, runLoadersCallback)
}
convertArgs(args, currentLoaderObject.raw);
runSyncOrAsync(fn, loaderContext, args, (err, ...args) => {
if(err) return runLoadersCallback(err);
return iterateNormalLoaders(options, loaderContext, args, runLoadersCallback)
})
});
}
function iteratePitchLoaders(options, loaderContext, runLoadersCallback) {
if(loaderContext.loaderIndex >= loaderContext.loaders.length) {
return processResource(options, loaderContext, runLoadersCallback)
}
let currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
if(currentLoaderObject.pitchExecuted) {
loaderContext.loaderIndex++;
return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
}
loadLoader(currentLoaderObject, () => {
let fn = currentLoaderObject.pitch;
currentLoaderObject.pitchExecuted = true;
if(!fn) {
return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
}
runSyncOrAsync(fn, loaderContext, [
loaderContext.remainingRequest,
loaderContext.previousRequest,
loaderContext.data
], (err, ...args) => {
if(args.length > 0 && args.some(item => !!item)){
}else{
return iteratePitchLoaders(options, loaderContext, runLoadersCallback)
}
})
});
}
function runSyncOrAsync(fn, context, args, callback) {
let isSync = true;
let isDone = false;
let innerCallback = context.callback = (...args) => {
isDone = true;
callback(null, ...args);
}
context.async = () => {
isSync = false;
return innerCallback
}
var result = fn.apply(context, args);
if(isSync) {
isDone = true;
if(result === undefined) {
return callback();
}else{
return callback(null, result);
}
}
}
function loadLoader(loaderObject, callback) {
var module = require(loaderObject.path);
loaderObject.normal = module.normal;
loaderObject.pitch = module.pitch;
loaderObject.raw = module.raw;
callback();
}
function runLoader(options, finalCallback) {
let resource = options.resource;
let loaders = options.loaders || [];
let loaderContext = options.context || {};
let readResource = options.readResource || fs.readFile;
let loaderObjects = loaders.map(createLoaderObject);
loaderContext.resource = resource;
loaderContext.readResource = readResource;
loaderContext.loaders = loaderObjects;
loaderContext.loaderIndex = 0;
loaderContext.callback = null;
loaderContext.async = null;
Object.defineProperty(loaderContext, 'request', {
get() {
return loaderContext.loaders.map(l => l.path).concat(loaderContext.resource).join("!");
}
});
Object.defineProperty(loaderContext, 'remainingRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(i => l.path).concat("!");
}
});
Object.defineProperty(loaderContext, 'currentRequest', {
get() {
return loaderContext.loaders.slice(loaderContext.loaderIndex).map(i => l.path).concat("!");
}
});
Object.defineProperty(loaderContext, 'previousRequest', {
get() {
return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(i => l.path).concat("!");
}
});
Object.defineProperty(loaderContext, 'data', {
get() {
return loaderContext.loaders[loaderContext.loaderIndex].data
}
});
let processOptions = {
resourceBuffer:null,
readResource
}
iteratePitchLoaders(processOptions, loaderContext, (err, result) => {
finalCallback(
err,{
result,
resourceBuffer: processOptions.resourceBuffer
}
)
})
}