我应该如何去理解Webpack Loader

149 阅读4分钟

💡Loader是什么?

Loader在webpack中实际担当的是一个模块转换器。他可以在你import一个模块的时候,对这个模块进行预处理,将文件的内容从不同的语言转换为JavaScript,或者将内联图像转换为dataURL。 Loader实际上是一个函数,接收一个文件的内容,经过一系列处理之后,然后返回新的文件内容。

function Loader(content) {
  // do something
  const newContent = handleContent(content)
  return newContent
}

🙇‍♂️我们为什么需要Loader? webpack运行在node上,仅仅只能识别JavaScript模块,在对所有的模块处理之前,必须将相应模块转换成JavaScript。例如,在我们编写TypeScript代码时,必须通过对应的ts-loader将 TypeScript文件编译成JavaScript,然后webpack才能进行后续的处理。

💡Loader怎么用?

module.exports = {
  entry: './src/index.js',
  output: '[name].js',
  mode: 'development',
  module: {
    rules: [
      {
        test: /.css$/,
        use: ['style-loader', 'css-loader'],
        enforce: 'post'
      },
      {
        test: /.js$/,
        exclude: /node_modules/
        use: [{
          loader: 'babel-loader',
          options: {
            presets: [
              [
                '@babel/preset-env',
              ]
            ],
            plugins: [
              ['@babel/plugin-transform-runtime', {
                corejs: 3
              }],
            ]
          }
        }]        
      }
    ]
  }
}

test

test接受一个正则表达式,模块如果命中了这个正则表达式,就会交由对应的loader去处理。

use

use规定了当匹配到test中的模块时,应该由哪个loader去处理,可以是一个字符串或者一个数组。
当use接受一个数组时,会按照从右到左,从下往上的顺序去处理。

enforce

enforce规定了同一个模块的loader执行顺序,enforce可接受的值为pre,post,normal(默认)。
分别代表了前置,后置和默认。

💡Loader的执行顺序?

在webpack中,loader的执行分为了两个阶段:

  • pitch阶段
  • normal阶段
module.export = {
  module: {
    rules: [
      {
        test: /\.js&/,
        use: ['normal1-loader', 'normal2-loader']
      },
      {
        test: /\.js&/,
        use: ['pre1-loader', 'pre2-loader']
      },
      {
        test: /\.js&/,
        use: ['post1-loader', 'post2-loader']
      },   
    ]
  }
}
// 以及在入口文件中,存在一个内联loader
import example from 'inline1-loader!inline2-loader!./index.js';

上述例子的执行顺序为:

image.png loader在执行的时候,会首先经过loader.pitch阶段,pitch阶段结束后才会读取文件内容,然后执行normal阶段。

  • 在Pitch阶段,顺序为:Post => Inline => Normal => Pre
  • 在Normal阶段,顺序为: Pre => Normal => Inline => Post
在Pitch阶段中,如果某一个Loader的pitch函数中返回了一个非undefined的值时,就会发生**熔断**的效果。
所谓熔断,就是指loader原先的执行顺序会被阻断,会立马掉头执行,从当前的loader.pitch中跳出,然后执行上
一个已经执行的loader的normal阶段。

image.png

💡Loader的类型?

同步Loader

所谓的同步loader,就是在loader本身处理一些同步的代码逻辑,并且返回相对应的值

function loader(content) {
  return content
}
loader.pitch = function () {
  ...
}

异步Loader

当我们需要在loader执行的时候,进行一些异步操作,例如Ajax请求,定时器等时,我们需要用到异步loader

function asyncLoader() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('test')
    }, 1000)
  })
}

// 或者使用this.async的方式
function asyncLoader() {
    const callback = this.async()
    callback('test')
}


Raw Loader

当我们处理一些图片或文件等资源时,我们需要得到的文件内容通常是Buffer类型,这时候需要用到我们的Raw属性。

function loader(content) {
  return content
}
loader.raw = true

💡Loader的参数?

Normal Loader

normal loader默认接收一个参数,为需要处理的文件内容,如果存在多个loader,上一个loader的返回值,则会传给下一个loader作为入参,直到最后一个loader处理完毕之后,将文件内容交给Webpack处理。

Pitch Loader

Pitchloader通常接收3个参数,分别是:

  • remainingRequest
  • previousRequest
  • data
remainingRequest表示剩余需要处理的loader的绝对路径,并以!分隔组成的字符串。
另外,remainingRequest与剩余loader有没有pitch属性没有关系。

image.png 如上图所示, 对于loader1.pitch而言,remainingRequest的值为xxx/loader2.js!xxx/loader3.js。

:::tips previousRequest则表示已经处理过的loader的绝对路径,并以!分隔组成的字符串。 :::

data属性,默认是一个空对象{}。
data属性可以看做normalLoader和pitchLoader的交互桥梁。
// 在pitch函数里面对data赋值
loader2.pitch = function(remainingRequest, previousRequest, data){
	data.age = 12
}
// 在loader中可以获取
loader2 = function (){
  console.log(this.data.age)
}

💡Loader在Webpack的什么阶段执行?

1、Webpack首先会合并命令行参数和配置文件参数,得到参数对象。

2、将参数对象传递给webpack,得到一个Compiler对象。

3、执行Compiler.run方法,开始编译,生成一个Compliation对象

4、Compliation会依次执行addEntry方法和buildModule方法,在buildModule方法中会根据不同的模块类型 调用不同的loader进行解析和转译,解析完毕会将结果交给webpack进行后续的操作。

image.png

💡简单实现常用Loader

/** babel-core是babel转译代码的核心方法,他可以将我们的代码进行
 ** 词法分析-语法分析-语义分析,从而生成Ast语法树
*/

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

const schema = {
  type: 'object',
  properties: {
    presets: {
       type: 'array'
    }
  },
};

module.exports = function (content) {
    const callback = this.async();
    const options = this.getOptions(schema); 
    babel.transform(content,options, function(err, result) {
            if(err) callback(err)
            else callback(null, result.code)
    })
}

function styleLoader (content) {
   return content;
}

styleLoader.pitch = function (remainingRequest) {
    // 将remainingRequest里的loader的绝对路径转为相对路径, 然后再转为inline-loader
    const inlineLoder = remainingRequest.split('!').map(absolutePath => {
        return this.utils.contextify(this.context, absolutePath)
    }).join('!')

    // 借助inline-loader来获取css-loader处理好的css样式
    // !!禁止inline-loader之后的所有loader的使用
    const script = `
        import style from "!!${inlineLoder}"
        const styleEl = document.createElement('style');
        styleEl.innerHTML = style;
        document.head.appendChild(styleEl);
    `
    /** pitch返回了非undefined的值,触发了熔断,
    反向执行Normal阶段的loader,由于style-loader前面没有loader了,
    所以这里直接结束了loader的周期
    */
    return script
}
module.exports = styleLoader