以下内容基于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函数。