sunyongjian / blog

个人博客😝😋😄
666 stars 54 forks source link

package.json 中的 Module 字段是干嘛的 #37

Open sunyongjian opened 6 years ago

sunyongjian commented 6 years ago

引入

最近团队的一个同学在搞 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 开发者更友好一些。

同时支持的效果类似这样:

lib es

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 呢。

有一个图片很形象的解释了它的功能。 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(),
  ]
}

ugly-export1

上图即编译后 export1 模块的截图,可以看到 func2 已经被去掉了。不过在我开启 babel-loader 以后,babel 配置就是一个简单的 "presets: ["env"]",却发现 func2 又回来了,如下:

babel-export1

这是为什么呢。因为 tree-shaking 是依赖 ES Module 的静态加载,而 babel-presets-env 中是包含 ES2015 modules to CommonJS transform 的 plugin,也就是转化成 CommonJS,所以无法识别哪些代码是未引用的,也就是无法 tree-shaking,所以 babel transform 的时候应该保留 ES Module。

modules-false

通过 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 并不是自己了解的那么美好。

相关推荐

你的Tree-Shaking并没什么卵用

【译】如何在 Webpack 2 中使用 tree-shaking

手把手带你走进下一代的ES6模块打包工具—Rollup

William-WFC commented 6 years ago

赞,写的很好

coconilu commented 6 years ago

tree-shaking是个好东西,对于很多第三方库,有时候我们只需要它的一部分,其它累赘可以被去掉

toastsgithub commented 5 years ago

请问一下,比如一个lib库打了两份代码,一份es5, 一份 es6, 并在packge.json声明了 module 入口,指向 es6的入口,对于业务库来说,好像就默认优先使用 es6 的入口了,有没有什么配置或者字段来使得业务库可以决定走哪个入口?

sunyongjian commented 5 years ago

@toastsgithub 业务库是什么意思。入口文件在 package.json 里声明啊,main/ module。

  "main": "lib/index.js",
Raincal commented 5 years ago

@toastsgithub 配置下 mainFields https://webpack.js.org/configuration/resolve/#resolve-mainfields

toastsgithub commented 5 years ago

@Raincal 提到的正是我期望的,我之前以为 npm 自身的机制会支持这个,想不到是打包工具做的支持,不过确实解析过程应该是打包工具控制的;

顺便给后来的人 FYI ,我用的是 rollup 来进行打包的,我查了下这个工具里做和 webpack mainField 类似的事情是通过配置 rollup-plugin-node-resolve 插件来达成目的的;

Thanks!

waitingsong commented 5 years ago

@Raincal 提到的正是我期望的,我之前以为 npm 自身的机制会支持这个,想不到是打包工具做的支持,不过确实解析过程应该是打包工具控制的;

顺便给后来的人 FYI ,我用的是 rollup 来进行打包的,我查了下这个工具里做和 webpack mainField 类似的事情是通过配置 rollup-plugin-node-resolve 插件来达成目的的;

Thanks!

对于npm公共模块项目,省心的办法是

TypeScript 输出 ES6 -> rollup 打包成 esm/cjs/umd 多个版本 -> 设置 package.json 多个入口

可参考 我的模块项目模板

ps: webpack 太复杂了。我对于花费两天时间都搞不明白无法入门的技术就不碰了。

yanhaijing commented 4 years ago

https://github.com/yanhaijing/jslib-base @waitingsong 推荐用这个,把你的改成了命令行工具

waitingsong commented 4 years ago

https://github.com/yanhaijing/jslib-base @waitingsong 推荐用这个,把你的改成了命令行工具

得空看看

dzjwan521 commented 4 years ago

bable有个module配置 将此设置为false不会转换模块,不过我没有具体试验过,这样做之后 tree-shakeing是否就会有效果了

modules "amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false, defaults to "auto". Enable transformation of ES6 module syntax to another module type. Setting this to false will not transform modules. Also note that cjs is just an alias for commonjs.

sunft1996 commented 3 years ago

nice