loader执行流程
针对每个文件,从config文件中收集起来所有的loader之后,执行过程可以概括为以下几步:
1、先将loader进行分类,其中inlineLoaders直接从文件require或import的路径中取到
2、require或import时,可以在引用路径前加上前缀,来控制使用哪些loader
3、将loader名称转换为绝对路径
4、执行所有loader的pitch方法
5、执行所有loader的normal方法
loader分类
//loader 的叠加顺序 = post(后置)+inline(内联)+normal(正常)+pre(前置)
// 厚 脸 挣 钱
//写的是一样的
//执行的时候 是从右向左执行的
pre=>normal=>inline=>post
pre、normal、inline、post可以在enforce中配置,默认是normal:
module.exports = {
// ...
resolveLoader: {
//配置别名
alias: {
'inline1-loader': path.resolve(__dirname, 'loaders', 'inline1-loader.js'),
},
//配置去哪些目录里找loader
modules: ['node_modules', path.resolve(__dirname, 'loaders')]
},
module: {
rules: [
{
test: /.js$/,
enforce: 'post',
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-env"],
plugins: []
}
}
},
上面代码中,可以看到,我们能在webpack.config.js的resolveLoader中配置loader路径的别名
这里实际上有两种配置方式:
1、只配置alias,因为通过alias里面的绝对路径可以准确定位loader的位置
2、只配置modules,webpack在加载loader的时候可以从modules取出对应的目录挨个遍历取值
\
inlineLoaders、preLoaders、postLoaders、normalLoaders 4个数组中分别存储对应的loaders
const { runLoaders } = require('loader-runner');
const path = require('path');
const fs = require('fs');
//入口文件
const entryFile = path.resolve(__dirname, 'src', 'title.js');
//loader的转换规则配置
let rules = [
{
test: /title.js$/,
use: ['normal1-loader.js', 'normal2-loader.js']
},
{
test: /title.js$/,
enforce: 'post',
use: ['post1-loader.js', 'post2-loader.js']
},
{
test: /title.js$/,
enforce: 'pre',
use: ['pre1-loader.js', 'pre2-loader.js']
}
]
//手写style-loader的时候使用到
let request = `inline1-loader!inline2-loader!${entryFile}`;
let parts = request.replace(/^-?!+/, '').split('!');//['inline1-loader','inline2-loader',entryFile]
let resource = parts.pop();//entryFile
const inlineLoaders = parts;//['inline1-loader','inline2-loader']
const preLoaders = [], postLoaders = [], normalLoaders = [];
rules.forEach(rule => {
//if (rule.test.test(resource)) {
if (resource.match(rule.test)) {
if (rule.enforce === 'pre') {
preLoaders.push(...rule.use);
} else if (rule.enforce === 'post') {
postLoaders.push(...rule.use);
} else {
normalLoaders.push(...rule.use);
}
}
})
通过增加前缀来控制使用的loader
/**
* -! noPreAutoLoaders 不要前置和普通loader
* ! noAutoLoaders 不要普通loader
* !! noPrePostAutoLoaders 不要前置、后置、普通loader,只要内联
*/
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];
}
将loader名称转换为绝对路径
//用于把loader的名称转变成一个绝对路径
const resolveLoader = loader => path.resolve(__dirname, 'runner', loader);
loaders = loaders.map(resolveLoader);
执行所有loader的pitch方法
如果pitch有返回值,则不会再去取文件内容,接下来会直接执行上一个loader的normal方法
关于pitch需要注意的点:
- 比如 a!b!c!module, 正常调用顺序应该是 c、b、a,但是真正调用顺序是 a(pitch)、b(pitch)、c(pitch)、c、b、a,如果其中任何一个 pitching loader 返回了值就相当于在它以及它右边的 loader 已经执行完毕
- 比如如果 b 返回了字符串"result b", 接下来只有 a 会被系统执行,且 a 的 loader 收到的参数是 result b
- loader 根据返回值可以分为两种,一种是返回 js 代码(一个 module 的代码,含有类似 module.export 语句)的 loader,还有不能作为最左边 loader 的其他 loader(这种loader其实就是 '返回的字符串中没有模块化规范相关的js代码' 的模块)
- 有时候我们想把两个第一种 loader chain 起来,比如 style-loader!css-loader! 问题是 css-loader 的返回值是一串 js 代码,如果按正常方式写 style-loader 的参数就是一串代码字符串
- 为了解决这种问题,我们需要在 style-loader 里执行 require(css-loader!resources)
执行所有loader的normal方法
runLoaders({
resource,//要加载和转换的模块
loaders,//是一个绝对路径的loader数组
context: { name: 'zhufeng' },//loader的上下文对象
readResource: fs.readFile.bind(fs)//读取硬盘上资源的方法
}, (err, result) => {
console.log(err);//运行错误
console.log(result);//转换后的结果
//resourceBuffer 是buffer格式的源代码的内容,如果是pitch返回的,没有读取源文件,那么它就是null
if (result.resourceBuffer) {
console.log(result.resourceBuffer.toString('utf8'));//最初始的转换前的源文件内容
}
});
loader中的pitch可有可无,但normal必须要有
loader可以返回一个值 ,也可以返回多个值
返回一个值的话可以return 返回多个值的话,必须 this.callback(err,传递给下一个loader的参数)
// loader接收1个参数时的情况:
function loader(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);
}
// loader接收多个参数时的情况:
function loader(code,map,ast){
const source = 'xxxx'
// xxxxx
// 传给下一个loader的参数是source
return this.callback(source);
}
pitch和normal都在runLoaders方法中进行
loader中的this,默认是调用runLoaders时传的context:
runLoaders({
resource,//将要加载和转换的模块路径
loaders,//使用哪些loader来进行转换 8
context:{name:'zhufeng'},//上下文对象 一般来说没有用
readResource:fs.readFile//你可以自定义读取文件的方法
},(err,result)=>{
console.log(err);
console.log(result);
//console.log(result.resourceBuffer.toString('utf8'));
});
但是在loader-runner执行的过会给context增加很多的方法和属性
\
babel-loader使用及实现:
如果希望代码经过babel转换之后,调试时还可定位到自己写的源码中,需要加上下面的options的sourceMaps
{
test:/.js$/,
use:[{
loader:'babel-loader',
options:{
presets:["@babel/preset-env"],
//如果这个参数不传,默认值false,不会生成sourceMap
sourceMaps:true,
// filename是sourceMap映射过后在浏览器控制台的Source里面出现的文件名
// 这个文件名通常会在插件内部填上,因为每个文件都有不同的名字
// filename:'xxx'
}
}]
},
babel-loader的实现:
const babel = require('@babel/core');
/**
* babel-loader只是一个转换JS源代码的函数
* @param {*} source 接收一个source参数
* 返回一个新的内容
*/
function loader(source) {
let options = this.getOptions({})
let { code } = babel.transform(source, options);
return code;//转换成ES5的内容
}
module.exports = loader;
/**
* babel-loader
* @babel/core 真正要转换代码从ES6到ES5需要靠 @babel/core
* babel/core本身只能提供从源代码转成语法树,遍历语法树,从新的语法树重新生成源代码的功能
* babel plugin
* 转换箭头函数的插件 plugin-babel-transform-arrow-functions
* 插件知识如何转换语法树
* babel preset
* 单个配置插件太多太繁琐,所以可以把常用的插件打个包,起个名字进行配置比较方便
*
* 插件 保存的时候 reuqire => require
*/
file-loader使用及实现:
默认情况下loader接收的参数是字符串类型,即上一个loader传给当前loader的内容,或者是源文件的内容
如果用file-loader将图片从src目录拷贝到dist目录中,希望loader接收的参数是Buffer类型的,可以给loader加一个静态属性loader.raw = true
{
test:/.(jpg|gif|png)$/,
use:[
{
loader:'file-loader',
options:{
filename:'[hash].[ext]'
}
}
]
},
file-loader的内容:
const { getOptions,interpolateName } = require("loader-utils");
function loader(content){
let options = getOptions(this)||{};//{filename}
//this=loaderContext filename=文件名生成模板[hash].[ext] content是文件的内容
let filename = interpolateName(this,options.filename,{content});
//向输出目录里输出一个文件
//loaderRunner给的一个方法
this.emitFile(filename,content);// compilation.assets[filename]=content;
//最后一个loader肯定要返回一个JS模块代码,导出一个值,这个值将会成为此模块的导出结果
return `module.exports=${JSON.stringify(filename)}`;
}
loader.raw =true;
module.exports = loader;
url-loader使用及实现:
和file-loader相比,url-loader多了limit参数
{
test:/.(jpg|gif|png)$/,
use:[
{
loader:'url-loader',
options:{
filename:'[hash].[ext]',
limit:800*1024,
fallback:path.resolve('./loaders/file-loader.js')
}
}
]
},
url-loader:
const { getOptions,interpolateName } = require("loader-utils");
const mime = require('mime');
/**
*
* @param {*} content 上一个loader传给当前loader的内容,或者是源文件的内容
* content默认是字符串 先把content转换字符串给loader,
* 如果你希望得到Buffer,不希望转成字符串传递给你
*/
function loader(content){
let options = getOptions(this)||{};//{filename}
let {limit,fallback} = options;
if(limit)
limit = parseInt(limit,10);
const mimeType = mime.getType(this.resourcePath);
if(!limit || Buffer.byteLength(content) < limit){
let base64 = `data:${mimeType};base64,${content.toString('base64')}`;
return `module.exports=${JSON.stringify(base64)}`
}else{
//require('file-loader');//会去node_modules里找 require跟webpack没关系,走的原生的node模块查找逻辑
return require(fallback).call(this,content);
}
}
loader.raw =true;
module.exports = loader;
注意:webpack5以后,对图片类文件资源的处理,将不再使用loader,而走了asset/resource的配置:
module.exports = {
mode: 'development',
devtool: false,
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
publicPath: '/'
},
module: {
rules: [
{ test: /.txt$/, use: 'raw-loader' },
{ test: /.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
{ test: /.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] },
{ test: /.scss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
{
test: /.(jpg|png|gif|bmp|svg)$/,
type:'asset/resource',
generator:{
filename:'images/[hash][ext]'
}
}
]
},
less-loader实现:
默认情况下loader的执行是同步的,如果调用了async方法,可以把loader的执行变成异步
也可以直接调用this.callback实现异步
{
test:/.less$/,
use:[
'style-loader',//生成一段JS脚本,向页面插入style标签,style的内容就是css文本
'less-loader'//把less编译成css
]
}
let less = require('less');
/**
* 希望这个loader可以放在最左侧
* @param {*} inputSource
* 传入的参籹
* 如果是最后在的或者说最右边的loader,参数就是模块的内容
* 如果不是最后一个,参数就是上一个loader返回的内容
*/
function loader(inputSource) {
console.log('less-loader');
//默认情况下loader的执行是同步的,如果调用了async方法,可以把loader的执行变成异步
let callback = this.async();
//this.callback
//写死的,就是同步
less.render(inputSource, { filename: this.resource }, (err, output) => {
// 如果直接把output.css返回的话,就不是模块化代码了,在webpack的配置文件中,
// 就不能写在rules.use数组中的最左侧了
// callback(null, output.css);
// 因此在实际的源码中less-loader其实返回的是一段JS脚本,来保证它可以放在最左侧
callback(null, `module.exports = ${JSON.stringify(output.css)}`);
});
}
module.exports = loader;
style-loader实现:
缩水版:
function loader(inputSource){
let script = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(inputSource)};
document.head.appendChild(style);
`;
return script;
}
module.exports = loader
完整版:
let loaderUtils = require('loader-utils');
/**
* @param {*} inputSource less-loader编译后的CSS内容
* inputSource `module.exports = "#root{color:red}"`
*/
function loader(inputSource){
/* let script = `
let style = document.createElement('style');
style.innerHTML = ${JSON.stringify(inputSource)};
document.head.appendChild(style);
`;
return script; */
}
/**
* 如果pitch函数有返回值,不需要于执行后续的loader和读文件了
* @param {*} remainingRequest
* @param {*} previousRequest
* @param {*} data
* @returns
*/
loader.pitch = function(remainingRequest,previousRequest,data){
console.log('remainingRequest',remainingRequest);//less-loader.js!index.less
console.log('previousRequest',previousRequest);//""
console.log('data',data);//{}
let script = `
let style = document.createElement('style');
style.innerHTML = require(${loaderUtils.stringifyRequest(this,"!!"+remainingRequest)});
document.head.appendChild(style);
`;
//这个返回的JS脚本给了webpack了
//把这个JS脚本转成AST抽象语法树,分析里的require依赖
//
return script;
}
module.exports = loader;
/**
* [style-loader,less-loader]
* request = style-loader!less-loader!index.less
* previousRequest = ''
* remainingRequest=less-loader!index.less
* !!less-loader!index.less
* stringifyRequest 把绝对路径转成相对路径
* remainingRequest C:\less-loader.js!C:\index.less
* 相对于根目录的路径 类似于此模块的ID
* !!./loaders/less-loader.js!./src/index.less
* loader执行完后会把下面的代码给webpack
* let style = document.createElement('style');
style.innerHTML = require("!!./loaders/less-loader.js!./src/index.less");
document.head.appendChild(style);
webpack会去分析依赖
!!的前缀代表只要行内或者说是内联,不要前置后置和普通
*
*
*/