这样做,你的组件也可以支持tree-shaking

3,795 阅读5分钟

今天不介绍tree-shaking,毕竟通过这几年的科普,大家已经对它有一些认识或了解了,各位在工作中用到的三方组件库(如antd)都是充分支持的,这样做的好处不言而喻。有时候我们自己也可能会写一些组件给别人用,如果你的不支持tree-shaking,是不是有点low(不用ts也一样),那么如何让你的组件库显得更加专业呢,这就是我们今天要讨论的问题,首先我们要清楚几个基础概念。

副作用

函数有副作用的概念,组件库也有,对于构建平台来说,内部依赖关系复杂,可能像是一个连体婴儿,缺了谁都不行,可能也不是(工具还没那么聪明可以识别),因此如果你的组件库明确声明了哪些有副作用(不要乱摇),哪些没副作用(使劲摇),没有歧义了这事儿岂不是就成了么。这里的副作用就是说明代码有没有关联,会不会影响功能,能不能删掉再打包。

ESM

需要支持tree-shaking,那么你的组件库就必须时基于ESM编写的,然后本地项目使用esm编写,你敢问我为什么吗,我不想回答(静态分析),因此写的代码不要CMD,不要AMD,也不要cjs,就用import/export就完了。说到这里手写一个简单的测试包试试吧。首先需要初始化一个本地项目,然后测试的包取名为test-esm-pkg,包含两个文件,最终项目结构如下:

--node_modules
     --test-esm-pkg
        --index.js
        --package.json
--src
     --app.js
--webpack.config.js
--package.json

index.js很简单,包含两个函数,分别打印出名字:

// index.js
function a() {
    console.log('a');
}

function b() {
    console.log('b');
}
module.exports = {
    a,
    b,
};

test-esm-pkg的package.json里面进行一下简单的入口文件配置:

"main": "index.js",

当包在被引入的时候,就会访问test-esm-pkg/index.js文件了,接下来完善一下app.js:

import * as test from 'test-esm-pkg';
test.a();

这里只访问了a方法,那么经过摇树之后,b是不会出现在打包文件里了吧,确实也是如此,最终打包输出之后的js如下:

(()=>{"use strict";console.log("a")})();

因此你的代码库如果基于esm编写,也输出esm文件供外部使用,其实在js的部分已经支持了tree-shaking,这是各种构建平台自带的功能。很多组件库还支持cjs模式,例如:

"main": "lib/index.js",
"module": "es/index.js",

main呢往往是cjs模块的入口,module就是esm的入口了,一般的组件库都会配置这两个入口,同时支持cjs和esm两种模块,具体决定用哪种,其实是和你的源代码使用哪种模块绑定的,你用esm语法写代码,那么就会去加载module入口文件,反之亦然。如果只有一个main入口,那么没有选择,只能使用这个了,当然webpack这类工具会帮忙进行cjs->esm的转换,因此我们写的代码是无感知的(如果你使用过ts,那么ts默认是不会帮你转换的,需要你配置编译选项),如果你没配置module且mian入口是cjs模块,那么你就不能快乐的摇树了,如果你的module入口文件是挂羊头买狗肉,你可以试试看。

这里还想再说说cjs的模块导入的情况,cjs的导出是一个运行时对象,依赖模块的运行,不能提前分析出关联,因此不能做到tree-shaking。

Babel在捣乱?

有人会觉得使用了Babel会导致Webpack的Tree-shaking失效,因为会把esm转换成cjs,那么不就不能摇树了么,点击看看这个b站的视频,其实不用担心,放心用吧。

再说几句

sideEffects

从antd的官网上看,说默认支持基于 ES module 的 tree shaking,对于 js 部分,直接引入 import { Button } from 'antd' 也会有按需加载的效果,但是对于css/less来说,还是要使用babel-plugin-import插件来引入,那么我们可以看下antd是如何定义包的。

"sideEffects": [
    "dist/*",
    "es/**/style/*",
    "lib/**/style/*",
    "*.less"
  ],

可以看到在项目的package.json中定义了四个文件资源,标明这些资源是有副作用的,包括样式文件和包含所有组件的js/css资源,这里说一下样式文件,一般情况下,我们在代码中会这样引入:

import React from 'react';
...
import './index.less';
...

引入的这个less文件在构建工具看来,只是简单引入了,压根就没有使用啊,因此如果不在sideEffects中标记出来的话,打包工具就会把它摇掉了,最终的效果就是组件没了样式,因此在实现自定义的组件库时,需要注意。

/*#__PURE__*/

如果你看过一些包的源代码,如vue,很可能会看到/#*__PURE__*/这样的注释,其实呢就是标记这块代码是纯(无副作用)的,那么构建工具就可以大胆的把他踢出了,当然是在没有使用的前提(再说一句,这种注释只对esm有效),这里我们不妨写写代码试试效果吧,还是以上面的test-esm-pkg为例。

function a() {
    console.log('a');
}

function b() {
    console.log('b');
}

function c() {
    console.log('c');
}
c();

export {
    a,
    b
};

这一次加了一个c函数,然后直接调用它,那么最终打包的效果如下:

(()=>{"use strict";console.log("c"),console.log("a")})();

然后添加上注释调用/*#__PURE__*/c();,打包的效果如下:

(()=>{"use strict";console.log("a")})();

其实对于构建工具来说,c()这样的代码其实是不能确定有没有副作用的,那么就不删除代码是合理的选择,如果有了标记的注释就很清晰了,可以放心的去掉这段代码,从而优化构建体积,这种注释在模块内部函数调用的场景使用较多,一般自己写简单组件库可能也用不上。

最后再总结一下,组件采用esm编写,package.json配置正确的esm版本的入口文件,然后那些非js资源(css/less/图片等)在sideEffects里面声明,确保样式和UI的正常逻辑即可。