webpack plugin源码解析(七) html-minimizer-webpack-plugin、json-minimizer-webpack-plugin

作用

  • 压缩html文件
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");

new HtmlMinimizerPlugin()

涉及 webpack API

  • compilation.getCache:获取缓存

    • 具体查看 copy-webpack-plugin 解析文章
  • compiler.webpack.ModuleFilenameHelpers.matchObject:匹配文件方法

    • 具体查看 copy-webpack-plugin 解析文章

实现 html-minimizer-webpack-plugin

constructor

constructor(options) {
  validate(
  schema, options || {}, {
    name: "Html Minimizer Plugin",
    baseDataPath: "options"
  });
  const {
    minify = htmlMinifierTerser, // 默认使用 html-minifier-terser 库进行压缩
    minimizerOptions, // 压缩参数
    parallel = true,
    test = /\.html(\?.*)?$/i, // 匹配 html 文件
    include,
    exclude
  } = options || {};

  let minimizer;
  
  if (Array.isArray(minify)) {
    minimizer = minify.map((item, i) => {
      return {
        implementation: item,
        options: Array.isArray(minimizerOptions) ? minimizerOptions[i] : minimizerOptions
      };
    });
  } else {
    minimizer =
    {
      implementation: minify,
      options: minimizerOptions 
    };
  }
  
  this.options = {
    test,
    parallel,
    include,
    exclude,
    minimizer
  };
}

apply

apply(compiler) {
  const pluginName = this.constructor.name;
  // 获取 cpu 多核数量,用于创建 worker
  const availableNumberOfCores = HtmlMinimizerPlugin.getAvailableNumberOfCores(this.options.parallel);
  
  compiler.hooks.compilation.tap(pluginName, compilation => {
    compilation.hooks.processAssets.tapPromise({
      name: pluginName,
      stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, // 优化现有 asset 大小,例如进行压缩或者删除空格阶段
      additionalAssets: true
    }, assets => 
      this.optimize(compiler, compilation, assets, {
      availableNumberOfCores
    }));
    
    compilation.hooks.statsPrinter.tap(pluginName, stats => {
      stats.hooks.print.for("asset.info.minimized").tap("html-minimizer-webpack-plugin", (minimized, {
        green,
        formatFlag
      }) => minimized ?
      /** @type {Function} */
      green(
      /** @type {Function} */
      formatFlag("minimized")) : "");
    });
  });
}

getAvailableNumberOfCores

  static getAvailableNumberOfCores(parallel) {
    // In some cases cpus() returns undefined
    // https://github.com/nodejs/node/issues/19022
    const cpus = os.cpus() || {
      length: 1
    };
    return parallel === true ? cpus.length - 1 : Math.min(Number(parallel) || 0, cpus.length - 1);
  }

optimize 压缩过程

获取 html 文件资源

async optimize(compiler, compilation, assets, optimizeOptions) {
	
	const cache = compilation.getCache("HtmlMinimizerWebpackPlugin");
	let numberOfAssets = 0;
	
	// 筛选 html 文件,获取源文件信息以及设置对应的缓存
	const assetsForMinify = await Promise.all(Object.keys(assets).filter(name => {
      const {
        info
      } =
      compilation.getAsset(name); // Skip double minimize assets from child compilation

      if (info.minimized) { // 通过插件压缩后会设置 minimized 参数,避免重复压缩
        return false;
      }
	  // webpack 提供的匹配文件名的方法
      if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
      undefined, this.options)(name)) {
        return false;
      }

      return true;
    }).map(async name => {
      const {
        info,
        source // 这里的 source 是源文件字符串,非 buffer
      } =

      compilation.getAsset(name);
      // 根据文件内容生成 hash
      const eTag = cache.getLazyHashedEtag(source);
      // 返回|新建缓存
      const cacheItem = cache.getItemCache(name, eTag);
      const output = await cacheItem.getPromise();

      if (!output) {
        numberOfAssets += 1; // 获取需要压缩的 html 资源数量
      }

      return {
        name,
        info,
        inputSource: source,
        output,
        cacheItem
      };
    }));
	
	if (assetsForMinify.length === 0) { // 不需要压缩退出
      return;
    }
	
	// 通过 Worker 进行压缩
	// ...
}

创建 Worker

  • 通过 jest-worker 创建 Worker,通过 Worker 线程执行压缩任务,避免在主线程中进行耗时的计算操作
async optimize(compiler, compilation, assets, optimizeOptions) {
	// 筛选 html 资源等操作
	
	let getWorker;
	let initializedWorker;
	let numberOfWorkers;
	
	if (optimizeOptions.availableNumberOfCores > 0) { // 获取到的系统 cup 数量 - 1 
		// 如果 html 文件数量小于系统 cup 数量,那么根据 html 文件数量创建 worker 数量
		numberOfWorkers = Math.min(numberOfAssets, optimizeOptions.availableNumberOfCores);
	
	   getWorker = () => { // 创建 worker 任务
        if (initializedWorker) { // 避免重复创建
          return initializedWorker;
        }
		
		// 通过 worker 线程执行对应的方法
        initializedWorker = new Worker(require.resolve("./minify"), {
          numWorkers: numberOfWorkers,
          enableWorkerThreads: true
        }); // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
		
		// 在开发过程中更加方便地调试和排查问题

        const workerStdout = initializedWorker.getStdout();
		
        if (workerStdout) {
          workerStdout.on("data", chunk => process.stdout.write(chunk)); // 监测主线程输出
        }

        const workerStderr = initializedWorker.getStderr();

        if (workerStderr) {
          workerStderr.on("data", chunk => process.stderr.write(chunk)); // 监测主线程错误输出
        }

        return initializedWorker;
      };
	}
	
	// 执行压缩任务
	// ...
}

主线程派发压缩任务

async optimize(compiler, compilation, assets, optimizeOptions) {
	// ...
	
	const {
      RawSource //创建 webpack 格式文件
    } = compiler.webpack.sources;
    
    const scheduledTasks = [];
    
    // 筛选出的每个 html 文件都是一个任务
    for (const asset of assetsForMinify) {
      scheduledTasks.push(async () => {
        const {
          name,
          inputSource,
          cacheItem
        } = asset;
        
        let {
          output
        } = asset;
        
        let input;
        const sourceFromInputSource = inputSource.source(); // 源文件字符串内容,非 buffer

        if (!output) {
          input = sourceFromInputSource;

          if (Buffer.isBuffer(input)) { // 将 buffer 内容转换成字符串
            input = input.toString();
          }
          
          const options = {
            name,
            input,
            minimizer: this.options.minimizer
          };

          try {
          	// serialize 通过 serialize-javascript 库将对象序列化成字符串,因为是跨线程传递参数
          	// 通过 jest-wokrer 库在创建的worker线程中执行 minify.js 文件导出的 transform 方法
            output = await (getWorker ? getWorker().transform(serialize(options)) : minifyInternal(options));
          } catch (error) {
            compilation.errors.push(
            /** @type {WebpackError} */
            HtmlMinimizerPlugin.buildError(error, name));
            return;
          }
		  // 获取压缩结果
          output.source = new RawSource(output.code);
          // 将结果进行缓存
          await cacheItem.storePromise({
            source: output.source,
            errors: output.errors,
            warnings: output.warnings
          });
        }
		// 表示文件已压缩过了,后续跳过
        const newInfo = {
          minimized: true
        };

        if (output.warnings && output.warnings.length > 0) {
          for (const warning of output.warnings) {
            compilation.warnings.push(HtmlMinimizerPlugin.buildWarning(warning, name));
          }
        }

        if (output.errors && output.errors.length > 0) {
          for (const error of output.errors) {
            compilation.errors.push(HtmlMinimizerPlugin.buildError(error, name));
          }
        }
		// 更新源文件内容为压缩后内容、添加文件信息info
        compilation.updateAsset(name, output.source, newInfo);
      });
    }
	// 设置一次执行的任务数量
	const limit = getWorker && numberOfAssets > 0 ? numberOfWorkers : scheduledTasks.length;

    await throttleAll(limit, scheduledTasks); // 一次执行的任务数量,具体实现查看 copy-webpack-pulgin

    if (initializedWorker) {
      await initializedWorker.end(); // 压缩完成后关闭 worker 线程
    }
}

Worker 线程执行压缩任务

getWorker().transform

async optimize(compiler, compilation, assets, optimizeOptions) {
	// ...
	output = await (getWorker ? getWorker().transform(serialize(options)) : minifyInternal(options));
	// ...
}
  • 因为是跨线程传递参数,需要序列化和反序列化参数
  • 通过 new Function 将传递的字符串对象,反序列化还原成对象

async function transform(options) { // option 被 serialize-javascript 序列化成了字符串
  // 'use strict' => this === undefined (Clean Scope)
  // Safer for possible security issues, albeit not critical at all here
  // eslint-disable-next-line no-new-func, no-param-reassign
  
  // 通过 new Function 将传递的字符串对象,反序列化还原成对象
  const evaluatedOptions = new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`)(exports, require, module, __filename, __dirname);
  
  return minify(evaluatedOptions);
}

module.exports = {
  minify,
  transform
};

minify

const minify = async options => {
  const result = {
    code: options.input,
    warnings: [],
    errors: []
  };
  const transformers = Array.isArray(options.minimizer) ? options.minimizer : [options.minimizer];

  for (let i = 0; i <= transformers.length - 1; i++) {
    const {
      implementation
    } = transformers[i]; 
	
	// 通过 html-minifier-terser 压缩
    const minifyResult = await implementation({
      [options.name]: result.code
    }, transformers[i].options);
    
	// 是否压缩成功,压缩成功返回对象
    if (Object.prototype.toString.call(minifyResult) === "[object Object]" && minifyResult !== null && "code" in minifyResult) {
      result.code = minifyResult.code;
      result.warnings = result.warnings.concat(minifyResult.warnings || []);
      result.errors = result.errors.concat(minifyResult.errors || []);
    } else {
      // @ts-ignore
      result.code = minifyResult;
    }
  }

  return result;
};

json-minimizer-webpack-plugin

  • 代码和 html-minimizer-webpack-plugin 逻辑一致,区别在于压缩方式
// ...
output = await minify(options);
// ...

通过 JSON.stringify 的第三个参数进行压缩

const result = JSON.stringify(JSON.parse(input), replacer, space); // 第三个参数 space 控制缩进和插入,当为 undefiend 时,没有缩进,实现压缩
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值