class.forname()中要写相对路径吗?_为什么要写 Webpack 插件

59593c2f99c65e701bd7a86d6af9bfd0.png

本文只会针对 Webpack 插件展开,没有 Webpack 基础的同学慎入,讨论的版本是 Webpack4。下面我将通过 4 个部分拆解 Webpack 插件。

  • What,什么是 Webpack 插件?
  • How,怎么编写一个 Webpack 插件
  • Why,为什么要编写 Webpack 插件
  • Where,Webpack 插件实战,离线包

什么是 Webpack 插件

一个标准的 Webpack4 配置应该是下面这样的。

module.exports = {
  mode: "development",
  entry: {},
  output: {},
  optimization: {},
  module: {},
  plugins: [],
};

我们知道在 Webpack 中,每一个资源(asset)就是一个模块,所以对资源的操作都可以交由module完成,但是打包相关的或者热更新等,module都无法完成,而 Webpack 插件支持在程序各个阶段执行,这主要基于 Tapable,Webpack 基于它暴露各个阶段的钩子,这些钩子可以通过 Compiler Hooks 这个链接查看,它支持同步和异步。

怎么编写一个 Webpack 插件

我们观察到 Webpack 配置中 plugins 是一个数组,数组中每一项都是 Webpack 插件的实例。Webpack 遍历执行每一个插件,然后调用插件的apply方法,并传递compiler对象,Webpack 插件必须实现apply方法。

下面我们看一下最简单的 Webpack 插件

class SimpleWebpackPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    // 一般约定tab里的第一个字符串与插件名相同
    compiler.hooks.done.tap("SimpleWebpackPlugin", (stat) => {
      console.log(this.options.name + " compiler done");
    });
  }
}

在 Webpack 配置中新增一项

const SimpleWebpackPlugin = require("./SimpleWebpackPlugin");

module.exports = {
  plugins: [
    new SimpleWebpackPlugin({
      name: "webpack",
    }),
  ],
};

为什么要编写 Webpack 插件

有人可能会好奇,编写 Webpack 插件还需要问为什么吗?那是没见过那些不懂 Webpack 机制的,比如一个 cra(create-react-app)搭建的项目,直接一手yarn run eject,然后就开始在项目之间魔改。

下面的例子不是本人!比如想在项目打完包之后将文件压缩生成 zip 文件,然后上传 zip 到服务器,直接找到项目中Webpack compiler的build函数,在 build 函数的回调里生成 zip 文件,这也是典型的here, it works,又不是不能跑。这有什么缺点?不够优雅?还有其他的吗,难道不够优雅还不够吗。如果谁都在构建配置中随意添加功能,性能是一方面,维护和扩展性也很差。而通过 Webpack 插件的形式简单,我们甚至不需要执行操蛋的yarn run eject

Webpack 插件实战,离线包

下面我们来实战写一个离线包插件。介绍一下离线包的使用场景,一个 App 里很多页面是 h5 的,如果每次都要通过网络读取远程静态资源,会造成页面白屏时间较长,且造成流量浪费,我们可以通过一个离线的 zip 包提前将资源打包好,这样 App 每次只需要从本地下载好的离线包读取数据就行。再优化一点,对新旧资源做差分检查,每次只将代码改变的部分打成 zip 包,并更新新旧资源的映射表(需要服务端配合)。反正离线包就是为了优化资源访问。

话不多说,直接上代码

// OfflineZipWebpackPlugin.js

const fs = require("fs-extra");
const path = require("path");
const JSZip = require("jszip");
const { RawSource } = require("webpack-sources");

const zip = new JSZip();

// 必须放在htmlWebpackPlugin之后,否则html不再编译阶段

/**
 * 生成zip离线包脚本,通过callback回调上传离线包
 * @param filename
 * @param outputPath
 * @param callback
 * @param excludeDir
 */

module.exports = class OfflineZipWebpackPlugin {
  constructor(options) {
    this.options = options;
  }

  excludeFiles(files) {
    const { excludeDir } = this.options;
    const newFiles = {};
    if (Array.isArray(excludeDir)) {
      excludeDir.forEach((dirName) => {
        for (let path in files) {
          if (!path.includes(dirName)) {
            newFiles[path] = files[path];
          }
        }
      });
      return newFiles;
    }
    return files;
  }

  apply(compiler) {
    const { filename, callback, outputPath } = this.options;
    let newOutputPath = "";
    compiler.hooks.emit.tapAsync(
      "OfflineZip",
      (compilation, pluginCallback) => {
        const folder = zip.folder(filename);

        for (let filename in compilation.assets) {
          const source = compilation.assets[filename].source();
          folder.file(filename, source);
        }

        zip.files = this.excludeFiles(zip.files);

        zip
          .generateAsync({
            type: "nodebuffer",
          })
          .then((content) => {
            if (!fs.existsSync(outputPath)) {
              fs.mkdirSync(outputPath);
            } else {
              fs.emptyDirSync(outputPath);
            }

            newOutputPath = path.join(outputPath, filename + ".zip");

            const relativePath = path.relative(
              compilation.options.output.path,
              newOutputPath
            );
            // 需要相对路径
            compilation.assets[relativePath] = new RawSource(content);
            console.log("newOutputPath", newOutputPath);

            pluginCallback();
          });
      }
    );

    compiler.hooks.done.tap("OfflineZip", () => {
      if (callback) callback(newOutputPath);
    });
  }
};

webpack配置

// webpack.config.js

module.exports = {
  plugins: [
    new OfflineZipWebpackPlugin({
      filename: paths.zipFileName,
      outputPath: paths.zipDir,
      excludeDir: ["/static/media"],
      callback: (zipPath) => {
        uploadFile(zipPath);
      },
    }),
  ],
};

解释一下OfflineZipWebpackPlugin构造函数里传递的参数

  • filename zip文件名
  • outputPath zip包文件输出到哪个目录
  • excludeDir 可忽略某一路径下的文件
  • callback 打包成功之后的回调

说一下OfflineZipWebpackPlugin类的apply方法里都干了哪些事。

  • 通过compiler.hooks.emit.tapAsync监听文件即将要构建(emit)的钩子。
  • 将一个个资源对象都加到jszip的实例里。folder.file(filename, source);
  • 再用jszipgenerateAsync方法将内容(content)以nodebuffer的方式输出
  • webpack-sourcesRawSource封装对象,再赋值到compilation.assets
  • 调用pluginCallback,通知Webpack,钩子回调已经支持完毕。
  • 最后监听compiler.hooks.done.tap,当编译完成时,触发回调,将zip包上传到远程服务器。

这里有一个小矛盾,callback函数到底要不要放到OfflineZipWebpackPlugin里,这个看个人喜好吧。如果说你崇尚高内聚松耦合,那放在一起更容易维护。如果你是Kiss(Keep it simple and stupid)原则的拥簇者,那可以新写一个AfterOfflineZipWebpackPlugin,单独处理离线包的上传。

最后的最后

其实编写一个Webpack插件很简单,但是要理解Webpack插件的机制就需要理解Tapable机制,它利用了发布订阅模式,或者说观察者模式,很好的解藕了分布者和订阅者之间的联系,他们不用互相关心,交给中间方就行。

文章中用到了几个模式的术语,如果想了解设计模式,可以看这篇文章。

成楠Peter:设计模式之美-前端​zhuanlan.zhihu.com
ac8c2a934dda6c4760dfdf66aab88cb4.png

Webpack的学习推荐极客专栏,玩转Webpack。它不仅非常全面的介绍了Webpack相关的知识,还介绍了工程管理相关的内容,相信如果你认真看完这个专栏,你一定会成为一个Webpack配置工程师加工程管理工程师。

0165081abce44b28dd34d5846740f8d1.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值