在之前 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。
当然硬要在 .js/.cjs 文件使用 esm 语句(也就是 import)的话会报错:
SyntaxError: Cannot use import statement outside a module
反过来在 .mjs 文件使用 commonjs 语句(也就是 require)的话会报错:
ReferenceError: require is not defined in ES module scope, you can use import instead
二、混搭使用
正常使用当然是两厢安好,但是可能还是会出现一些旧模块是 commonjs 写的,新的又想用 esm 去写的情况。
- 在 .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 参考了这个实现。
- 在 .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 = xxx
和 exports.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。