G2 是蚂蚁集团 AntV 团队开源的企业级可视化框架,这周的某一天突然有三个业务方同时找到我,说项目中 G2 的图表线上和线下不一致:比如线上的一个饼图在改变图表大小的时候,会出现如下图"飞出去"的情况:
排查问题
这种线上和线下不一致的问题不太容易排查:用户没有办法给到一个最小的图表复现 Demo,不能排除很多非 G2 代码本身的问题,比如浏览器版本,使用的框架等。针对这种问题,比较好的办法就是让业务方给你一个预览链接,然后你自己去调试。
定位问题
否定了如下一些错误的猜想之后:
- 浏览器版本太低
- 本地和线上构建工具不一致
- ...
突然发现线上打包后的代码少了一行很重要的代码:
js
runtime.enableCSSParsing = false;
也就是 G2 代码中 src/index.ts
中的这一行代码,在打包后就消失了!
这里简单解释一下这行代码的作用:关闭底层渲染引擎 @antv/g 的解析 css 属性的能力。这个能力对 G2 的性能有较大的影响,所以需要关闭。如果不关闭,目前 G2 代码中的一些属性解析就会有问题,从而导致线上代码和线下的运行不一致。
那么问题就是:为啥这行打包之后就没了?
我突然意识到,这可能和这个 PR 新增的按需打包的能力有关系,package.json 中的 sideEffects 应该写错了!
json
{
"sideEffects": ["src/index.ts"]
}
什么是 Tree Shaking
在介绍 sideEffects 之前,得先了解一个概念:Tree Shaking。
Tree Shaking 这个概念最开始是打包工具 Rollup 提出来的,后来 Webpack 等打包工具也支持这个能力。简单来讲,就是一种在打包过程中去掉没有使用的代码(dead-code),从而减少代码体积的手段。需要注意的是,这里的源代码需要使用 ESM 模块系统。
比如使用了一个名叫 math.js 的第三方库,这个库导出了两个方法:
javascript
// index.js
export function add(a, b) {
return a + b;
}
export function sub(a, b) {
return a - b;
}
在 vector.js 项目中只使用了 add 方法:
js
// vectorAdd.js
import { add } from 'math.js'
export function vectorAdd([x, y], [x1, y1]) {
return [add(x, x1), add(y, y1)]
}
打包工具默认会把 math.js 里面的 add 和 sub 都打包进来了,那如何不打包 sub 函数呢?这就需要指定 math.js 项目中 package.json 的 sideEffects 为 false
:
json
{
"sideEffects": false
}
这样 sub 函数就不会出现在最后的构建产物里面了,这就是所谓的 Tree Shaking。
什么是 sideEffects
可是发现,为了使用 Tree Shaking 的能力,我们需要指定 package.json 的 sideEffects 字段,这字段是什么?
这个字段出现是因为:打包工具不能完全确定文件中哪些是无用代码,需要开发者提供更多信息。上面的 sub 函数可以很容易确定是无用代码,因为 vectorAdd.js 这个文件中没有任何函数依赖它。但是文件中没有任何函数依赖就是无用代码了吗?
答案是不是。
G2 中 src/index.ts
中的 runtime.enableCSSParsing = false;
就是很好的一个例子。src/index.ts
没有任何函数依赖它,但是它却不是无用代码:因为我们知道,@antv/g 的某些文件依赖了这个变量。换句话说,这行代码就是有副作用(SideEffect) 的,不能被 Tree Shaking 掉。
为了解决这个问题,sideEffects 这个字段就诞生了:指定项目有副作用的文件。
为了安全,默认项目中所有的文件都有副作用。如果项目中所有的文件都没有副作用,比如 math.js,就可以设置 sideEffects 为 false
。如果项目中某些文件有副作用,那么可以通过一个数组指定有副作用的文件,而其余的文件都没有副作用,比如 G2 只用 src/index.ts
有副作用,就可以如下声明 sideEffects:
json
{
"sideEffects": ["src/index.ts"]
}
然后就出问题了!
解决办法
上面的写法乍一看完全没有问题,G2 代码确实就这个文件有副作用,但是别人项目使用的是你 src 下面的代码吗?其实不是,通过 G2 的 packge.json 可以发现,依赖项目中使用的应该是构建后 esm/index.js
的代码。
json
{
"module": "esm/index.js",
"exports": {
".": {
"import": "./esm/index.js",
}
},
}
这使得让打包工具误认为除了 src/index.ts
以外所有的文件都是没有副作用的,包括 esm/index.js
这个文件,所以那一行代码就被去掉了。
最后正确的写法应该是:
json
{
"sideEffects": ["./esm/index.js"]
}
收获
通过这次事情,可以得到一个教训:sideEffects 指定的路径一定是依赖项目使用的文件路径,或者是 package.json module 字段指定的文件路径。
对于 TypeScript 的项目,应该是你构建的产物的路径,因为 TypeScript 项目是一定要构建的。对于 JavaScript 项目,则也可能直接是 src 路径。