某天,抽取组件之后,顺利接入到项目内,想着终于搞定了。提交代码准备编译一波,给测试同学测试看看。没想到,编译之后直接收到了包体积增大告警。组件接入之后包体积增大了1.76MB。
虽然组件确实有那么点复杂, 但是不至于有这么多代码吧。怀揣着忐忑的心,看看到底是编了个啥进去。 经过使用webpack-bundle-analyzer分析,组件原来的代码确实不多。那肯定就是依赖的锅了。
依赖
引入组件之后很多的代码都是由依赖引起的。可以根据情况优化依赖关系。
考虑暴露给外部实现
这个组件有个依赖是「用户详情展示弹窗」,是个体积很大的组件。
一方面,业务方本身也会有这个弹窗的实现。组件内部再次依赖,可能导致打包多余的代码。
另一方面,这个弹窗本身也可能包含一些业务定制功能,放在组件内部也不一定合适。
所以,第一步先把「用户详情展示弹窗」弹出能力转包给业务方实现看看。
通过一个plugin的机制,把弹窗的能力注册进来,内部直接调用即可。
// 插件定义
interface BasePlugin {
onCreate();
onDestroy();
run();
}
// 插件实现
class ReportPlugin extends BasePlugin {
run(key, actions) {
collectEvent(key, actions);
}
}
// 插件使用
onclick() {
getPlugin('report').run('xx', 'click');
}
// 插件维护
let pluginMap = {};
function registerPlugin(key, plugin) {
pluginMap[key] = plugin;
}
function unregisterPlugin(key) {
delete pluginMap[key];
}
function getPlugin(key) {
return pluginMap[key];
}
编译之后,包体积从9.3MB到8.23MB,少了1MB,效果还是很明显。
当然,也不是所有体积大的依赖,都应该放在外面实现,还是要考虑组件使用的便利性。
peerDependence
有些依赖,在组件使用场景内,业务方基本都会有用到。组件内部可以直接使用peerDependence来依赖。这样相关的依赖就不会被安装在组件下的node_modules,而是和宿主使用同一个包。
尤其是对有些存在全局影响的npm包,更应该直接考虑使用peerDependence,比如antd这种基础组件可能会修改全局的样式,如果组件内版本和宿主版本不一致可能导致最终样式出现异常。
"peerDependencies": {
"xxx": ">=1.9.0"
},
组件内部这样申明,在开发阶段也不会下载依赖,所以开发时需要手动安装。可以在devDependencies内申明一个具体的依赖:
"peerDependencies": {
"xxx": "1.9.2"
},
依赖版本一致
依赖声明范围放宽
因为yarn会扁平化管理npm依赖,如果宿主和组件的依赖版本是一致,那么组件内部就会直接使用外部的依赖。所以,对于某些不同版本兼容性比较好的依赖库,可以把依赖声明放宽一些,比如使用^2.0.0
而不是指定具体版本。
如果某个版本之后存在兼容性问题,也可以使用>=2.0.0 <=2.3.3
的方式来指定范围。
monorepo组件库的依赖,尽量一致
对于使用monorepo方式维护组件的仓库,所有组件原则上应该依赖相同版本的依赖库。因为业务方很可能不止使用其中一个组件。当需要安装多个组件时,相同的依赖库版本,可以减少安装不同版本依赖的可能性。
但是这种情况也有个badcase,如果业务方本身依赖了一个1.0.0的库A,在组件库内部依赖了^1.1.0的库A,yarn安装时会导致每个组件下都有个库A的依赖,而且版本是一样的。之后webpack构建的时候就会认为这些依赖是不一样的,而把相同的库打包多次。
// 一个 badcase:
--node_modules
--A@1.0.0
--B
--A@1.1.0
--C
--A@1.1.0
对于这种case还没有想到特别好的解法。而且这种问题比较隐蔽,如果没有监控,不一定能够及时发现。
一种思路是,使用webpack插件来监测如果存在这种情况,给业务方一个预警。但是这种需要业务方接入,不一定能够很好地执行。
组件内部依赖申明范围尽可能大一些,也可以一定程度规避这种问题。
国际化文案
目前我们国际化文案有16种语言,全量打包进去,对包体积会有一定的增大。而且大部分情况下都是使用一个语言,其他的资源都是无效的。
以我这个组件为例:整体包大小为25kb,文案大小4kb。占比16%.
可以考虑把多语言包动态加载,组件内部导出各个语言包的内容。在使用时,由调用者在外部传入需要使用的语言包。
Loadable({
loader: () =>
Promise.all([
import(
/* webpackChunkName:"my-component" */ '@com/my-component',
),
import (`@com/my-component/es/i18n/locales/${locale}.js`)
]),
loading: xx
render: xxx
});
TreeShaking
组件内部有些代码可能并不是必须的,如果没有设置好TreeShaking,会导致业务方编译时把所有的代码都打包进来。
关键是在package.json里的设置好SideEffect。
// ...
"sideEffects": [
"**/*.css",
"**/*.scss",
"./esnext/index.js",
"./esnext/configure.js"
],
// ...
如果组件会存在多种样式,每次必然只会使用其中一种,也可以考虑导出多个入口。便于根据需要使用。
结语
如果上面的一些建议还是无法把代码缩小到可接受范围,就只能继续使用webpack-bundle-analyzer来逐步分析具体依赖了,GoodLuck!
有其他的思路也欢迎补充:)
本文首发于 一粟(https://www.zeyio.com),欢迎转载,但是必须保留本文的署名和链接。