npm包支持tree-shaking能够实现按需引入,对生产环境优化有重要意义,本次实践参考antd,对移动端组件库@fx-ui/jdy-design-mobile进行了改造node
在阐述理论以前,先看一下该npm包处理先后的体积对比,用实力说话💪react
处理前该npm包体积418.74KB webpack
处理后该npm包体积274.19KB git
减小的145KB就是该npm包被tree-shaking剔除的代码es6
Tree shaking is a term commonly used in the JavaScript context for dead-code elimination. It relies on the static structure of ES2015 module syntax, i.e. import and export.
复制代码
webpack文档给出的解释中有个关键词就是ES2015
,大多数状况下,咱们开发的npm包为了更好的浏览器兼容性,会用babel将es6转译成es5或者更低的版本,从而丢失了tree-shaking的能力。github
另外,tree-shaking须要配合压缩工具例如UglyfiJs来使用,UglyfiJs会识别代码中的/*#__PURE__*/
标注,并将未被使用的函数移除:web
var renderContent = function renderContent() {
return (
/*#__PURE__*/
React.createElement("div", {
className: cls,
style: getStyle(),
ref: elRef
}, children)
);
};
复制代码
你不须要手动添加这些标注,babel会在转化的时候自动帮你加上算法
打包工具(Webpack, Rollup)会优先经过package.json来判断一个npm包是否支持tree shaking:typescript
sideEffects
以外的文件时,才会应用tree-shaking{
"sideEffects": [
"dist/*",
"es/components/**/style/*",
"lib/components/**/style/*",
"*.less"
]
}
复制代码
main
字段的以外的module
字段,该字段将指定npm包的es6版本{
"main": "lib/index.js",
"module": "es/index.js",
}
复制代码
打包工具优先经过module和sideEffects指定的路径来引入该包的es6版本,并应用tree-shaking,若是发现es6版本不可用,则会使用备选项,即main
字段指定的低版本。npm
这里有个疑问就是为何不直接让pkg.main指向es6格式的源码呢? 有2个缘由:
大部分的开发者在使用babel的时候都会避开node_modules来提升编译速度,此时若是使用es6的包则须要配置复杂的编译规则来将该npm包加入白名单。
有些开发者可能会在nodejs环境中引用该npm包,好比lodash,此时es6就不适合了
虽然webpack的功能更强大,但gulp能够更好的控制整个打包流程,相比于项目的开发,gulp和rollup更适合库的开发。
lib/
和 es/
参考antd来讲,通常具备tree-shaking机制的包都会有lib/
和es/
两个文件夹,gulp会经过gulp-typescript
和gulp-babel
将src
目录下的.ts
, .tsx
完整的映射到lib
或者es
目录,当咱们在babel配置中添加modules: false
时,转换出的就是es6语法的js,若是不添加modules
字段,则默认转换出es5:
const getBabelConfig = (modules) => ({
presets: [
resolve('@babel/preset-react'),
[
resolve('@babel/preset-env'),
{
modules,
targets: {
browsers: [
'last 2 versions',
'Firefox ESR',
'> 1%',
'ie >= 9',
'iOS >= 8',
'Android >= 4',
],
},
},
],
],
plugins: []
});
复制代码
src的同级目录
到如今为止,一切都很完美,但实际状况却稍微复杂一些,好比说像jdy-design-mobile
这个项目下除了src还有个同级目录biz,打包后生成了business文件夹,项目中可能会直接经过路径来引用business下的模块,好比:
import { SearchInput } from '@fx-ui/jdy-design-mobile/business';
复制代码
这个时候就无法经过package.json
中的字段来动态引用了,因而business下面也必须存在2个目录lib
和es
,而后在项目中手动引入business/lib
或者business/es
,此时是否使用tree-shaking是由开发者决定的。
biz -> business/(lib|es) 和 src -> (es|lib)的步骤是同样的,但前者由于目录结构发生了很大变化,须要对import语句作一些处理:
修正biz和src之间的相对引用
biz以前引用的是src,如今business(lib|es)引用(lib|es)
// 针对business/es
gulp.src(rawSourceBiz).pipe(replace(/(import.*from.*)\/src(.*)/g, '$1/es$2')
// 针对business/lib
gulp.src(rawSourceBiz).pipe(replace(/(import.*from.*)\/src(.*)/g, '$1/lib$2')
复制代码
修正biz目录下的模块间的相对引用
原来的buessiness/somefile.js
如今变成了buessiness/(lib|es)/somefile.js
由于如今的buessiness/(lib|es)/somefile.js
已是babel处理以后的了,咱们无法再经过字符串替换的方式来处理import语句,可是babel提供了自定义插件的方式,容许咱们在ast阶段处理字符串,好比ImportDeclaration
就对应着源代码中的import语句,这时再作替换就很方便了:
function replacePath(path) {
if (path.node.source && /\/(lib|es)/.test(path.node.source.value)) {
const esModule = path.node.source.value.replace(/\/(lib|es)/, '/../$1');
path.node.source.value = esModule;
}
}
// babel插件,修改import语句的字符串
function replaceLiborEs() {
return () => ({
visitor: {
ImportDeclaration: replacePath,
ExportNamedDeclaration: replacePath,
},
});
}
复制代码
若是咱们使用的npm包A依赖了包B,那么当咱们选择A的es6版本时,它所依赖的包B也应该自动切换到es6版本,在理想状况下,npm的模块机制已经自动实现了这个算法。
但若是就像上面说的,当包B存在src的同级目录时,状况就会变得复杂,若是A在src源码中引用了B的lib版本,好比:
// a.js
import SomeBModule from 'B/business/lib'
复制代码
那么在A的es6版本代码中则必须将B/business/lib
改为B/business/es
,才能将tree-shaking的效果发挥到极致,antd也是利用自定义babel插件replaceLib来实现这一替换的:
const { dirname } = require('path');
const fs = require('fs');
const { getProjectPath } = require('./utils/projectHelper');
function replacePath(path) {
if (path.node.source && /\/lib\//.test(path.node.source.value)) {
// 替换import语句中lib为es
const esModule = path.node.source.value.replace('/lib/', '/es/');
// 确保包B的es6版本确实存在
const esPath = dirname(getProjectPath('node_modules', esModule));
if (fs.existsSync(esPath)) {
path.node.source.value = esModule;
}
}
}
function replaceLib() {
return {
visitor: {
// 修改ast的ImportDeclaration节点
ImportDeclaration: replacePath,
ExportNamedDeclaration: replacePath,
},
};
}
复制代码
npm包的tree-shaking机制是在确保可使用es5代码的基础上,提供es6代码做为可选项。在改造的过程当中,除了在package.json声明字段,还须要注意打包先后的文件引用路径的变化。
它的样式通常采用总体引入(不作tree-shaking),固然若是须要按需引入样式也能够配合babel-plugin-import来作,不过antd官方已经不推荐了