统一资源引用路径
使用 webpack-bundle-analyzer
分析umi项目的构建产物,突然从分析界面中发现。es 和 lib中的文件都被打包进来,并且有很多重复文件。
查看 package.json
文件,我们可以看到,lib目录是 commonjs
产物, es目录是 es module
产物,且专门声明了 "sideEffects": false
来启用webpack的 tree shaking
。 逐一分析了几个依赖了@ant-design/icons
的组件仓库,找到了问题所在: 从绝对路径的图标代码文件中导入的,有 es 也有 lib。
js
import { createFromIconfontCN } from '@ant-design/lib/icons';
import { createFromIconfontCN } from '@ant-design/es/icons';
想要从源头解决这个问题,其实是很难的。我们不可能把这么多组件中所有的import都去改一遍,也没办法限制用户使用同一种方式去导入图标。所以只能想办法去修正这些引用路径,统一从一个地方去导入。
webpack的 resolve.alias
配置项可以让开发者给模块设置导入时的别名,从而用一个简短的别名来代替长路径的导入。
我们可以借助这一特性来将所有的 @ant-design/icons/lib
路径替换为 @ant-design/icons/es
,这样整个图标都可以从 es 目录引入,从而避免代码被重复打包。
arduino
// config/config.ts 或 umirc.ts
export default defineConfig({
alias: {
'@ant-design/icons/lib': '@ant-design/icons/es',
},
})
经过如上改造后,图标库体积从83kb降到了47kb,重复代码减少了43%。
拯救lodash等无法被tree shaking的包
lodash
这个工具包大家一定不陌生,但你知道lodash并不支持esm吗?是的,它目前只支持了commonjs模块语法,所以并不能天然被 tree shaking。
json
{
"name": "lodash",
"version": "4.17.21",
"main": "lodash.js"
}
目前,常用的 lodash
代码精简手段包括:
- 手动按需引入
import merge from 'lodash/merge'
- 手动单个函数模块引入
import merge from 'lodash.merge'
- 使用 esm 版本的
lodash-es
,import { merge } from 'lodash-es'
- 使用
babel-plugin-lodash
将全量引入的lodash转换为按需引入的形式
另外还有一个小妙招,那就要请出本篇文章的主角 alias 了。如下所示,我们直接把 lodash
的别名配置为 lodash-es
。
php
export default defineConfig({
alias: {
lodash: 'lodash-es',
},
})
这样配置后,当代码中出现 import { merge } from 'lodash';
这样的语句时,webpack 会将其解析为 import { merge } from 'lodash-es';
,从而使 tree shaking 生效。
修改前,代码体积是111kb,修改后,代码体积仅有42kb,减少了69kb。
风险提示
- 兼容性问题
项目中可能存在多个第三方依赖同时使用了lodash
的情况,各自使用的版本也各不相同。强行把这些不同版本的lodash
替换为我们安装的lodash-es
,可能会出现不兼容的情况。 - 构建工具和插件
某些构建工具或插件可能对 lodash 有特定的优化,而这些优化可能不适用于 lodash-es。
常规用法
resolve.alias
在项目中有几个实际的好处:
- 简化模块引用 :当你的项目结构变得复杂时,你可能需要频繁地使用相对路径去引用模块,例如
import '../../../utils/my-util'
。通过设置别名,你可以简化这个引用,例如import 'Utils/my-util'
。 - 提高可维护性:如果你决定改变某个模块的位置,通常你需要更新所有引用该模块的文件中的路径。使用别名可以让你只在webpack配置中更新一次路径。
- 避免相对路径混乱 :在大型项目中,过多的相对路径(如
../../..
)会使代码难以理解和维护。别名提供了一种清晰的方式来表示模块的位置。 - 模块解析优化:在某些情况下,你可能想要针对不同的环境或目标指定不同的模块实现。通过别名,你可以在构建时决定使用哪个模块版本。
- 与第三方库集成:有时候,你可能需要覆盖第三方库中的某些文件或模块。通过设置别名,你可以重定向这些导入到你自己的实现中。
- 支持模块重构:当你重构项目,将代码分割成不同的包或模块时,别名可以帮助你更平滑地过渡,因为你可以保持导入语句不变,即使文件的实际位置已经改变。
下面是一个resolve.alias
的配置示例:
java
module.exports = {
// ...
resolve: {
alias: {
'@components': path.resolve(__dirname, 'src/components/'),
'@utils': path.resolve(__dirname, 'src/utils/'),
// 可以指定特定文件的别名
'module-to-mock': path.resolve(__dirname, 'mocks/module-to-mock.js')
}
}
};
在这个配置中,任何时候你导入@components
或@utils
,webpack都会自动解析到对应的目录。这样,你就不需要写出完整的相对或绝对路径,从而使得代码更加简洁和易于维护。