NodeJS 的 commonjs、esm 模块使用以及一些字段释义

在之前 js 的模块化迟迟没有落地,所以 NodeJS 自己整了个 commonjs,也就是我们代码里常看到 require('moduleName')。后来浏览器支持了 esm 标准也就是代码里的 import export,NodeJS 也给加上了。

在不支持的年代,我们代码能直接写上模块化语句,都是 webpack 和 babel 的功劳。无论是 import 还是 require,最后都会处理成 _webpack_require_(应该是这个 api)。

这里记录下 NodeJS 使用 commonjs、esm 的一些注意点,以及 package.json 一些看了老是记不住的字段。

一、文件格式区分

NodeJS 要使用 esm 模块,文件必须声明为 .mjs 后缀。依然使用 .js 或者声明为 .cjs 都是使用 commonjs。

  1. 当然硬要在 .js/.cjs 文件使用 esm 语句(也就是 import)的话会报错

    SyntaxError: Cannot use import statement outside a module

  2. 反过来在 .mjs 文件使用 commonjs 语句(也就是 require)的话会报错

    ReferenceError: require is not defined in ES module scope, you can use import instead

二、混搭使用

正常使用当然是两厢安好,但是可能还是会出现一些旧模块是 commonjs 写的,新的又想用 esm 去写的情况。

  1. 在 .js/.cjs 文件引用了 esm 模块的话会报错

    require() of ES Module /xxx/yyy/zzz/utils.mjs not supported.Instead change the require of /xxx/yyy/zzz/utils.mjs to a dynamic import() which is available in all CommonJS modules.

按照指示,改成动态引入 import() 就可以了。

import('./utils.mjs').then(console.log);
// [Module: null prototype] {
//   default: [Function: runEsm],
//   runSub: [Function: runSub]
// }

可以看出,export default 导出的东西被挂载 default 字段上。上面说了在标准还没落地时,webpack 就是这么做的。不知道是不是 NodeJS 参考了这个实现。

  1. 在 .mjs 文件 文件引用了 commonjs 模块的话就没问题。= =!

只不过 commonjs 里并没有 default 这个概念,所以如 import runCommonDefault, { runExport } from './utils.cjs'; 里,runCommonDefault 就是整个 module.exports 导出的对象。而 runExport 就是纯粹对 module.exports 导出对象的解构,属于其中一个字段内容。

三、esm 模块不支持 __filename,__dirname

esm 模块里是没有 __filename__dirname 这两个全局变量的(其实不是全局变量,和 require 一样是外部传进来,姑且当其是个全局变量)。可以这样实现:

import path from 'path';
import url from 'url';
const __filename = url.fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

四、esm 文件可以直接写 await 方法

意思就是整份文件在被引入的时候已经是 async 的形式引入,所以直接在文件最顶层就可以直接写 await 关键字。和上面说的 .js/.cjs 文件需要用 import() 引用 esm 模块是不是有点关联起来。

这里可以了解一下 NodeJS 的模块加载机制,很有意思。整个模块是包在一个函数体执行一样,require 和上面说到的 __filename__dirname 都是这个函数的入参,从外部传进来的,而不是挂载在全局。
这里不展开,有兴趣可以自行搜索。或者看看网上大佬文章,虽然比较考古,不过应该也是大差不差《require() 源码解读》。

五、commonjs 里 module.exports 和 exports 的关系

一开始这两个确实是指向同个对象,打印能看出: console.log(module.exports === exports) // => true
也就是 module.exports.funA = xxxexports.funA = xxx 确实效果是一样的。
但是现在很习惯会统一导出,很少一个个声明:module.exports = { ... }。这样子就把 module.exports 的指向对象给改了。再也和 exports 聊不到一块儿去了。

六、package.json 声明导出相关的字段

module:
用于指向 esm 模块的库入口。

main:
用于指向 commonjs 模块的库入口。

brower:
用于指向 umd 模块的库入口。umd 全称是UniversalModuleDefinition,是一种通用模块定义格式。简单理解就是糅合不同标准,根据宿主的支持度使用相应的标准。很适合给浏览器端引入使用。

exports:
用于声明模块的导出路径映射。

"exports": {
  ".": {
    "types": "./dist/index.d.ts",
    "import": "./dist/index.mjs",
    "require": "./dist/index.js"
  },
  "./sub": {
    "types": "./dist/sub.d.ts",
    "import": "./dist/sub.mjs",
    "require": "./dist/sub.js"
  },
  "./package.json": "./package.json",
  "./*": "./*.js"
},

如上例子所示,我们分别在 require('myUtils')require('myUtils/sub') 的时候,就会相应的导出不同的文件。最后那个 "./package.json": "./package.json", "./*": "./*.js" 可以酌情写。
其实在没这个字段的时候,就是按照文件夹去找的。有了这个字段后就会严格按照这个字段去寻找,此时不声明 package.json 的导出的话,甚至都获取不到 package.json。