以下内容基于v22 LTS版的Node
Node
支持两套模块化系统CommonJS(CJS)
和ECMAScript modules(ESM)
CommonJS Node
加载CJS
是完全同步(synchronous
)的,因为需要执行模块文件。
Node
将如下文件视作CJS
模块:
扩展名为.cjs
扩展名为.js
且package.json
中配置了"type": "commonjs"
当扩展名为.js
或无扩展名时,不存在package.json
或最近的package.json
没有配置type
。除非按CJS
解析有语法错误但按ESM
解析没有语法错误
扩展名不为 .mjs
, .cjs
, .json
, .node
, .js
。当package.json
配置了"type": "module"
,此类文件按CJS
解析当且仅当它们被require()
引用
导出 1 2 3 4 5 6 7 function foo ( ) {}module .exports = { foo } exports .foo = foo
上面两种方法都可以用来导出模块。注意,module.exports
和exports
是两个指针,module.exports
指向的内容是最终导出的内容,exports
初始指向与module.exports
相同,如果对exports
重新赋值,如:
会导致exports
指向另一个对象,相当于改变一个指针的指向,而不会修改指针原先所指向的内容,所以不会导出任何内容。近似类比如下C
代码:
1 2 3 int a = 1 , b = 2 , *p = &a;p = &b; *p = b;
因此不难理解如下代码最终导出的内容只有name: "calculator"
:
1 2 3 4 5 6 7 8 9 function plus (a, b ) { return a + b; } exports .plus = plus;module .exports = { name : "calculator" }
因为我们让module.exports
指向了一个新的对象。
导入 1 const m = require ("./lib.js" )
m
就是module.exports
对象。import()
可以用来导入ESM
。
用require导入ESM
Support for loading ES Module in require() is an experimental feature and might change at any time
require
导入ESM
须满足如下条件:
ESM
完全同步(synchronous
),即顶层不包含await
(在CJS
中异步模块要以import()
导入)
以下三条满足其一
目标文件扩展名为.mjs
目标文件扩展名为.js
且最近的package.json
配置了"type": "module"
目标文件扩展名为.js
且最近的package.json
未配置"type": "commonjs"
且目标文件包含ESM
语法
引用Node
官方文档 的例子:
1 2 3 4 5 6 7 8 9 export function distance (a, b ) { return (b.x - a.x ) ** 2 + (b.y - a.y ) ** 2 ; }const distance = require ('./distance.mjs' );console .log (distance);
1 2 3 4 5 6 7 8 9 10 11 12 export default class Point { constructor (x, y ) { this .x = x; this .y = y; } } const point = require ('./point.mjs' );console .log (point);
require
在导入默认导出时,会添加__esModule: true
,以区分CJS
的exports.default
和ESM
的exprot default
。如果在导出时已经有了__esModule
属性则不会再添加。不要 在任何时候使用这个属性,因为该特性尚不稳定可能改变。
当默认导出和具名导出同时存在时:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 export function f (a, b ) { return a - b; } export default class T { constructor ( ) { console .log ("constructed!" ) } } const lib = require ("./lib.js" )console .log (lib)
可以通过export as
自定义哪些内容会导出给CJS
,此时其余内容会被忽略。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 export function f (a, b ) { return a - b; } export default class T { constructor ( ) { console .log ("constructed!" ) } } export { f as "module.exports" }const lib = require ("./lib.js" )console .log (lib)
源码 Node
的CJS
模块运行在一个包裹函数wrapper
中,这就是为什么我们可以访问到module
, require
, __dirname
等。wrapper
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 let wrap = function (script ) { return Module .wrapper [0 ] + script + Module .wrapper [1 ]; }; const wrapper = [ '(function (exports, require, module, __filename, __dirname) { ' , '\n});' , ]; let wrapperProxy = new Proxy (wrapper, { __proto__ : null , set (target, property, value, receiver ) { patched = true ; return ReflectSet (target, property, value, receiver); }, defineProperty (target, property, descriptor ) { patched = true ; return ObjectDefineProperty (target, property, descriptor); }, }); ObjectDefineProperty (Module , 'wrap' , { __proto__ : null , get ( ) { return wrap; }, set (value ) { patched = true ; wrap = value; }, }); ObjectDefineProperty (Module , 'wrapper' , { __proto__ : null , get ( ) { return wrapperProxy; }, set (value ) { patched = true ; wrapperProxy = value; }, });
源码中的_compile
方法使模块在wrapper
中运行并给出其运行结果,即导出的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 Module .prototype ._compile = function (content, filename, format ) { if (format === 'commonjs-typescript' || format === 'module-typescript' || format === 'typescript' ) { content = stripTypeScriptModuleTypes (content, filename); switch (format) { case 'commonjs-typescript' : { format = 'commonjs' ; break ; } case 'module-typescript' : { format = 'module' ; break ; } default : format = undefined ; break ; } } let redirects; let compiledWrapper; if (format !== 'module' ) { const result = wrapSafe (filename, content, this , format); compiledWrapper = result.function ; if (result.canParseAsESM ) { format = 'module' ; } } if (format === 'module' ) { loadESMFromCJS (this , filename, format, content); return ; } const dirname = path.dirname (filename); const require = makeRequireFunction (this , redirects); let result; const exports = this .exports ; const thisValue = exports ; const module = this ; if (requireDepth === 0 ) { statCache = new SafeMap (); } setHasStartedUserCJSExecution (); this [kIsExecuting] = true ; if (this [kIsMainSymbol] && getOptionValue ('--inspect-brk' )) { const { callAndPauseOnStart } = internalBinding ('inspector' ); result = callAndPauseOnStart (compiledWrapper, thisValue, exports , require , module , filename, dirname); } else { result = ReflectApply (compiledWrapper, thisValue, [exports , require , module , filename, dirname]); } this [kIsExecuting] = false ; if (requireDepth === 0 ) { statCache = null ; } return result; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 function wrapSafe (filename, content, cjsModuleInstance, format ) { assert (format !== 'module' , 'ESM should be handled in loadESMFromCJS()' ); const hostDefinedOptionId = vm_dynamic_import_default_internal; const importModuleDynamically = vm_dynamic_import_default_internal; if (patched) { const wrapped = Module .wrap (content); const script = makeContextifyScript ( wrapped, filename, 0 , 0 , undefined , false , undefined , hostDefinedOptionId, importModuleDynamically, ); const { sourceMapURL } = script; if (sourceMapURL) { maybeCacheSourceMap (filename, content, cjsModuleInstance, false , undefined , sourceMapURL); } return { __proto__ : null , function : runScriptInThisContext (script, true , false ), sourceMapURL, }; } let shouldDetectModule = false ; if (format !== 'commonjs' ) { if (cjsModuleInstance?.[kIsMainSymbol]) { shouldDetectModule = getOptionValue ('--experimental-detect-module' ); } else { shouldDetectModule = getOptionValue ('--experimental-require-module' ); } } const result = compileFunctionForCJSLoader (content, filename, false , shouldDetectModule); if (result.sourceMapURL ) { maybeCacheSourceMap (filename, content, cjsModuleInstance, false , undefined , result.sourceMapURL ); } return result; }
通过调用primordials.ReflectApply
执行包裹后的代码。primordials
是一个内部(internal
)模块,ReflectApply
用C++
实现,我没有看懂相关代码。
ECMAScript Module Node
将如下代码视作ESM
模块:
扩展名为.mjs
扩展名为.js
且package.json
中配置了"type": "module"
含有ESM
语法,并且和显式模块类型声明(文件扩展名和package.json
配置)不冲突。ESM
语法不包括import()
,因为它在两种模块系统中都能用;ESM
语法包括:
import
、export
、import.meta
顶层await
重新声明包裹函数参数require
、module
、exports
、__dirname
、__filename
导出 1 2 export default { foo : 1 } export const obj = { bar : 2 }
导入 1 2 3 4 import Obj from "module1" import { Obj as Obj2 } from "module2" import * as Obj3 from "module3" import ("module4" ).then (Obj4 => {})
用import导入CJS 我们测试如下的代码:
1 2 3 4 5 import lib1a from "./lib1.cjs?query=1" import * as lib1b from "./lib1.cjs?query=2" console .log ("import lib1a: " , lib1a, "\n" )console .log ("import * as lib1b: " , lib1b)
1 2 3 4 5 6 7 8 9 10 module .exports .f = function f (a, b ) { return a - b; } module .exports .U = class U { constructor ( ) { console .log ("constructed!" ) } }
在配置了"type": "module"
的语境下执行node index.js
,得到输出:
1 2 3 4 5 6 7 import lib1a: { f: [Function: f], U: [class U] } import * as lib1b: [Module: null prototype] { U: [class U], default: { f: [Function: f], U: [class U] }, f: [Function: f] }
module.exports
是默认导入会导入的内容。
1 2 3 import { default as lib } from "./lib.cjs" import lib from "./lib.cjs"
CJS包裹内容的平替 CJS
因为外层包裹可以使用__dirname
、__filename
、require.resolve()
等,在ESM
中,其平替存在于import.meta
中,其包含如下内容:
import.meta.filename
:同__filename
import.meta.dirname
:同__dirname
import.meta.resolve
:同require.resolve
,参数为想导入之模块的路径,返回值为其file:///
开头的URL
import.meta.url
:以file:///
开头的当前文件URL
但require.cache
、require.extensions
、NODE_PATH
没有平替,无法访问到。如果想使用require
函数,使用node:module
中的createRequire
函数创建require
函数。