Skip to content

古老的node.js

当年只有cjs模块化方式的时候,node.js是通过package.json里面的main字段暴露的:

json
{
    "main": "index.js"
}

关于main字段和type字段的关系

package.json里面的type字段可以写commonjs或module,它其实是决定了你这个包里面到底什么扩展名下可以写“import语法”。 即:决定了当调用方或者nodejs来执行你的这个包内的某个文件的时候,到底使用esmmodule去执行它还是用commonjs去执行它,这个策略就是依据“type的值+文件扩展名”一起决定的。 例如你type写成module,那么你扩展名为js的文件里就不能写require或者exports.xxx=xxx这种语法。

而main字段,仅仅是告诉调用方,当它写出 import xx from 'yy' 的语法时,这个yy到底是引用哪个文件。

因此,就算你这个依赖包的package.json里面写的是 type:module,即esm包。 你的main字段依然可以指向一个app.cjs(只要你的app.cjs里面代码无比使用cjs语法写就行)。 这两者之间没有必然联系。

后来支持esm之后

module字段只是某些社区打包器发明出来的一个字段,可能是webpack。即webpack可以在打包的时候,如果发现某个node_modules依赖里面写有module字段,则它会认为这里有一个esm模块入口,他就优先用这个入口去按esm加载了。但webpack假如打包目标是cjs或umd,其实它最终还是把这个依赖的esm模块转成commonjs的模块化语法放到产物里面。(不过事实上,产物里面其实这些依赖包都变成了webpack产物中一个工厂函数而已,也没有所谓的cjs模块化了)。

但实际上Node.js至今都根本不识别package.json里面的module字段. Node是发明了一个更灵活的exports字段来解决入口文件如何暴露的问题。

exports字段更强大

对于目前复杂的环境,我们可能包里面要暴露cjs和esm两种模块,那么我们就可以通过exports字段来暴露了。所以我们还是尽量优先用exports字段,而不是简单的用module字段。

毕竟你只用module字段,就意味着你只能暴露一个入口,而exports字段可以让你暴露多个入口,且每个入口都能按cjs或esm指定不同的文件。

语法写法:

json
// 其中点号就代表默认入口。即调用方 import yy from 'xx', 只写了一个xx包名的情况。
  "type": "module",
  "exports": {
    ".": {
      "require": "./main.cjs",
      "import": "./main.js",
      "default": "./main.js"
    }
  }

调用机制

调用方,调用一个依赖的node_modules下面的包。

  1. 它优先看这个依赖包的package.json里面有没有 exports,
  2. 有exports字段,就按exports字段的规则去加载。注意:exports字段仅仅用来告诉调用方加载哪个文件,这个exports映射你无论写cjs还是js扩展名它都不管。
  3. 如果没有exports字段,nodejs会去找main字段。同样它也不管你main字段里写的到底是个啥文件。
  4. 找到这个文件后。node就可以思考按照cjs还是esm模块化方式执行这个文件。其思考方式就是看package.json里type的值,以及当前文件的扩展名。

探讨:难道真的要暴露多种入口吗

我们前面学到过,cjs和esm之间本来就可以互相交互的。既然如此,为何还要向上文说的一样 ,通过 module或exports字段来给cjs和esm单独指定不同的index.js文件呢?

试想:

  • 假如我项目是一个esm编写的项目,依赖了一个cjs的包,我其实本来就可以直接用esm语法import一个cjs的模块。而cjs包的作者它只需要记得我们前面学习过的规律:记得用exports.xxx=xxx这种写法暴露具名模块。 就像是nodejs的 fs模块,我既可以import fs from 'fs' 来从默认导出fs上访问appendFile,也可以import {appendFile} from 'fs' 来访问具名导出appendFile。

这不就已经解决问题了吗。

  • 假如我项目是cjs编写的项目。依赖了一个esm的包。这种情况下我直接require一个esm模块,理论上确实行不通(因为cjs加载esm必须用动态import语法)。如果你强行写require去加载esm文件,会报错:

::: error Error [ERR_REQUIRE_ESM]: require() of ES Module O:\code\fet-block-dep3\b.js from O:\code\fet-block-dep3\a.cjs not supported. Instead change the require of b.js in O:\code\fet-block-dep3\a.cjs to a dynamic import() which is available in all CommonJS modules. :::

所以,对于esm的包,确实有必要暴露一个cjs的入口----从而给那些cjs写代码的调用方项目来用。

但即使你想暴露成cjs模块,他也是有难度的,因为你在自己的包里面写个cjs的入口,它如何能加载你包里面的esm写的代码呢,这同样面临问题。假设你用标准的import语法来加载esm代码,由于它是异步的,你依然无法向外再次暴露成同步的cjs模块。感觉这种场景下,确实需要构建技术,将你的esm源码构建一份cjs结果,然后再借用上文所讲的exports或module/main字段,向外暴露esm和cjs各自的入口。

不过,现在大多数新项目,他们项目本身都是用esm语法。所以你作为一个依赖包的开发者,其实也没太有必要去暴露成ejs方式。如果确实要暴露(为了防止开发项目的调用方还在使用cjs方式),那么就确实得用打包器了----例如rollup能将esm源码构建成esm、cjs、umd等等形式产物------建议就直接构建成cjs版本产物(axios就是这么干的)。