引入
最近团队的一个同学在搞 npm library 源码的调试插件,因为内部的一个组件库含有大量的逻辑,在某个项目中不经意就出现一个磨人的 bug,但是组件库发布都是打包编译后的代码,而且没有 publish src 代码,不方便调试,每次还要 down 一下包的源码,再改下 webpack 的配置(比如 rule 中 exclude 去掉组件库, 改下 resolve ,在 dll 中去掉组件库)。被他们耳语目染了好几天,我就想,记得 npm 包是可以直接引源码的,大概改下 webpack 配置就可以了。然后便找到了 package.json 中 module 字段,并查漏 js 中 tree shaking 的知识,所以我并没有去研究怎么搞那样的一个插件?,而是由 package 中的 module 字段延伸出的一些知识。
为何有 module
查阅了 package.json 的文档,并没有找到 module 字段的定义,直到 google 才知道它是 rollup 中最早就提出的概念 --- pkg.module。大概就是最早的 npm 包都是基于 CommonJS 规范的,package.json 形如:
"name": "package1",
"version": "1.0.0",
"main": "lib/index.js"
当 require('package1')
的时候,就会根据 main 字段去查找入口文件。
而 es2015 后,js 拥有了 ES Module,相较于之前的模块化方案更爽滑,更优雅,并且 ES 模块也是官方标准(JS 规范),而 CommonJS 模块是一种特殊的传统格式,在 ES 模块被提出之前做为暂时的解决方案。所以 rollup 去利用 ES Module 构建,就可以利用 ES Module 的很多特性,从而提高打包的性能,其中提升一个便是 tree shaking,这个我们后面去介绍。在这个构建思想的基础上,开发、产出的 npm 包同样使用 es6 的 module,即可同样受益于 tree shaking 等特性。
而 CommonJS 规范的包都是以 main 字段表示入口文件了,如果使用 ES Module 的也用 main 字段,就会对使用者造成困扰,假如他的项目不支持打包构建,比如大多数 node 项目(尽管 node9+ 支持 ES Module)。这就是库开发者的模块系统跟项目构建的模块系统的冲突,更像是一种规范上的问题。况且目前大部分仍是采用 CommonJS,所以 rollup 便使用了另一个字段:module。
像这样:
"name": "package1",
"version": "1.0.0",
"main": "lib/index.js",
"module": "es/index.js"
webpack 从版本 2 开始也可以识别 pkg.module 字段。打包工具遇到 package1 的时候,如果存在 module 字段,会优先使用,如果没找到对应的文件,则会使用 main 字段,并按照 CommonJS 规范打包。所以目前主流的打包工具(webpack, rollup)都是支持 pkg.module 的,鉴于其优点,module 字段很有可能加入 package.json 的规范之中。另外,越来越多的 npm 包已经同时支持两种模块,使用者可以根据情况自行选择,并且实现也比较简单,只是模块导出的方式。
注意:虽然打包工具支持了 ES Module,但是并不意味着其他的 es6 代码可以正常使用,因为使用者可能并不会对你的 npm 包做编译处理,比如 webpack rules 中 exclude: /node_modules/
,所以如果不是事先约定好后编译或者没有兼容性的需求,你仍需要用 babel 处理,从而产出兼容性更好的 npm 包。还好 rollup 在这方面做的不错,对于 library 开发者更友好一些。
同时支持的效果类似这样:
<img width="693" alt="lib" src="https://user-images.githubuse...;>
<img width="663" alt="es" src="https://user-images.githubuse...;>
package.json 只需要对应相应的文件就可以了。
"name": "drag-list",
"version": "1.0.0",
"main": "lib/drag-list/index.js",
"module": "es/drag-list/index.js"
Tree-shaking
tree-shaking 是近两年才在 JS 中出现的,之前没有的,而模块化的概念是一直都有方案的,只不过直到 ES Module 才有统一的标准趋势。
前面提到 rollup 采用 ES Module,带来的一个优点便是 tree shaking,那什么是 tree-shaking 呢。
有一个图片很形象的解释了它的功能。
tree-shaking 的功能就是把我们 JS 中无用的代码,给去掉,如果把打包工具通过入口文件,产生的依赖树比作 tree,tree-shaking 就是把依赖树中用不到的代码 shaking 掉。
我们通过代码了解下,webpack3.x 下打包验证 tree-shaking。
// 入口文件 index.js
import { func1 } from './export1';
func1();
// export1 文件
export function func1() {
console.log('func1');
}
export function func2() {
console.log('func2');
}
func2 方法虽然导出了,但是在 index.js
中是没有用到的,func2 就是无用代码,最终打包生成的 build 是应该去掉的。
使用最简单的 webpack 配置,不使用 babel,产出 build.js
,export1 是这样的:
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
/* harmony export (immutable) */ __webpack_exports__["a"] = func1;
/* unused harmony export func2 */
function func1() {
console.log('func1');
}
function func2() {
console.log('func2');
}
/***/ })
我们发现有两行注释,/* harmony export (immutable)
表明代码是有用的,unused harmony export func2
表明 func2 是无用代码,说明 webpack 已经识别。不过 webpack 仅仅是做了“标记”,去掉这些代码的能力,是通过插件实现的,常用的便是 unglify。在 plugins 用启用 UglifyJsPlugin 后,查看下 build。
// webpack.config.js
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
...
plugins: [
new UglifyJsPlugin(),
]
}
上图即编译后 export1 模块的截图,可以看到 func2 已经被去掉了。不过在我开启 babel-loader 以后,babel 配置就是一个简单的 "presets: ["env"]"
,却发现 func2 又回来了,如下:
这是为什么呢。因为 tree-shaking 是依赖 ES Module 的静态加载,而 babel-presets-env 中是包含 ES2015 modules to CommonJS transform
的 plugin,也就是转化成 CommonJS,所以无法识别哪些代码是未引用的,也就是无法 tree-shaking,所以 babel transform 的时候应该保留 ES Module。
通过 presets 的 option 选择,设置 modules 为 false 即可。
另外,tree-shaking 并不是完美无缺的,有些情况还无法识别。比如你导入了一个模块,但是这个变量代码中未使用,是不会去掉的,细节可以看这篇文章
为什么是 ES Module
ES Module 之前的 JS 实现模块化,都是基于第三方依赖管理实现的,比如 requirejs,seajs,这都是在代码执行的时候,才知道依赖了哪些模块,常用的 node 中的 commonjs,也是如此
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
所以,当 ES Module 在代码不执行的时候,就可以知道模块的依赖关系,就不难理解是为什么了。
思考
我的本意是,可否利用 module 字段的特性,让我的 npm 包支持引入源码,从而可以实现源码调试、并且后编译的效果,不过从目前的规范看来,内部还是可以试一下的,开源的包最好不要这样做,除非你有自己的一套规范以及后编译生态。虽然没有达到目的,不过也后知后觉的了解到 module 的用意,以及 rollup 在开发包时候的妙用,以及 tree-shaking 并不是自己了解的那么美好。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。