loader

2021/03/22 tools

webpack loader

1、loader

webpack只能处理JavaScript的模块,如果要处理其他类型的文件就需要使用loader进行转换。

loader是webpack中一个重要的概念,它是指用来将一段代码转换成另一段代码的webpack加载器。

2、一个简易loader

初始化一个npm项目,安装webpack,配置webpack.config.js

let path = require('path');

module.exports = {
  mode: 'development',
  entry: "./src/index.js",
  output: {
    filename: "build.js",
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, 'loaders','jsLoader.js')
      }
    ]
  }
}


// 加载loader还可以通过配置resolveLoader 别名alias的配置,不需要在配置loader的地方写path.resolve(__dirname, 'loaders','jsLoader.js') 这么长的内容
let path = require('path');
module.exports = {
  mode: 'development',
  entry: "./src/index.js",
  output: {
    filename: "build.js",
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    alias: {
      myJsLoader: path.resolve(__dirname, 'loaders', 'jsLoader.js')
    }
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: 'myJsLoader'
    }]
  }
}

// 还可以配置成自己寻找  配置modules,设置如果`node_modules`里面找不到就去`./loaders`里面找,这样就可以自动寻找我们自定义的loader了
module.exports = {
  mode: 'development',
  entry: "./src/index.js",
  output: {
    filename: "build.js",
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: 'jsLoader'
    }]
  }
}

加载一个我们自己的loader,在/loaders/jsLoader.js

function loader(source){
  return source;
}

module.exports = loader;

loader就是一个函数,接受一个参数,参数是源代码,多个loader的时候从下自上依次执行,前面执行的loader的返回值是下一个loader执行的参数。

输入以上代码执行npx webpack打包,dist目录内的build.js内容主要如下

eval("console.log('loader 1 转换的js');\n\n//# sourceURL=webpack://loader/./src/index.js?");

会发现源代码会被eval()方法包裹。

这就是一个简易的loader了

3、配置多个loader

多个loader的执行顺序默认是 从下到上,从右到左。

先创建多个loader


// loaders/loader2.js
function loader(source) {
  source =  source.replace(/loader1/, 'loader2')
  return source;
}

module.exports = loader;



// loaders/loader3.js
function loader(source) {
  source = source.replace(/loader2/, 'loader3')
  return source;
}

module.exports = loader;

修改webpack.config.js的配置,可以加载多个loader

module.exports = {
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [{
      test: /\.js$/,
      use: [
        'loader3',
        'loader2',
        'jsLoader'
      ]
    }]
  }
}

然后运行npx webpack来看结果, 打开dist/build.js运行代码后输出

loader3 转换的js

可以发现loader的确是从右到左从下到上执行的,而且前面loader的返回值是后面loader的参数。

4、loader的分类

分为4类

  1. pre:在前面的loader
  2. post:在后面的loader
  3. normal:正常的loader
  4. inline: 行内的loader

执行顺序是: pre -> normal -> inline -> post。其中inline loader不需要写在配置里,可以在行内引入使用。

当我们loader的书写顺序需要调整,但是又不想改代码顺序的时候可以配置loader的分类,改变loader的加载顺序

module.exports = {
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'jsLoader',
        },
        enforce: 'pre',
      },
      {
        test: /\.js$/,
        use: {
          loader: 'loader2',
        }
      },
      {
        test: /\.js$/,
        use: {
          loader: 'loader3',
        },
        enforce: 'post',
      }
    ]
  }
}

这样写不看enforce配置单纯看执行顺序是loader3 -> loader2 -> jsLoader, 实际的顺序还是之前的jsLoader -> loader2 -> loader3

4.1 inline-loader

使用inline-loader可以在代码里直接使用。

let str = require('-!inline-loader!./a.js');

创建inline-loader.jsa.js

// a.js
module.exports = 'a.js'

// inline-loader.js
function loader(source) {
  console.log('inline loader')
  return source;
}

module.exports = loader;

index.js里引入a.js

let str = require('-!inline-loader!./a.js');

执行npx webpack看控制台会打印出inline loader

标识符的含义:

  • -!标识符(-!inline-loader.js)禁用前置和正常loader;会让当前引入的文件不需要通过pre + normal状态的loader进行处理,直接跳过这两种loader交给inline-loader来处理,然后处理完交给postloader处理。
  • !标识符(inline-loader!./a.js)是前置的意思,意思是让前面的loader来处理,inline-loader!./a.js:意思是让inline-loader.js处理 ./a.js
  • !标识符(!inline-loader.js)禁用普通loader;会跳过normal状态的loader,只使用pre的loader处理完成之后直接给inline-loader处理,然后处理完交给postloader处理。
  • !!标识符(!!inline-loader.js)禁用前置后置和正常loader; 意思是只通过inline-loader来处理,不需要其他loader处理。

5、loader的组成

每个loader都有两个部分组成pitch & normal, pitch和normal的执行顺序相反,normal是正常的loader,当pitch没有定义或者没有返回值的时候,会先依次执行pitch在获取资源执行loader,如果定义的某个pitch有返回值则会跳过读取资源和自己的loader。

loader配置pitch的执行的顺序:loader3.pitch -> loader2.pitch -> loader1.pitch -> 获取到资源 -> loader1 -> loader2 -> loader3

如果pitch没有返回值就按上面正常流程执行。

先把index.js里面引用a.js的代码注释掉,代码展示:

// 每个loader里面加如下代码

// jsLoader.js
function loader(source){
  console.log('jsLoader 执行')
  return source;
}
loader.pitch = function(){
  console.log('loader1 jsLoader pitch 执行')
}
module.exports = loader;


// loader2.js
function loader(source) {
  source = source.replace(/loader1/, 'loader2')
  console.log('loader2 执行')
  return source;
}
loader.pitch = function() {
  console.log('loader2 pitch 执行')
}
module.exports = loader;

// loader3.js
function loader(source) {
  source = source.replace(/loader2/, 'loader3')
  console.log('loader3 执行')
  return source;
}

loader.pitch = function() {
  console.log('loader3 pitch 执行')
}
module.exports = loader;

然后运行npx webpack,可以看loader的加载执行顺序如下

loader3 pitch 执行
loader2 pitch 执行
loader1 jsLoader pitch 执行
jsLoader 执行
loader2 执行
loader3 执行

这是pitch没有返回值的情况下,走的正常的加载流程。如果pitch有返回值,则会打断这个顺序,假设loader2pitch有返回值,其他代码依旧

// loader2.js
function loader(source) {
  console.log('loader2 执行')
  source = source.replace(/loader1/, 'loader2')
  return source;
}

loader.pitch = function() {
  console.log('loader2 pitch 执行')
  return 'console.log("loader2 执行的pitch返回值")';
}
module.exports = loader;

然后运行npx webpack,打印出来结果如下

loader3 pitch 执行
loader2 pitch 执行
loader3 执行

如果loader2.pitch有返回值的话的执行顺序: loader3.pitch -> loader2.pitch -> loader3

然后运行dist/build.js,打印结果如下

loader3 执行的pitch返回值

6、loader的特点

  1. 第一个loader要返回js脚本
  2. 每个loader只做一件事,可以兼容更多场景,然后链式调用
  3. 每个loader都是一个模块
  4. 每个loader都是无状态的,确保loader在不同模块转换之间不保存状态。

7、babel-loader源码

安装@babel-core @babel/preset-env@babel-loader我们自己写。

loaders创建文件babel-loader.js,输入代码

let babel = require('@babel/core');
// 获取在 `webpack.config.js`里面配置loader地方传入的options
let loaderUtils = require('loader-utils');

function loader(source) {
  let options = loaderUtils.getOptions(this);
  // cb 内置的回调,在异步执行的时候调用它就相当于return了,调用它无需再写  `return source`
  let cb = this.async(); 
  // 使用babel来转化源代码
  babel.transform(source, {
    ...options,
    sourceMap: true,
    // 开启sourcemap  可以在浏览器里面看到源码,方便debug
    filename: this.resourcePath.split('/').pop()
  }, function(err, result) {
    cb(err, result.code, result.map);
  })
}

module.exports = loader;

检验我们代码有没有问题,在src/index.js里面输入一段ES6的箭头函数

const a =  () => console.log('2222');

运行npx webpack,看dist/build.js

贴一下webpack.config.js里的配置

module.exports = {
  devtool: 'source-map',
  module: {
    rules: [{
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            // 这里就是可以通过 loader-utils 拿到的参数,想传什么传什么,比如一行文本,或者一个文件
            presets: [
              '@babel/preset-env'
            ]
          }
        }
      }]
  }
}

另外当我们写loader需要对参数进行验证格式的时候可以使用schema-utils

当我们引用了其他文件作为依赖的时候,依赖文件更新需要重新触发编译可以添加

this.addDependency(filePath);

8、file-loader

简单写一下file-loader的源码

// file-loader.js
// file-loader 处理图片是需要把一个图片转成一个MD5,放到dist目录下, 然后返回一个url
let loaderUtils = require('loader-utils');

function loader(source) {
  // interpolateName: 根据当前提供的东西生成一个url
  // '[hash].[ext]' :生成hash的名字+原来的格式
  let filename =  loaderUtils.interpolateName(this, '[hash].[ext]', { content: source });
  this.emitFile(filename, source);
  return `module.exports = "${filename}"`;
}

loader.raw = true; // 改成二进制
module.exports = loader;

9、url-loader

在处理图片之类的时候,会用到url-loader,如果文件不大就转换成base64,如果太大就继续调用file-loader处理

// url-loader.js
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('./file-loader').call(this, source);
  }
}
loader.raw = true; // buffer
module.exports = loader;

10、less-loader

处理.less的文件需要less-loader css-loader style-loader

// less-loader.js   负责转换.less文件为css,传给css-loader处理,然后再由css-loader传给style-loader 插入到HTML中
let less = require('less'); // 解析less 转成 css

function loader(source) {
  let css = null;
  less.render(source, function(err, res) {
    css = res.css;
  })
  return css;
}

module.exports = loader;  

11、css-loader

css-loader负责处理css

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;
    let last = reg.lastIndex - matchUrl.length;
    // 存放字符串匹配到的前面部分
    arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
    pos = reg.lastIndex;
    // 存放匹配到的部分
    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;

12、style-loader

style-loader负责把处理好的css代码插入到文档里面

function loader(source){
  // webpack loader的最后一个loader必须返回一个可执行的js字符串

  let str = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.body.appendChild(style);
  `;

  return str;
}

module.exports = loader;

Search

    Table of Contents