Skip to content

项目体积优化 ​

背景 ​

近期组内有对部分应用做资源排查, 计划做下瘦身. 在看具体的项目bundle 图时, 发现lodash 的资源有些过大, 占了100K 左右, 但是在具体代码里面其实也就用到了一两个函数, 所以这个资源的大小就有点不太正常.

解决方案 ​

由于没有前置知识, 所以我这里一开始用的是网上成熟的三方库, babel 的 babel-plugin-lodash 插件与 lodash-webpack-plugin 配合使用, 配置后也确实起到了一些作用, 体积减少了 20K.

于是周后上就说了下这个事情, 然后发现资源体积大的问题并不是想的那么简单. 因为项目中用的都是lodash-es 依赖, 而这个依赖自身就做了tree shaking , 所以打包后又出现lodash 资源的原因可能是依赖的依赖用到了lodash, 然后导致打包后出现了这个资源.

问题找到后就会发现之前的解决方案是不合适的, 所以有了下面的解决方案:

  • webpack打包的时候进行资源的解析, 将所有的lodash 资源解析到lodash-es 上

从底层上进行处理

具体实现 ​

1. 自己实现 ​

由于开始不知道webpack 有内置的api, 所以就自己根据网上的资料写了一个转换的插件, 具体如下:

该插件入参为数组, 支持多个依赖的转换

javascript
/**
 * options为转换数组
 * source: 需要转换的依赖
 * target: 转换为具体哪个依赖
 * exclude: 不过滤的依赖
 */
class DependenciesReplacementPlugin {
  constructor(options) {
    // 可选的插件配置
    this.options = options ?? [];
  }

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap('DependenciesReplacementPlugin', (factory) => {
      // 监听 module 创建过程
      factory.hooks.factorize.tap('DependenciesReplacementPlugin', (details) => {
        this.options.forEach(({ source, target, exclude = [] }) => {
          // 找到需要转换的依赖项
          if (details.request && source.test(details.request)) {
            // 进行不转换过滤
            if (!exclude.includes(details.request)) {
              // 重写模块的请求路径
              details.request = details.request.replace(source, target);
            }
          }
        });
      });
    });
  }
}

module.exports = DependenciesReplacementPlugin;

2. webpack 内置api实现 ​

后来知道了webpack 内置了一个api, 就将上面的插件删除使用了其内置的api, 具体实现如下

javascript
new webpack.NormalModuleReplacementPlugin(/^lodash$|^lodash\//, (resource) => {
  resource.request = resource.request.replace('lodash', 'lodash-es');
}),

即只需要一行代码即可, 根据正则匹配到具体代码, 然后进行路径的替换, 不需要另写插件

该api的实现原理和上面自己写的差不多, 但是多了一些兼容, 完整的api代码如下

javascript
/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const { join, dirname } = require("./util/fs");

/** @typedef {import("./Compiler")} Compiler */
/** @typedef {function(TODO): void} ModuleReplacer */

class NormalModuleReplacementPlugin {
	/**
	 * Create an instance of the plugin
	 * @param {RegExp} resourceRegExp the resource matcher
	 * @param {string|ModuleReplacer} newResource the resource replacement
	 */
	constructor(resourceRegExp, newResource) {
		this.resourceRegExp = resourceRegExp;
		this.newResource = newResource;
	}

	/**
	 * Apply the plugin
	 * @param {Compiler} compiler the compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		const resourceRegExp = this.resourceRegExp;
		const newResource = this.newResource;
		compiler.hooks.normalModuleFactory.tap(
			"NormalModuleReplacementPlugin",
			nmf => {
				nmf.hooks.beforeResolve.tap("NormalModuleReplacementPlugin", result => {
					if (resourceRegExp.test(result.request)) {
						if (typeof newResource === "function") {
							newResource(result);
						} else {
							result.request = newResource;
						}
					}
				});
				nmf.hooks.afterResolve.tap("NormalModuleReplacementPlugin", result => {
					const createData = result.createData;
					if (resourceRegExp.test(createData.resource)) {
						if (typeof newResource === "function") {
							newResource(result);
						} else {
							const fs = compiler.inputFileSystem;
							if (
								newResource.startsWith("/") ||
								(newResource.length > 1 && newResource[1] === ":")
							) {
								createData.resource = newResource;
							} else {
								createData.resource = join(
									fs,
									dirname(fs, createData.resource),
									newResource
								);
							}
						}
					}
				});
			}
		);
	}
}

module.exports = NormalModuleReplacementPlugin;

其中兼容的代码为

javascript
if (
  newResource.startsWith("/") ||
  (newResource.length > 1 && newResource[1] === ":")
) {
  createData.resource = newResource;
} else {
  createData.resource = join(
    fs,
    dirname(fs, createData.resource),
    newResource
  );
}

问了下AI, 知道了这里主要是用来处理不同平台路径标识符的问题的, 确保最后资源都指向为绝对路径

image-20231226161242135