按需引用Tree-Shaing
也就是为了优化打包体积,消除对无用模块的引用,作为性能优化中的重要手段之一,也应该是每个合格组件库都应该做到的。接下来就以package-demo
组件库为例,一步步构建出一个内部组件库。
引用方式
首先可以确定的是用户希望的使用方式是(方式1):
import { Button } from 'package-demo';
而不是需要知道组件的具体位置之后,手动引入(方式2):
import Button from 'package-demo/lib/button.vue';
import 'package-demo/lib/style/button.css'
由方式二到方式一的实现可以有两种方法:
- 借助
babel
插件。比如antd
之前提供的babel-plugin-import
,插件通过引入固定路径的组件及组件样式,替代手动shaking
的过程。由此也可以确定打包后的文件路径(组件要求lib/xx,样式文件要求lib/xx/style/xx)和文件模块CommonJs
import { Button } from 'antd'; ReactDOM.render(); ↓ ↓ ↓ ↓ ↓ ↓var _button = require('antd/lib/button'); require('antd/lib/button/style/css'); ReactDOM.render(<_button>xxxx);
- 通过开启
webpack
对ES modules
的Tree-shaking
(mode=production 时默认开启此功能)。
/harmony import/
,使用过的export标记为 /harmony export/
,未使用过的export标记为/unused harmony export/
,再配合Uglifyjs
等工具把没用的代码删除输出格式
示例中也是采用了基于
ES modules
的 tree-shaking
来实现按需引用,那为什么ESM可以被tree-shaking,什么是tree-shaking呢?先来了解一下概念:ES modules
ES6 module
中通过import
关键字引入模块,通过export
关键字导出模块,在编译时输出静态定义的接口。import
命令会被js引擎静态分析,优于模块内的其他内容执行,加载某个输出值而不是整个模块,这种加载称为“编译时加载”。import { member1 , member2 } from "module-name";
// 导入指定名称的多个成员
export function add() {};
与之不同的
CommonJs
,通过require
关键字加载的是整个对象(module.exports属性),从对象上读取方法,这种加载称为“运行时加载”。该对象是在脚本加载完成后生成的,所以不能进行静态分析。exports.fs = fs;
module.exports = fs;
module.exports:module是一个变量,指向一块内存,exports是module中的一个属性,存储在内存中,然后exports属性指向{}模块。既可以使用.语法,也可以使用=直接赋值。所谓静态分析就是不执行代码,从字面量上对代码进行分析,我们使用
require()
语法的 CommonJS
模块规范,它允许代码根据条件去动态加载不同的模块,只有代码运行时才知道引用的什么模块。ESM的模块引入只能在条件外,作为模块顶层的语句出现,下面的写法会造成语法错误。// CommonJs
var dynamicModule;
if (condition) {
dynamicModule= require("moduleA");
} else {
dynamicModule= require("moduleB");
} // ESM 错误语法
if (condition) {
dynamicModule= import("moduleA");
} else {
dynamicModule= import("moduleB");
}
所以总结就是
CommonJs
引入的是整个对象,并且可以进行条件加载;ESM
模块在加载时引入的是某个输出值,只能进行静态加载。ESM
模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这也是tree-shaking
的基础。tree-shaking Tree-Shaking 依赖于 ES6 import 和 export 静态分析能力,当模块是通过 static 方式引用,它会尽可能的删除只写不读的变量和函数。
未被使用的函数和变量 定义两个模块文件moduleA和moduleB
// moduleA.js
export function a1() {
console.log('a1')
};
export function a2() {
console.log('a2')
}// moduleB.js
import { a1 } from 'moduleA';
export function b1() {
a1();
};
export function b2() {
console.log('b2')
}
在index.js中进行调用
moduleB.b2
import { b2 } from 'moudleB';
b2();
经过编译就可以看到代码中只包含了
b2
函数,未使用的b1
及模块A
都在tree-shaking之后被清除,即使模块A被引入了。'use strict';
function b2() {
console.log('b2');
}b2();
存在副作用 当一个函数修改了自己函数范围外的资源,该函数就存在副作用。比如在
moduleA
中添加对数组原型链的修改// moduleA
Array.prototype.find = null;
【如何让自己的组件库支持按需引入】经过编译可以看到代码中整个引入了模块
A
,这就是因为模块A
中存在副作用,打包工具将其保留了下来,如果在模块B
中只引入moduleA.a1
而不调用,会看到模块A
虽然被消除了,但是副作用代码Array.prototype.find = null;
也会被保留下来。'use strict';
require('moduleA');
function b2() {
console.log('b2');
}b2();
未使用的类 定义函数
Util
类,在类中定义isPhone
方法和isEmail
方法// Utils.js
export default class Util {
isPhone () {
return 'phone'
}isEmail () {
return 'email'
}
}
在模块B中引入
Util
类import Util from './util.js'
export function b2() {
let util = new Util()
let result1 = util.hello()
console.log(result1)
};
同样的在
index.js
中调用b2()
,却发现打包后的代码存在isEmail
函数,将Util
类完整的打包了进去,并没有按照所想的那样删除没有使用过的isEmail
函数'use strict';
class Util {
isPhone () {
return 'phone'
}
isEmail () {
return 'email'
}
}
function b2() {
let util = new Util();
let result1 = util.hello();
console.log(result1);
}
b2();
由于 JS 的动态语言特性所致,如果删除了
isEmail
方法,当通过变量(Util['isEmail']
)去访问Util
类上的方法时,就会运行出错,所以为了保守起见,webpack和rollup在打包时都会保留类和对象的内部。webpacke打包后会多出很多模块定义的代码,为了方便观看例子是rollup经过tree-shaking的打包结果虽然tree-shaking并不能完全的消除未使用的代码,但配合webpack的相关配置(工程化管理下项目一般会搭配webpack),也可以达到我们想要的按需加载的效果。
打包工具
综上我们需要一个可以输出es模块的打包工具,
rollup
便是很好的选择,webpack
打包的文件会添加一些自定义的模块加载代码__webpack_require__
等工具函数,这些是一个libary
库所不需要的。package配置rollup
不支持动态加载和代码分割,所以一般的application
应用还是webpack
比较合适。
module 当用户安装组件库时,
npm install
命令会根据项目根目录下的package.json
配置文件,自动下载所需的模块。package.json还定义了项目的版本、名称和script脚本,具体可以查看javascript标准参考教程,其中main
字段指定加载的入口文件,早期基于CommonJs规范npm包都以此为入口,所以在ESM出现后,rollup
便采用了另一个字段module
,也逐渐被webpack所支持,当配置文件中存在 module
字段,会优先使用,如果没找到对应的文件,则会使用 main
字段。当然两者也可以指向同一个入口。"main": "lib/index.js",
"module": "es/index.js"
sideEffects 当引入不是像lodash那样的纯工具函数的
package-demo
组件库时,会发现由于副作用的存在,开启了tree-shaking的bundle还是引入了大部分模块,这时就需要sideEffects
。当引入的 package-demo
被标记为 sideEffects: false
时,webpack
就知道这个npm
包里的文件是没有副作用的,只要组件没有被引用到,整个 模块/包 都会被完整的移除。( webpack version >= 4才能启用)只要你的包不是用来做 polyfill 或 shim 之类的事情,比如修改了 window 上的属性,复写了原生对象方法等,就可以设置 sideEffects: false,至于是不是真的有副作用则并不重要,这对于 webpack 而言都是可以接受的。
调试本地组件库 在组件库还没有上线之前,可以在
package-demo
中运行npm link
,组件库会根据package.json
中填写的信息(包名package-demo),被链接到全局。在其他项目(项目A)中执行 npm link package-demo
,就可以在其node_modules
中发现组件库的快捷方式,此快捷方式映射着组件库。当组件库发生修改时,项目A中也可以同步看到修改。总结
如果想按需加载一个组件库
package-demo
,首先组件库需要输出es modules
模块规范,并在package.json
中配置sideEffets
和module
,其次使用者需要使用webpack
。欢迎关注公众号,分享前端小知识~
文章图片