Webpack的Loader和Plugin

发布于:2024-04-17 ⋅ 阅读:(16) ⋅ 点赞:(0)

1.Loader

1.1 Loader作用

把js和json外的其它文件转为Webpack可以识别的模块

1.2 Loader简介

1.2.1 Loader类型

1.总类型

pre: 前置loader

normal: 普通loader

inline: 内联loader

post: 后置loader

2.默认类型

默认为normal类型

3.修改类型

配置时可以通过enforce修改pre,normal,post类型。

{
enforce: 'post',
test: /\.js$/,
loader: 'loader'
}
1.2.2 Loader顺序

1.总顺序

类型顺序 > 配置顺序

举例:

配置loader:[A, B, C],执行顺序为:C -> B -> A

配置loader:[A(enforce: pre), B, C],执行顺序为 A -> C -> B

2.类型顺序

pre > noraml > inline > post

3.配置顺序

从右到左,从下到上(即配置的链表的逆序)

1.2.3 Loader使用

1.配置Loader

在webpack.config.js中配置Loader将处理哪些类型的文件

配置方法: 见“Webpack学习记录”

2.内联Loader

在引入某个文件时指定使用的Loader

内联方法:

  • Loader: 多个Loader间用!隔开

  • 参数: 和URL一样用?和&传参给Loader

  • 文件: 文件和Loader间用!隔开

  • 优先级: 类似于配置Loader中的enforce。

    • !: 跳过普通Loader

    • -!: 跳过前置Loader和普通Loader

    • !!: 跳过前置Loader和普通Loader和后置Loader

注意:内联Loader在每次引入文件时使用,写的内容太多太分散,且不利于排查问题。不推荐使用。

import test from 'B-loader!A-loader?mode=txt&type=run!./test.txt'
module.exports = function(content) {
    // 通过loaders获取内联的每个loader的具体信息,包括查询参数
    console.log(this.loaders)
    return content
}

3.脚手架Loader

配置方法: 了解即可。下面是对.jade和.css文件使用对应的loader

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

1.3 Loader开发

1.3.1 Loader模式

1.开发Loader基本概念

函数: Loader是一个函数,涉及this调用,不要使用箭头函数

函数参数:

  • content: 文件内容

  • map: 代码映射

  • meta: 传递给下一个Loader的内容

链式调用: 不论采用简洁模式还是普通模式,后续loader会通过callback或返回值获取前一个loader处理内容继续处理

2.同步Loader

注意:同步Loader中不应该存在异步操作

简洁模式

module.exports = function(content, map, meta) {
 return content
}

普通模式

module.exports = function(content, map, meta) {
 this.callback(null, content, map, meta)
}

3.异步Loader

注意:虽然异步Loader中有异步操作,但是链式调用时只有异步操作完成,才能继续链式调用

简洁模式

module.exports = function(content, map, meta) {
 return new Promise((res) => {
     setTimeout(() => {
         res(content)
     }, 1000)
 })
}

普通模式

module.exports = function(content, map, meta) {
 const callback = this.async()
 setTimeout(() => {
     callback(content, map, meta)
 }, 1000)
}

3.Raw Loader

用途: Raw Loader配置raw为true即可,表示接收Buffer格式的文件二进制数据,通常用于处理图片,音视频等。

模式: 同步和异步模式Raw Loader都可以使用。

module.exports.raw = true
module.exports = function(content, map, meta) {
	return content
}

4.Pitch Loader

用途: Pitch Loader配置pitch为函数即可,表示提前执行pitch函数,可以在函数中返回一个非undefined值来中断链式调用中后续Loader的执行。

模式: 同步和异步模式Pitch Loader都可以使用。

中断: Pitch函数返回值中断后,会导致无法读取文件,后续执行的Loader函数的文件来源是中断Pitch函数的返回值。

顺序:

  • Pitch阶段: 按照配置的Loader的链表的正序执行它们的Pitch函数。Pitch一旦有返回值,立即执行上一个Pitch对应的Loader并终止链式调用。
  • 读取文件: Pitch阶段结束后Loader读取文件准备执行Loader函数。
  • Normal阶段: Normal阶段包括pre,normal,inline,post。Normal阶段晚于Pitch阶段。按照配置的Loader的链表的逆序执行它们的Loader函数。

参数:

  • remainingRequest: 当前Loader之后要使用的Loader,以内联Loader格式显示。
  • precedingRequest: 当前Loader之前要使用的Loader,显示Loader位置。
  • data: 用于同一对Pitch和Loader通信。设置data上的属性,Loader可以在this.data中获取到。
module.exports.pitch = (remainingRequest, precedingRequest, data) => {
    // pitch和loader间通信
    data.x = 123
    // 有返回值提前中断
    return 'result'
}
module.exports = function(content, map, meta) {
    // pitch和loader间通信
    console.log(this.data.x)
	return content
}
1.3.2 Loader方法

常用方法

方法 描述 用法
this.callback 描述Loader返回结果 this.callback(null, content, map, meta)
this.async 标记Loader为异步Loader并返回callback const callback = this.async()
this.getOptions 获取webpack.config.js中配置的Loader的options (注意:schema对象,用于描述校验规则,类似于props-type库) const options = this.getOptions(schema)
this.emitFile 输出文件到打包后的文件夹中 (注意:通常在处理webpack默认解析不了的文件,并且想让它输出到打包后文件夹中的场景中使用) this.emitFile(name, content, sourceMap)
this.utils.contextify 产生一个相对路径 (注意:Path模块产生的路径可能不满足某些Loader的需求,因此一般使用该方法) this.utils.contextify(content, request)
this.utils.absolutify 产生一个绝对路径 this.utils.absolutify(content, request)
1.3.3 clean-log-loader

实现一个清除所有console.log语句的loader

module.exports = function(content) {
	return content.repalce(/console\.log\(.*\);?/g, '')
}
1.3.4 banner-loader

实现一个添加作者信息的loader,并支持options配置

banner-loader.js

const schema = {
// options类型
type: "object",
// options属性
properties: {
 name: {
   type: "string",
 },
},
// options是否可以追加属性
additionalProperties: false,
};

module.exports = function (content) {
const options = this.getOptions(schema);

const prefix = `
     /*
        *   Author: ${options?.name || 'Your Name'}
        */
    `;
  return prefix + content;
};

webpack.config.js

module.exports = {
	module: {
		rules: [
            {
                test: /\.js$/,
                loader: './loader/banner-loader',
                options: {
                    name: 'Danny'
                }
            }
		]
	}
}
1.3.5 babel-loader

实现一个babel-loader,做一个控制传参与调用的中间层,转译模块调用第三方模块。

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);
});
};
1.3.6 file-loader

实现一个file-loader,让webpack能够处理png资源

注意:回顾一下,通常配置webpack的Loader时对于这种资源只配置 type: 'asset'即可,不用指定Loader

  • 重写文件名: 生成带有Hash值的文件名称
  • 输出文件: 输出资源到打包后文件夹
  • 导出文件: 配置资源导出方式
const loaderUtils = require("loader-utils");

// 处理图片,音视频,字体等文件,需要处理二进制文件
module.exports = function (content) {
  // 生成哈希值文件名
  const interpolatedName = loaderUtils.interpolateName(
    this,
    "[hash].[ext][query]",
    { content }
  );

  // 输出文件
  this.emitFile(interpolatedName, content);

  // 文件输出方式修改
  return `module.exports = '${interpolatedName}'`
};
module.exports.raw = true;

webpack.config.js

module.exports = {
    module: {
        rules: [
            {
                test: /\.png$/,
                loader: './loader/file-loader',
                // 禁止webpack默认处理文件资源,只使用我们自定义的loader
                type: 'javascript/auto'
            }
        ]
    }
}
1.3.7 style-loader

注意:style-loader的实现是一种利用pitch loader解决特殊链式调用的解决方案

实现一个style-loader,配合css-loader使用。在实现时请注意这些问题:

  • style-loader的作用: style-loader实现时把样式作为集成到style标签中插入文档。

  • css-loader的作用: css-loader帮助我们解决了依赖引入等问题,例如背景图需要使用图片。

  • css-loader的返回值: css-loader返回一段JavaScript脚本,包含导入导出语句,因此你无法用eval执行获取结果。这和其它大部分Loader在链式调用中返回文件内容不同。

    // Imports
    import ___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___ from "../node_modules/css-loader/dist/runtime/noSourceMaps.js";
    import ___CSS_LOADER_API_IMPORT___ from "../node_modules/css-loader/dist/runtime/api.js";
    import ___CSS_LOADER_GET_URL_IMPORT___ from "../node_modules/css-loader/dist/runtime/getUrl.js";
    var ___CSS_LOADER_URL_IMPORT_0___ = new URL("./assets/development.png", import.meta.url);
    var ___CSS_LOADER_EXPORT___ = ___CSS_LOADER_API_IMPORT___(___CSS_LOADER_API_NO_SOURCEMAP_IMPORT___);
    var ___CSS_LOADER_URL_REPLACEMENT_0___ = ___CSS_LOADER_GET_URL_IMPORT___(___CSS_LOADER_URL_IMPORT_0___);       
    // Module
    ___CSS_LOADER_EXPORT___.push([module.id, `.test {
      width: 100%;
      height: 100%;
      background-image: url(${___CSS_LOADER_URL_REPLACEMENT_0___})
    }
    `, ""]);
    // Exports
    export default ___CSS_LOADER_EXPORT___;
    

注意:其实问题就是在style-loader中怎么引入css-loader返回的脚本的导出值。在Loader函数中你无法通过入参和this上的属性来获取导出值的路径

module.exports = function () {};
module.exports.pitch = function (remainingRequest) {
  // 通过Pitch Loader获取内联Loader调用目标文件的形式
  // 通过引用内联Loader,你可以获取css-loader脚本的导出值
  const relativePath = remainingRequest
    .split("!")
    .map((str) => this.utils.contextify(this.context, str))
    .join("!");

  // 获取css-loader脚本的导出值,在Loader函数中获取不到内联函数调用形式  
  const script = `
          import result from '!!${relativePath}'
          const style = document.createElement('style');
          style.innerHTML = result;
          document.head.append(style)
      `;

  // 终止后续loader执行
  return script;
};

2.Plugin

2.1 Plugin作用

扩展Webpack的功能。

2.2 Plugin简介

2.2.1 Plugin原理

Plugin在Webpack工作流程插入操作来扩展Webpack功能。

2.2.2 Webpack钩子

1.钩子

Webpack暴露若干种钩子,表示Webpack工作的不同阶段。

Plugin可以通过注册钩子插入Webpack工作流程。

2.Tapable

Tapable是Webpack内部引用的一个库,帮助Webpack使用钩子。

Tapable对开发者无感知,Webpack包装了Tapable的某些方法供开发者注册钩子。

  • tap 注册同步钩子和异步钩子
  • tapAsync 回调方式注册异步钩子
  • tapPromise Promise方式注册异步钩子
2.2.3 Webpack构建对象

1.Compiler

Webpack工作时创建Compiler对象,保存了webpack.config.js等配置信息。

Compiler在API形式定制Webpack和Plugin开发时会用到。

  • compiler.options webpack.config.js中的配置信息
  • compiler.inputFileSystemcompiler.outputFileSystem 进行文件操作,类似fs
  • compiler.hooks 注册钩子到整个打包过程

2.Compilation

Webpack工作时创建Compilation对象,保存了对模块编译的信息。

Compilation在Plugin开发时会用到。

  • compilation.modules 访问遇到的模块(文件)
  • compilation.chunks 访问遇到的chunks
  • compilation.assets 访问遇到的资源文件
  • compilation.hooks 注册钩子到编译过程
2.2.4 Webpack生命周期

Webpack生命周期可以通过钩子描述,下面结合Compiler和Compilation的常用钩子描述Webpack生命周期

compiler.initialize: 初始化。

compiler.run: 开始构建。

compiler.compilation: 创建编译实例。

compiler.make: 开始一次编译 (每个文件编译时会触发,包括下述红色部分)

compilation.buildModule: 构建模块。

compilation.seal: 构建完成。

compilation.optimize: 模块优化。

compiler.afterCompile: 所有文件编译完成。

compiler.emit: 输出资源。

compiler.done: 构建过程完成。

2.3 Plugin开发

2.3.1 Plugin模式
  • 调用方式: Plugin以构造函数调用

  • 核心方法: Plugin的核心方法是constructor和apply

    • constructor: Webpack加载webpack.config.js的配置时调用每个plugin的constructor
    • apply: Webpack生成配置对象compiler后调用plugin实例的apply方法
class TestPlugin {
    constructor() {
        console.log('TestPlugin constructor')
    }

    apply(compiler) {
        console.log('TestPlugin apply')
    }
}

module.exports = TestPlugin
2.3.2 Plugin钩子
  • 注册方式: Plugin钩子在apply中注册
  • 钩子执行: 钩子执行分为同步,异步串行,异步并行。执行方式由钩子说明文档决定。

1.异步串行

注:每个钩子执行完毕后才能执行下一个钩子,钩子的执行会阻塞Webpack的构建过程

// compiler.emit钩子文档描述是异步串行
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
    // 参数是compilation,可以以此注册compilation钩子
    console.log("TestPlugin emit sync");
});

compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
    setTimeout(() => {
        console.log("TestPlugin emit async");
        callback();
    }, 10000);
});

compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log("TestPlugin emit promise");
            resolve();
        }, 2000)
    });
});

2.异步并行

注:每个钩子同时触发

// compiler.make钩子文档描述是异步并行
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
    setTimeout(() => {
        console.log("TestPlugin make async1");
        callback();
    }, 3000);
});

compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
    setTimeout(() => {
        console.log("TestPlugin make async2");
        callback();
    }, 1000);
});
2.3.3 banner-webpack-plugin
  • 获取文件资源: 通过 compilation.assets对象获取键名来得知文件名
  • 追加内容: 设置 compilation.assets[key]为对象,实现 source方法和 size方法来描述输出文件
// 给文件添加注释的插件
class BannerWebpackPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync(
      "BannerWebpackPlugin",
      (compilation, callback) => {
        // 1.获取输出的文件资源。只保留js和css资源
        const assets = Object.keys(compilation.assets).filter((assetPath) => {
          const extensions = ["css", "js"];
          const typeName = assetPath.split(".").slice(-1).join("");
          return extensions.includes(typeName);
        });

        // 资源内容上追加内容
        const prefix = `
        /*
          Author: xxx
        */
        `;

        // 2.在文件上追加内容
        assets.forEach((asset) => {
          // 获取原来内容
          const source = compilation.assets[asset].source();
          // 新内容
          const content = prefix + source;
          compilation.assets[asset] = {
            // 最终资源输出时调用source方法
            source() {
              return content;
            },
            // 资源大小
            size() {
              return content.length;
            },
          };
        });

        callback();
      }
    );
  }
}

module.exports = BannerWebpackPlugin;
2.3.4 clean-webpack-plugin

webpack4中有该插件,但是webpack5内置了该功能。在此尝试实现clean-webpack-plugin。

  • 获取webpack.config.js配置: compiler.options
  • 获取文件操作工具: fs = compiler.outputFileSystem
  • 获取目录下文件和文件夹: fs.readdirSync
  • 获取文件状态: fs.statSync
  • 判断文件是否是目录: fs.isDirectory
  • 删除文件: fs.unlinkSync
// 清理上次打包内容插件
class CleanWebpackPlugin {
  apply(compiler) {
    // 打包输出目录
    const outputPath = compiler.options.output.path;
    // 类似于fs
    const fs = compiler.outputFileSystem;

    compiler.hooks.emit.tap("CleanWebpackPlugin", (compilation) => {
      this.removeFiles(fs, outputPath);
    });
  }

  removeFiles(fs, filePath) {
    // 读取目录下所有文件和文件夹
    const files = fs.readdirSync(filePath);

    files.forEach((file) => {
      const path = `${filePath}/${file}`;
      //  分析文件状态
      const fileStat = fs.statSync(path);
      // 判断是否是文件夹(是文件夹则先删除子文件)
      if (fileStat.isDirectory()) {
        this.removeFiles(fs, path);
      } else {
        // 同步删除方法
        fs.unlinkSync(path);
      }
    });
  }
}

module.exports = CleanWebpackPlugin;
2.3.5 analyze-webpack-plugin

实现一个文件大小分析插件。实现需要API可以参考banner-webpack-plugin。

// 分析文件资源大小插件
class AnalyzeWebpackPlugin {
apply(compiler) {
 compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => {
   // 1.分析输出资源大小
   const assets = Object.entries(compilation.assets);

   // 2.生成分析md文件
   const baseContent = `| 资源名称 | 资源大小 |\n | --- | --- |`;
   const content = assets.reduce((content, [filename, file]) => {
     return content + `\n| ${filename} | ${file.size()} |`;
   }, baseContent);

   // 3.输出分析md文件
   compilation.assets["analyze.md"] = {
     source() {
       return content;
     },
     size() {
       return content.length;
     },
   };
 });
}
}

module.exports = AnalyzeWebpackPlugin;
2.3.6 inlineChunk-webpack-plugin

作用

配置了runtimeChunk后webpack打包生成的runtime文件可能非常小,可以考虑直接内联注入到index.html入口中。

思路

因为输出index.html是html-webpack-plugin。因此需要借助这个插件向index.html中追加内容

实现

  • 内联runtime文件中的内容到index.html
  • 删除打包后产生的runtime.js文件
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");

class InlineChunkWebpackPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap(
      "InlineChunkWebpackPlugin",
      (compilation) => {
        // 1.获取html-webpack-plugin内部的自定义hooks
        const hooks = HtmlWebpackPlugin.getHooks(compilation);

        // 2.根据其文档注册alterAssetTagGroups钩子(此时要标签已经分好组),将runtime内容内联到index.html
        hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
          /*  headTags
                [
                    {
                        tagName: 'script',
                        voidTag: false,
                        meta: { plugin: 'html-webpack-plugin' },
                        attributes: { defer: true, src: 'jsruntime~main.js.js' }
                    }
                ] 
          */

          // 处理头部标签和身体标签(不确定runtime.js会被html-webpack-plugin放到哪个部分)
          assets.headTags = this.getInlineChunk(
            assets.headTags,
            compilation.assets
          );
          assets.bodyTags = this.getInlineChunk(
            assets.bodyTags,
            compilation.assets
          );
        });

        // 3.根据其文档注册afterEmit钩子(此时已经输出资源),将产生的runtime.js删除
        hooks.afterEmit.tap("InlineChunkWebpackPlugin", () => {
          Object.keys(compilation.assets).forEach((filePath) => {
            if (/runtime(.*)\.js$/g.test(filePath))
              delete compilation.assets[filePath];
          });
        });
      }
    );
  }

  getInlineChunk(tags, assets) {
    return tags.map((tag) => {
      // 不是script标签不处理
      if (tag.tagName !== "script") return tag;

      // 获取文件资源路径
      const filePath = tag.attributes.src;
      if (!filePath) return tag;

      // 不是runtime文件不处理
      if (!/runtime(.*)\.js$/g.test(filePath)) return tag;

      return {
        tagName: "script",
        // assets是通过compilation获取文件资源
        innerHTML: assets[filePath].source(),
        closeTag: true,
      };
    });
  }
}

module.exports = InlineChunkWebpackPlugin;

3.调试

在构建工具中调试

1.构建工具代码中设置debugger断点

2.配置调试指令

  • -brk: 在第一行代码停下来
  • cli.js: 运行cli.js通过webpack脚手架启动webpack
{
    "scripts": {
        "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
    }
}