24.重学webpack——loader的原理及常用loader的实现(高频面试题)

【重学webpack系列——webpack5.0】

1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
从本节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
可以点击上方专栏订阅哦。

以下是本节正文:


loader的原理

1.loader介绍

  • loader其实就是一个到处为函数的js模块,函数的参数接收上一个代码产生的结果或者资源文件。loader可以组合使用。
  • loader的结果是String或Buffer。
  • 所有loader处理后的最终结果会被拿去解析成为ast语法树。(这也是loader的结果需要String或Buffer的原因)

2.loader的类型

2.1 loader分类

loader一般分为四类:

  • post(后置)
  • inline(内联)
  • normal(正常)
  • pre(前置)
2.2 四类loader如何区分,如何配置?

除了inline-loader在业务代码内配置,也就是使用的时候配置。其余均在webpack配置文件中配置。

  • require内的是inline-loader
  • webpack中通过enforce来配置是normal(可以不写,默认值)还是post还是pre
//定在require方法里的 inline Loader
let filecontent = require(`!!inline1-loader!inline2-loader!${filePath}`); //inline-loader是这么写的!!!
//不同的loader并不决定loader的类型属性,而是你在使用 的使用了什么样的enforce
let rules = [
    {
        test: /\.js$/,
        use: ['normal1-loader', 'normal2-loader']//普通的loader
    },
    {
        test: /\.js$/,
        enforce: 'post',
        use: ['post1-loader', 'post2-loader']//post的loader 后置
    },
    {
        test: /\.js$/,
        enforce: 'pre',
        use: ['pre1-loader', 'pre2-loader']//pre的loader 前置 
    },
]
  • 内联loader前面的符号
符号变量含义
-!noPreAutoLoaders不要前置和普通 loader, 这个require的文件不会走前置和普通的loader
!noAutoLoaders不要普通 loader, 这个require的文件不会走普通的loader
!!noPrePostAutoLoaders不要前后置和普通 loader,只要内联 loader, 这个require的文件只会走内联的loader

loader有特定的执行顺序,且是在所以loader开始执行前,先把loader的顺序整理好,然后一个个开始执行的。所以本节2.2中的三个符号,可以过滤掉指定的loader。

2.loader的运行时机

在webpack开始正式编译的时候,会找到入口文件,然后调用loader对文件进行处理,处理的时候会按照一定顺序对loader进行组合,然后一个个执行loader。执行完后会返回处理完的代码,是Buffer或String类型。然后webpack才开始真正的ast语法树解析。

  • 上面所述的"一定顺序"指的是什么?
    • 就是loader的执行顺序,具体看下文。

3. loader的执行顺序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i2XGunCY-1628164544396)(C:\Users\yuhua7\Desktop\loader执行流程.png)]

  • 一般来说loader.pitch是从左往右,normal是从右往左。具体是这样的:

    首先loader能够分成四类:post-loader(后置loader)、inline-loader(内联loader)、normal-loader(普通的loader)和pre-loader(前置loader)

    另外,一个loader由两部分组成,一个是普通的函数,一个数这个函数的pitch方法。那么我们姑且认为一个完整的laoder由loader-pitch方法和loader-normal方法组成。那么结合上面所说的四类来将,loader的执行顺序是这样的:

    首先执行loader-pitch方法,先是post-loader的pitch,然后inline-loader的pitch,然后是normal-loader的pitch,然后是pre-loader的pitch,pitch走完,开始走loader-normal方法,先走pre-loader的方法,再走normal-loader的方法,再走inline-loader的方法,再走post-loader的方法。

    如果是同类loader,比如都是normal-loader,有loader1,loader2,loader3,那么就是先走loader1的pitch,再走loader2的pitch,再走loader3的pitch,然后走loader3的normal函数,再走loader2的normal函数,最后走loader1的normal函数。这里最后走的loader的normal函数的顺序,其实就是我们常说的laoder的执行顺序,从右往左。但其实是要分pitch和normal两种的。

    当然,pitch方法如果有返回值,那么将忽略后面所有的loader以及本身这个loader对应的normal函数,然后相反顺序一次走前面的loader的normal函数

  • loader有特定的执行顺序,且是在所以loader开始执行前,先把loader的顺序整理好,然后一个个开始执行的。所以本节2.2中的三个符号,可以过滤掉指定的loader。

  • loader的执行顺序是洋葱模型吗?

    不是,洋葱模型是嵌套关系,而loader执行顺序是并列关系。

4. 常用loader的实现

1. babel-loader

babel-loader的主要原理(面试点)就是:调动@babel/core这个包下面的transform方法,将源码通过presets预设来进行转换,然后生成新的代码、map和ast语法树传给下一个loader。这里的presets,比如@babel/preset-env这个预设其实就是各类插件的集合,基本上一个插件转换一个语法,比如箭头函数转换,有箭头函数转换的插件,这些插件集合就组成了预设。

const babel = require('@babel/core');
const path = require('path')

function loader(inputSource, map, ast){ // 上一个loader的源码。映射,和抽象语法树
  const options = {
    presets: ["@babel/preset-env"], // 转换靠预设。预设是插件的集合
    sourceMaps: true, // 如果这个参数不传,默认值为false,不会生成sourceMap
    filename: path.basename(this.resourcePath) // 生成的文件名

  }
  // 返回有三个值,code转换后的es5代码,map转换后的代码到转换前的戴梦得映射,ast是转换后的抽象语法树
  let transRes = babel.transform(inputSource, options);

  // loader的返回值可以是一个值,也可以是多个值
  // return inputSource; // 返回一个值,用return
  return this.callback(null, transRes.code, transRes.map, transRes.ast); // 返回多个值, 必须调用this.callback(err, 后面的参数是传递给下一个loader的参数)。这个callback是loader-runner提供的一个方法,内置的。这个this默认是loader-runner内部的context,默认是空对象,但是在loader-runner执行的过程中会天机爱很多方法和属性,包括这个callback方法。
}

module.exports = loader;
2. file-loader的实现

file-loader的原理(面试点)就是通过laoder的参数拿到文件的内容,然后解析出file-loader配置中的名字,解析名字其实就是替换[hash]、[ext]等,然后向输出目录里输出一个文件,这个文件的内容就是loader的参数,名字就是刚刚说的解析出的名字。但是,实际上,并不是在loader里输出文件的,loader只是向webpack的complication的assets中,添加的文件id和内容,最终还是webpack将文件写进硬盘的。

const { getOptions, interpolateName } = require("loader-utils");

/*
  content是上一个loader传给当前loader的内容,或者源文件内容,默认是字符串类型
  如果你希望得到Buffer,不希望转成字符串,那么就给loader.row置为true。即loader.row若为默认值false则content是字符串,为true就是Buffer

*/
function loader(content){
  let options = getOptions(this) || {}; // 拿到参数
  // 下面的参数 this是loaderContext, filename是文件名生成模板,即webpack配置中的[hash].[ext] content是文件内容
  let url = interpolateName(this, options.filename || "[hash].[ext]", {content}); // 转换名字
  // 向输出目录里输出一个文件
  // this.emitFile是loaderRunner提供的
  this.emitFile(url, content);// 向输出目录里输出一个文件,其实本质就是webpack中的complication.assets[filename]=content,然后webpack会将assets写到目标目录下。所以不是loader去生成文件的。
  return `module.exports = ${JSON.stringify(url)}`; // 这里的loader肯定要返回一个JS模块代码,即导出一个值,这个值将会成为次模块的导出结果
}

loader.raw = true; // loader的参数content会是buffer类型

module.exports = loader;
3.url-loader的实现

(面试点)url-loader是file-loader的升级版,内部包含了file-loader。url-loader配置的时候回配置一个limit,这个配置的值代表小于limit的值的时候,转成base64,大于的时候还是文件,比如说原本是图片,那大于limit就还是图片。

所以,url-loader主要就是先判断大小(内容的buffer的lenth)是否大于limit,大于就走file-loader,否则就用toStrng('base64')转成base64。

const { getOptions, interpolateName } = require("loader-utils");
const mime = require('mime');

/*
  content是上一个loader传给当前loader的内容,或者源文件内容,默认是字符串类型
  如果你希望得到Buffer,不希望转成字符串,那么就给loader.row置为true。即loader.row若为默认值false则content是字符串,为true就是Buffer

*/
function loader(content){
  // console.log(content)
  let options = getOptions(this) || {}; // 拿到参数
  let { limit, fallback } = options;
  if (limit) {
    limit = parseInt(limit, 10);
  }
  const mimeType = mime.getType(this.resourcePath);
  if (!limit || content.length < limit) {
    let base64 = `data:${mimeType};base64,${content.toString('base64')}`;
    return  `module.exports = ${JSON.stringify(base64)}`;
  } else {
    // 这里不能用require('file-loader'),因为如果这样写的话,会去node_modules中找,而不是我们自己的file-loader了。源码是可以的,因为源码总file-loader就是装在node_modules中
    return require(fallback).call(this, content);
  }
}

loader.raw = true; // loader的参数content会是buffer类型

module.exports = loader;
4. 样式处理的loader——style-loader、css-loader和less-loader

一般我们处理像是的loader配置为:

{
	test:/\.less$/,
    loaders: [
        'style-loader',
        'css-loader',
        'less-loader',
    ]
}
1.less-loader的实现

less-loader的原理(面试点):主要是借助less模块的render方法,将less语法进行转换成css语法,然后返回或者额调用this.callback()传递给下一个loader。但是由于less-loader原本设计的时候,是想让less-loader可以作为最后一个loader使用的,所谓的最后一个loader,也就是说最后的返回值是一个js模块,也就是说module.exports = xxx这种,所以less-loader在返回结果的时候,将转换后的内容,外面套了一层module.exports = 转换后的内容。

那么这里变成了module.exports导出后,给到css-loader,css-loader只是处理了import、url等语法,将内容给到了style-loader,style-loader也就要跟着改变,因为style-loader的作用是创建一个style脚本,将css内容包裹在style标签中去,然后把style插入到document.head中。那么这里的关键就是拿到样式内容,这个内容刚才说了,被module.exports包裹了,那怎么拿到?直接require就可以了,因为module.exports本来就是js模块的导出格式,所以直接require就可以了。

实际上,在真正的style-loader、css-loader和less-loader的执行过程是这样的(面试点)

​ 先执行loader的pitch函数,pitch函数是从左往右的,从上到下的,也就是先执行style-loader的pitch,这个函数主要是创建一个script脚本,这个脚本主要是创建一个style标签,style标签的innerHTML就是css样式,然后将style标签插入document.head中,然后将这个script脚本返回。注意,这边是有返回值的。pitch-loader一旦有返回值,那么后面的css-loader和less-loader都将不会直接,也不会执行当前loader的normal-loader,既然都不会执行了,那么style标签的css内容哪里来呢?其实,他在创建style标签后,它又require了css-loader和less-loader这两个内联loader,是走了内联loader才获取到的。内联loader从右往左,从下往上,也就是先执行less-loader,然后执行css-loader,最后将内容返给stylel-loader,这样才得到了css内容,赋值给style标签的innerHTML,然后插入到document.head中,这样才完成了整个样式的loader处理。

(我比较啰嗦,按照面试的感觉来说的,偏口语化,谁能帮我组织下语言,万分感谢!)

let less = require('less')

function loader(inputSource){
  // let css;
  // less.render(inputSource, { filename: this.resource }, (err, output) => {
  //   css = output.css;
  // })
  // return css; // 虽然上面css赋值在回调中,但是本身render是同步的,所以可以在这里return。但是假如render是异步,那么就不能够这么写了,异步怎么写,看下面:

  let callback = this.async(); // 这种写法就是即便render是异步,也可以在loader中返回callback的参数值。this.async()这个方法是loader-runner提供的,乳沟调用了async方法,可以把loader的执行变成异步
  less.render(inputSource, { filename: this.resource }, (err, output) => {
    // less-loader本来可以写成callback(err, output.css),但是作者为了能够使得less-loader放在最后一个,也就是返回的应该是一段JS脚本,所以就写成了下面的写法
    callback(err, `module.exports = ${JSON.stringify(output.css)}`) // 这个callback的是this.async(),而this.async()里面的实现就是调用context.callback,而这里的this就是context,所以你不写let callback = this.async(),在回调中直接用this.callback(null, 内容)是一样的
  })
}

module.exports = loader;
2.css-loader的作用

其实less-loader已经转成css了,但是有些语法比如import、url还尚未处理,所以这个css-loader就是用来处理import、url等语法的,功能比较单一。

3.style-loader的实现

见less-loader的笔记(面试点)

const { Console } = require("console");
const loaderUtils = require('loader-utils');

function loader(){

}

/*
  参数:
  remainingRequest 剩下的请求
  previousRequest 前面的请求
  data 数据

*/

loader.pitch = (remainingRequest, previousRequest, data) => {
  console.log('remainingRequest', remainingRequest);
  console.log('previousRequest', previousRequest)
  console.log('data', data, loaderUtils.stringifyRequest(this, '!!' + remainingRequest));
  let script = `
    let style = document.createElement('style');
    style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}); // 依赖的值为!!C:/Users/yuhua7/Desktop/webpack/4.webpack-loader/loaders/less-loader.js!C:/Users/yuhua7/Desktop/webpack/4.webpack-loader/src/style.less
    document.head.appendChild(style);
    module.exports = '';
  `;
  // 这个返回的js脚本给了webpack了
  // webpack会把这个js脚本转成AST抽象语法树,分析脚本中的依赖,也就是上面的require,加载依赖,依赖(require的参数)为!!C:/Users/yuhua7/Desktop/webpack/4.webpack-loader/loaders/less-loader.js!C:/Users/yuhua7/Desktop/webpack/4.webpack-loader/src/style.less,那么这个参数有两个感叹号!!,这代表只走行内,也就是说其实只需要一个内联loader去处理,所以会去走内联loader处理文件。
  return script;
}

module.exports = loader;

css-loader的功能很纯粹,就是处理import和url的

最后一个loader需要返回的是js模块,也就是module.exports = JSON.stringify(内容)

5.loader.pitch的重要参数(面试点)

loader.pitch方法中有三个参数,分别是remainingRequestpreviousRequestdata

  • remainingRequest:剩余的请求
  • previousRequest:前面的请求
  • data:数据

下面解释下三个参数:

假设当前已经走到laoder3.pitch了,那么

  • remainingRequest剩余的请求就是当前loader后面(不含当前)的loader和file的路径用感叹号’!'拼接,类型是字符串。
  • previousRequest前面的请求就是当前loder之前(不含当前)的loader的路径用感叹号’!'拼接,类型是字符串。
  • data数据其实是一个空对象{},给loader内部存放数据使用的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F3ZaJOKQ-1628237907582)(C:\Users\yuhua7\AppData\Roaming\Typora\typora-user-images\image-20210806124157795.png)]

  • 上面除了remainingRequestpreviousRequest和图中的request,其实还有一个currentRequest,这个currentRequest其实就是上图中的loader3
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值