聊聊 package.json 文件中的 module 字段

2,479 阅读5分钟
原文链接: loveky.github.io

本文来和大家聊聊 pkg.module 字段的功能以及使用场景。

在谈 pkg.module 之前,让我们先来了解一个和它有着紧密关系的概念 —— Tree Shaking

什么是 Tree Shaking?

让我们通过两个小例子来了解。

假设我们有以下两个文件:

// math.js
exports.add1 = function (x) {
    return x + 1;
}

exports.add2 = function (x) {
    return x + 2;
}
// app.js
import {
    add1
} from './math';

add1(100);

app.js 文件通过 import 引入了 math.js 中的 add1 方法。

我们通过 webpack 命令打包:

webpack --entry ./app.js --output-filename app.bunble.js

在生成的 app.bundle.js 文件中我们可以看到以下内容:

这里我们可以看到虽然我们只用到了 math.js 文件中的 add1 方法,但是在最终生成的 bundle 文件中却包含了 add1add2 两个方法。这是为什么呢?

这是因为在 CommonJS 规范math.js 文件的模块格式即为 CommonJS)中,模块只能通过 exports 对象向外暴露属性。所有要暴露的方法、变量等都只能作为 exports 对象的一个属性出现。

由于在 JavaScript 中访问对象的属性是在是太灵活了,例如:

所以打包工具并不知道我们代码中最终会用到模块中的哪些方法。为了安全起见,整个模块的代码都被包含在了最终生成的 bundle 中。

随着 ES6 规范的出现,这个问题得到了解决。ES6 定义了一套基于 importexport 操作符的模块规范。它与 CommonJS 规范最大的区别在 ES6 中的 importexport 都是静态的。静态意味着一个模块要暴露或引入的所有方法在编译阶段就全部确定了,之后不能再改变。这样做的好处就是打包工具在打包阶段就可以分析出代码中用到了某个模块中的哪几个方法。其它没有用到的方法就可以从最终的 bundle 文件中剔除掉。这样既可以减少 bundle 文件的大小,又可以提高脚本的执行速度。这个机制就叫做 Tree Shaking。是不是很形象。

让我们把 math.js 改写成 ES6 的模块格式来看一下实际效果:

// math.js
export function add1 (x) {
    return x + 1;
}

export function add2 (x) {
    return x + 2;
}

再次使用 webpack 打包。查看生成的 bundle 文件:

我们可以注意到原来直接定义在 exports 对象上的两个方法现在都成了两个函数声明,并且只有 add1 方法被添加到了模块向外暴露的对象上。同时 webpack 还在注释中告诉我们 add2 方法没有被其它模块用到。配合 uglifyjs-webpack-plugin,就可以很轻松的把它从最终的 bundle 文件中移除。

关于 Tree Shaking 我们已经说得差不多了。你可能会想这和我们今天要聊的 pkg.module 字段有什么关系呢?

其实只需要进一步思考一个问题。假如我们是一个 npm 包的开发者,我们该如何发布我们的包以便于使用者在使用我们包的时候也可以利用 Tree Shaking 机制呢?

如何发布一个支持 Tree Shaking 机制的 npm 包?

你可能很容易想到直接把 pkg.main 指向我们 ES6 格式的源码文件不就可以了吗?但仔细想想这样做会带来两个问题:

  1. 通常人们在使用打包工具的 babel 插件编译代码时都会屏蔽掉 node_modules 目录下的文件。因为按照约定大家发布到 npm 的模块代码都是基于 ES5 规范的,因此配置 babel 插件屏蔽 node_modules 目录可以极大的提高编译速度。但用户如果使用了我们发布的基于 ES6 规范的包就必须配置复杂的屏蔽规则以便把我们的包加入编译的白名单。
  2. 如果用户是在 NodeJS 环境使用我们的包,那么极有可能连打包这一步骤都没有。如果用户的 NodeJS 环境又恰巧不支持 ES6 模块规范,那么就会导致代码报错。

基于以上两点我们可以确定 pkg.main 字段指向的应该是编译后生成的 ES5 版本的代码。

既然利用现有字段这条路走不通,那很自然的就会想到引入一个新字段来解决问题。这就是本文要说的 pkg.module字段。

综合前文讨论的结果,pkg.module 字段要指向的应该是一个基于 ES6 模块规范使用ES5语法书写的模块。

听起来是不是比较拗口?基于 ES6 模块规范是为了用户在使用我们的包时可以享受 Tree Shaking 带来的好处;使用 ES5 语法书写是为了用户在配置 babel 插件时可以放心的屏蔽 node_modules 目录。

我们的 package.json 文件中看起来会是这个样子:

{
  "main": "dist/dist.js",
  "module": "dist/dist.es.js"
}

相当于在一个包内同时发布了两种模块规范的版本。

当打包工具遇到我们的模块时:

  1. 如果它已经支持 pkg.module 字段则会优先使用 ES6 模块规范的版本,这样可以启用 Tree Shaking 机制。
  2. 如果它还不识别 pkg.module 字段则会使用我们已经编译成 CommonJS 规范的版本,也不会阻碍打包流程。

是不是很完美?

写在最后

要构建一个满足 pkg.module 字段要求的包其实很简单。如果你是使用 Rollup 打包代码, 那么只需要把 output 的格式设置为 es 就可以啦。

// rollup.config.js
export default {
  ...,
  output: {
    file: 'bundle.es.js',
    format: 'es'
  }
};

目前 pkg.module 还只是一个提案,并不是 package.json 文件标准格式的一部分。但它极有可能会成为标准的一部分,因为它目前已经是事实上的标准了(最早由 Rollup提出,Webpack也已支持)。

参考链接: