loader

loader的调用与执行顺序

一个最简单的 loader 代码结构

定义:loader 只是一个导出为函数的JavaScript 模块

  1. module.exports = function(source) {
  2. return source;
  3. };

多 Loader 时的执行顺序

多个 Loader 串行执行
顺序从后到前

  1. module.exports = {
  2. entry: './src/index.js', output: {
  3. filename: 'bundle.js', path: path.resolve(__dirname, 'dist')
  4. }, + module: {
  5. + rules: [
  6. + {
  7. + test: /\.less$/, + use: [
  8. + 'style-loader', + 'css-loader', + ' less-loader' + ]
  9. + }
  10. + ]
  11. + }
  12. };

函数组合的两种情况

  • Unix 中的 pipline
  • Compose(webpack采取的是这种) compose = (f, g) => (…args) => f(g(…args));

使用loader-runner高效进行loader的调试

loader-runner 介绍

定义:loader-runner 允许你在不安装 webpack 的情况下运行 loaders

  • 作为 webpack 的依赖,webpack 中使用它执行 loader ·进行 loader 的开发和调试

    loader-runner 的使用

    1. import { runLoaders } from "loader-runner";
    2. runLoaders({
    3. resource: “/abs/path/to/file.txt?query”, // String: 资源的绝对路径(可以增加查询字符串)
    4. loaders: [“/abs/path/to/loader.js?query”], // String[]: loader 的绝对路径(可以增加查询字符串)
    5. context: { minimize: true }, // 基础上下文之外的额外 loader 上下文
    6. readResource: fs.readFile.bind(fs) // 读取资源的函数
    7. }, function(err, result) {
    8. // err: Error?
    9. // result.result: Buffer | String
    10. })

    开发一个 raw-loader

    1. //src/raw-loader.js:
    2. module.exports = function(source) {
    3. const json = JSON.stringify(source) .replace(/\u2028/g, \\u2028 ' ) // 为了安全起见, ES6模板字符串的问题
    4. .replace(/\u2029/g, '\\u2029');
    5. return `export default ${json}`;
    6. };
    7. //src/demo.txt
    8. foobar

    使用 loader-runner 调试 loader

    1. //run-loader.js:
    2. const fs = require("fs");
    3. const path = require("path");
    4. const { runLoaders } = require("loader-runner");
    5. runLoaders(
    6. {
    7. resource: "./demo.txt",
    8. loaders: [path.resolve(__dirname, "./loaders/rawloader")], readResource: fs.readFile.bind(fs), },
    9. (err, result) => (err ? console.error(err) : console.log(result))
    10. );
    11. //运行查看结果:
    12. node run-loader.js

    截屏2022-02-24 下午7.27.05.png

    复杂loader的开发场景

    loader 的参数获取

    通过 loader-utils 的 getOptions 方法获取

    1. const loaderUtils = require("loader-utils");
    2. module.exports = function(content) {
    3. const { name } = loaderUtils.getOptions(this);
    4. };

    loader 异常处理

    loader 内直接通过 throw 抛出
    通过 this.callback 传递错误

    1. this.callback(
    2. err: Error | null,
    3. content: string | Buffer,
    4. sourceMap?: SourceMap,
    5. meta?: any
    6. );

    loader 的异步处理

    通过 this.async 来返回一个异步函数

  • 第一个参数是 Error,第二个参数是处理的结果

    1. module.exports = function(input) {
    2. const callback = this.async();
    3. // No callback -> return synchronous results
    4. // if (callback) { ... }
    5. callback(null, input + input);
    6. };

    在 loader 中使用缓存

    webpack 中默认开启 loader 缓存

  • 可以使用 this.cacheable(false) 关掉缓存

缓存条件: loader 的结果在相同的输入下有确定的输出

  • 有依赖的 loader 无法使用缓存

    loader 进行文件输出

    通过 this.emitFile 进行文件写入

    1. const loaderUtils = require("loader-utils");
    2. module.exports = function(content) {
    3. const url = loaderUtils.interpolateName(this, "[hash].[ext]", {
    4. content, });
    5. this.emitFile(url, content);
    6. const path = `__webpack_public_path__ + ${JSON.stringify(url)};`;
    7. return `export default ${path}`;
    8. };

    开发自动合成雪碧图的loader截屏2022-02-24 下午8.46.30.png

    两张图片合成一张图片
    使用 spritesmith (https://www.npmjs.com/package/spritesmith))

    1. spritesmith 使用示例
    2. const sprites = ['./images/1.jpg', './images/2.jpg'];
    3. Spritesmith.run({src: sprites}, function handleResult (err, result) {
    4. result.image;
    5. result.coordinates;
    6. result.properties;
    7. });

    plugin

    插件的运行环境

  • 插件没有像 loader 那样的独立运行环境

  • 只能在 webpack 里面运行

    插件基本结构介绍

    ```javascript 基本结构:

class MyPlugin {//插件名称 apply(compiler) {//插件上的 apply 方法 compiler.hooks.done.tap(‘ My Plugin’, (//插件的 hooks stats / stats is passed as argument when done hook is tapped. / ) => { console.log(‘Hello World!’);//插件处理逻辑 }); } } module.exports = MyPlugin;

插件使用: plugins: [ new MyPlugin() ]

  1. <a name="Wgffw"></a>
  2. ## 开发一个简单插件
  3. <a name="kKiZp"></a>
  4. ### 搭建插件的运行环境
  5. ```javascript
  6. const path = require("path");
  7. const DemoPlugin = require("./plugins/demo-plugin.js");
  8. const PATHS = {
  9. lib: path.join(__dirname, "app", "shake.js"),
  10. build: path.join(__dirname, "build"), };
  11. module.exports = {
  12. entry: {
  13. lib: PATHS.lib,
  14. },
  15. output: {
  16. path: PATHS.build, filename: "[name].js", },
  17. plugins: [new DemoPlugin()],
  18. };

代码

//src/demo-plugin.js
module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options;
    }
    apply() {
        console.log("apply", this.options);
    }
};

//加入到 webpack 配置中
module.exports = {
    ... plugins: [new DemoPlugin({ name: "demo" })]
};

复杂插件开发场景

插件中如何获取传递的参数

通过插件的构造函数进行获取

module.exports = class MyPlugin {
  constructor(options) {
      this.options = options;
  }
  apply() {
      console.log("apply", this.options);
  }
};

插件的错误处理

参数校验阶段可以直接 throw 的方式抛出

  • throw new Error(“ Error Message”)

通过 compilation 对象的 warnings 和 errors 接收

  • compilation.warnings.push(“warning”);
  • compilation.errors.push(“error”);

    通过 Compilation 进行文件写入

    Compilation 上的 assets 可以用于文件写入

  • 可以将 zip 资源包设置到 compilation.assets 对象上

文件写入需要使用 webpack-sources (https://www.npmjs.com/package/webpack- sources)

const { RawSource } = require("webpack-sources");
module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
    const { name } = this.options;
    compiler.plugin("emit", (compilation, cb) => {//可能是compiler.hooks
      compilation.assets[name] = new RawSource("demo");
      cb();
    });
  }
};

插件扩展:编写插件的插件
插件自身也可以通过暴露 hooks 的方式进行自身扩展,以 html- webpack-plugin 为例:

  • html-webpack-plugin-alter-chunks (Sync)
  • html-webpack-plugin-before-html-generation (Async)
  • html-webpack-plugin-alter-asset-tags (Async)
  • html-webpack-plugin-after-html-processing (Async)
  • html-webpack-plugin-after-emit (Async)

    开发压缩为zip包的插件

    编写一个压缩构建资源为zip包的插件要求:

  • 生成的 zip 包文件名称可以通过插件传入

  • 需要使用 compiler 对象上的特地 hooks 进行资源的生成

    1.Node.js 里面将文件压缩为 zip 包

    使用 jszip (https://www.npmjs.com/package/jszip))

    var zip = new JSZip();
      zip.file("Hello.txt", "Hello World\n");
      var img = zip.folder("images");
      img.file("smile.gif", imgData, {base64: true});
      zip.generateAsync({type:"blob"}).then(function(content) {
      // see FileSaver.js
      saveAs(content, "example.zip");
    });
    

    2.Compiler 上负责文件生成的 hooks

    Hooks 是 emit,是一个异步的 hook (AsyncSeriesHook)
    emit 生成文件阶段,读取的是 compilation.assets 对象的值

  • 可以将 zip 资源包设置到 compilation.assets 对象上