什么是loader

loader 用于对模块的源代码进行转换

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
在更高层面,在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。

使用loader

在你的应用程序中,有三种使用 loader 的方式:

  • 配置(推荐):在 webpack.config.js 文件中指定 loader。
  • 内联:在每个 import 语句中显式指定 loader。
  • CLI:在 shell 命令中指定它们。

    配置

    module.rules 允许你在 webpack 配置中指定多个 loader。 这是展示 loader 的一种简明方式,并且有助于使代码变得简洁。同时让你对各个 loader 有个全局概览:
    1. module: {
    2. rules: [
    3. {
    4. test: /\.css$/,
    5. use: [
    6. { loader: 'style-loader' },
    7. {
    8. loader: 'css-loader',
    9. options: {
    10. modules: true
    11. }
    12. }
    13. ]
    14. }
    15. ]
    16. }

内联

可以在 import 语句或任何等效于 “import” 的方式中指定 loader。使用 ! 将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。

尽可能使用 module.rules,因为这样可以减少源码中的代码量,并且可以在出错时,更快地调试和定位 loader 中的问题。

  1. import Styles from 'style-loader!css-loader?modules!./styles.css';
  2. // 例子
  3. import Styles from 'inline-loader?modules!./a.js'; // 内联loader写法1
  4. require('inline-loader!./a') // 内联loader写法2
  5. // -! 不会让文件 再去通过pre + normal loader来处理了
  6. require('-!inline-loader!./a') // 内联loader写法2
  7. // ! 没有normal
  8. require('!inline-loader!./a')
  9. // !! 什么都不要

CLI

你也可以通过 CLI 使用 loader:

  1. webpack --module-bind jade-loader --module-bind 'css=style-loader!css-loader'

这会对 .jade 文件使用 jade-loader,对 .css 文件使用 style-loadercss-loader

loader 特性

  • loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader,返回 webpack 所预期的 JavaScript。
  • loader 可以是同步的,也可以是异步的。
  • loader 运行在 Node.js 中,并且能够执行任何可能的操作。
  • loader 接收查询参数。用于对 loader 传递配置。
  • loader 也能够使用 options 对象进行配置。
  • 除了使用 package.json 常见的 main 属性,还可以将普通的 npm 模块导出为 loader,做法是在 package.json 里定义一个 loader 字段。
  • 插件(plugin)可以为 loader 带来更多特性。
  • loader 能够产生额外的任意文件。

loader 通过(loader)预处理函数,为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑,例如压缩、打包、语言翻译和其他更多

loader的api

https://webpack.docschina.org/api/loaders/

loaderapi之 emitFile

  1. this.emitFile(name: string, content: Buffer|string, sourceMap: {...})

产生一个文件。这是 webpack 特有的

loaderapi之 resource,resourcePath,resourceQuery

this.resource = this.resourcePath + this.resourceQuery

request 中的资源部分,包括 query 参数。
示例中:’/abc/resource.js?rrr’

  • webpack.config.js
    1. {
    2. test: /\.js$/,
    3. use: {
    4. loader: "test-api-loader",
    5. }
    6. },
    ```javascript function loader(source) { // G:\github_project\webpack-study-2022\src\index.js resource console.log(this.resource, ‘resource’) // ‘’ console.log(this.resourceQuery) // G:\github_project\webpack-study-2022\src\index.js resource console.log(this.resourcePath) return source }

module.exports = loader

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/866396/1643079913804-bc1cc9cf-a22e-4220-b74d-70d609947505.png#clientId=u1d003981-8e62-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=59&id=CjjAQ&margin=%5Bobject%20Object%5D&name=image.png&originHeight=118&originWidth=652&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19139&status=done&style=none&taskId=ub31f628f-36da-4f73-bf0b-1c675820be3&title=&width=326)
  2. <a name="RFXGw"></a>
  3. # loader帮助函数之loaderUtils
  4. - 'getOptions',
  5. - 'parseQuery',
  6. - 'stringifyRequest',
  7. - 'getRemainingRequest',
  8. - 'getCurrentRequest',
  9. - 'isUrlRequest',
  10. - 'urlToRequest',
  11. - 'parseString',
  12. - 'getHashDigest',
  13. - 'interpolateName'
  14. <a name="v4q8F"></a>
  15. ### 1)getOptions
  16. 获取options里面的数据
  17. ```javascript
  18. {
  19. test: /\.js$/,
  20. use: {
  21. loader: "test-api-loader",
  22. options: {
  23. customProps: 'wangling'
  24. }
  25. }
let loaderUtils = require('loader-utils')

function loader(source) {
  console.log(loaderUtils.getOptions(this)) // { customProps: 'wangling' }
  return source
}

module.exports = loader

如果没有options就返回null

{
  test: /\.js$/,
  use: 'test-api-loader'
}

loader的执行顺序

loader执行顺序如下 pre > normal > inline > post

默认执行顺序

从右向左,从下到上

从右到左

rules:[
  {
    test:/\.js$/,
    use:['loader1','loader1','loader3']
  },
]

从下到上

rules:[
  {
    test:/\.js$/,
    use: {
      loader: 'loader1'
    }
  },
  {
    test:/\.js$/,
    use: {
      loader: 'loader2'
    }
  },
  {
    test:/\.js$/,
    use: {
      loader: 'loader3'
    }
  },
]

enforce 改变loader执行顺序

  • pre:在前面执行
  • post:在后面执行
  • normal: 默认是normal
    module: {
      rules: [
        {
          test: /\.js$/,
          use: {
            loader: 'loader1'
          }
        },
        {
          test: /\.js$/,
          use: {
            loader: 'loader2'
          },
          enforce: "pre"
        },
        {
          test: /\.js$/,
          use: {
            loader: 'loader3'
          }
        }
      ]
    // 执行属性 loader2,loader3,loader1
    

    最简单loader - 执行顺序例子

    先看下例子,具体怎么写loader后面再说 ```javascript CustomLoader是自定义的loader的文件夹 |— undefined |— package-lock.json |— package.json |— README.md |— webpack.config.js |— CustomLoader | |— inline-loader.js | |— loader1.js | |— loader2.js | |— loader3.js |— src
      |-- a.js
      |-- index.js
    
```javascript
// loader1.js
function loader(source) {
  console.log('loader1')
  return source
}

module.exports = loader
// loader2.js
function loader(source) {
  console.log('loader2')
  return source
}

module.exports = loader
// loader3.js
function loader(source) {
  console.log('loader1')
  return source
}

module.exports = loader
// index.js
console.log('haha')
const path = require('path')
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    // 找loader的位置
    modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['loader3', 'loader2', 'loader1']
      }
    ]
  }
}

运行 npx webpack
结果如下

loader1
loader2
loader3

改变下

const path = require('path')
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    // 找loader的位置
    modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
  },
  module: {
    rules: [
       {
        test: /\.js$/,
        use: {
          loader: 'loader3'
        },
        enforce: "post"
      },
      {
        test: /\.js$/,
        use: {
          loader: 'loader2'
        },
        enforce: "pre"
      },
      {
        test: /\.js$/,
        use: {
          loader: 'loader1'
        }
      }
    ]
  }
}

结果如下

loader2
loader1
loader3

Loader的pitch方法

在定义一个loader函数时,可以导出一个pitch方法,这个方法会在loader函数执行前执行。
另外,如果存在多个loader串行的情况,这些loader的pitch函数会从左到右依次执行,其示意图如下:
image.png

// loader1.js
function loader(source) {
  console.log('loader1')
  return source
}
loader.pitch = function() {
  console.log('loader1 pitch')
}
module.exports = loader
// loader2.js
function loader(source) {
  console.log('loader2')
  return source
}
loader.pitch = function() {
  console.log('loader2 pitch')
}
module.exports = loader
// loader3.js
function loader(source) {
  console.log('loader3')
  return source
}
loader.pitch = function() {
  console.log('loader3 pitch')
}
module.exports = loader
const path = require('path')
module.exports = {
  mode: 'production',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ['loader3', 'loader2', 'loader1']
      }
    ]
  }
}

运行 npx webpack
image.png
如果给loader2的pitch一个返回值呢?

// loader2.js
function loader(source) {
  console.log('loader2')
  return source
}
loader.pitch = function() {
  console.log('loader2 pitch')
  // 返回任意值都是一样的效果,除了undefined
  return 'xxx'
}
module.exports = loader

image.png
只输出了一个loader3

手写loader

babel-loader实现

npm i @babel/core@7.2 @babel/preset-env@7.2 loader-utils@1.1 -S

babel-loader实现

let babel = require('@babel/core');
let loaderUtils = require('loader-utils')

function loader(source) {
  /*
   loaderUtils.getOptions(this) 获取options这部分
  * options: {
      presets: ['@babel/preset-env']
    }
  * */
  let options = loaderUtils.getOptions(this);
  let cb = this.async();
  babel.transform(source,{
    ...options,
    // 源代码调试
    sourceMap:true,
    // 主要是为了sourceMap显示文件名名称,不然会显示undefined
    filename: this.resourcePath.split('/').pop() // 文件名
  },function (err,result) {
    cb(err,result.code,result.map); // 异步
  });
}


module.exports = loader;

测试代码

// webpack.config.js
const path = require('path')
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'CustomLoader')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "custom-babel-loader",
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
}
// index.js
class MyName {
  constructor(name) {
    this.name = name
  }
  getName() {
    return this.name
  }
}
const name = new MyName('wangling')
console.log(name.getName())

file-loader实现

let loaderUtils = require('loader-utils');
function loader(source) {
  // file-loader 需要返回一个路径
  let filename = loaderUtils.interpolateName(this, '[name].[hash].[ext]', { content: source});
  // zly.4d069064177f2de284d771e6ff46df22.jpg filename
  console.log(filename, 'filename') 
  this.emitFile(filename, source); // 发射文件
  return `module.exports="${filename}"`
}
loader.raw = true; // 二进制
module.exports = loader;

url-loader实现

let loaderUtils = require('loader-utils');
let mime = require('mime');
function loader(source) {
  let { limit } = loaderUtils.getOptions(this) || {};
  if (limit && limit > source.length) {
    return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
  } else {
    return require('./custom-file-loader').call(this, source)
  }
}
loader.raw = true;
module.exports = loader;

banner-loader

let loaderUtils = require('loader-utils');
let validateOptions = require('schema-utils');
let fs = require('fs');
function loader(source) {
  this.cacheable && this.cacheable()
  let options = loaderUtils.getOptions(this);
  let cb = 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',function (err,data) {
      cb(err, `/**${data}**/${source}`);
    });
  }else{
    cb(null, `/**${options.text}**/${source}`);
  }
}
module.exports = loader;

style-loader,css-loader,less-loader实现

// style-loader
let loaderUtils = require('loader-utils');
function loader(source) {
  // 我们可以在style-loader中导出一个 脚本
  let str = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style);
  `
  return str;
}
// 在style-loader上 写了pitch
// style-loader     less-loader!css-loader!./index.less
loader.pitch = function (remainingRequest) { 
// 剩余的请求
// 让style-loader 去处理less-loader!css-loader/./index.less
// require路径 返回的就是css-loader处理好的结果 require('!!css-loader!less-loader!index.less')
  let str = `
    let style = document.createElement('style');
    style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)});
    document.head.appendChild(style);
  `
  return str;
}
module.exports = loader;
// css-loader 
function loader(source) {
  let reg = /url\((.+?)\)/g;
  let pos = 0;
  let current;
  let arr = ['let list = []'];
  while (current = reg.exec(source)) { // [matchUrl,g]
    let [matchUrl, g] = current;
    //console.log(matchUrl, g)
    let last = reg.lastIndex - matchUrl.length;
    arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
    pos = reg.lastIndex;
    // 把 g 替换成 require的写法  => url(require('xxx'))
    arr.push(`list.push('url('+require(${g})+')')`);
  }
  arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
  arr.push(`module.exports = list.join('')`);
  return arr.join('\r\n');
}

module.exports = loader;
// less-loader
let less = require('less');
function loader(source) {
  let css;
  less.render(source,function (err,r) { // r.css
    css = r.css;
  });
  console.log(css)
  return css
}

module.exports = loader;

参考链接

webpack 中如何自定义loader
手写清除console的loader
https://www.webpackjs.com/concepts/loaders/ webpack-loader介绍
https://www.webpackjs.com/contribute/writing-a-loader/ 编写一个loader
https://www.npmjs.com/package/loader-utils/v/0.2.15?activeTab=versions loader-utils