项目体积优化
背景
近期组内有对部分应用做资源排查, 计划做下瘦身. 在看具体的项目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, 知道了这里主要是用来处理不同平台路径标识符的问题的, 确保最后资源都指向为绝对路径

