Monorepo 中如何正确共享工具函数而不打包重复代码?
我用 pnpm 搭了个 Monorepo,里面有个 shared 包导出了一些工具函数,但在 apps 里引用时发现每个子项目都把 shared 的代码打包进去了,导致体积变大。明明 shared 是 ESM 格式,也配了 exports 字段,咋还是被重复打包?
比如我在 shared 里写了:
export const formatDate = (date) => {
return new Date(date).toLocaleDateString();
};
export const debounce = (fn, delay) => {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
};
然后在 app1 里 import { formatDate } from ‘@myorg/shared’,结果 vite 打包后发现整个 shared 都被打进来了,哪怕我只用了一个函数。是不是我的 package.json 配置有问题?
先说原理,为什么会被整包打进去。Tree-shaking 能生效需要满足几个条件,必须是 ESM 格式、package.json 要声明 sideEffects、还有 exports 或 main 字段要指向正确的入口文件。缺一个都不行。
咱们先看看 shared 包的 package.json 应该怎么配:
这里需要注意几个关键点。sideEffects 设为 false 是告诉打包器这个包没有副作用,可以放心地 tree-shaking。type 设为 module 确保 Node 把它当 ESM 处理。exports 字段要明确指定 import 条件。
但光配这个还不够,你的构建产物本身也得是 ESM 格式。很多人用 tsup 或者 unbuild 来打包 shared 包,配置不对的话打出来的可能是 CJS 或者伪 ESM。
我推荐用 tsup,配置简单:
如果你不想打包,想直接用源码,那也可以。Vite 是支持直接引用 TypeScript 源文件的,但需要配 resolve.alias 或者用 workspace 协议。不过我更建议打包一下,这样类型声明和入口都清晰。
还有个容易忽略的点,你的工具函数写法也得注意。比如你那个 debounce,如果写成 default export 就会影响 tree-shaking 效果:
然后是 Vite 那边的配置,确保它正确解析你的 monorepo 依赖:
如果你用的是 pnpm workspace,记得在根目录的 pnpm-workspace.yaml 里正确声明包位置,然后 shared 包的依赖关系要用 workspace 协议:
最后教你个排查方法。打包完成后,用 rollup-plugin-visualizer 或者直接看 dist 目录的文件大小,如果 shared 相关的代码比预期大很多,说明 tree-shaking 没生效。
还可以在 Vite 打包时加上 --debug 参数,或者用 ANALYZE=true vite build 配合 rollup-plugin-visualizer 看看具体哪些代码被打进去了。
总结一下关键点:package.json 要配 sideEffects: false 和正确的 exports、构建产物必须是纯正 ESM、用 named export、确保 pnpm workspace 链接正确。按这个路子走,你的 shared 包就能被正确地按需打包了。