Nodejs模块化 Nodejs遵循两套模块化规范:CommonJS规范和ESModule规范
CommonJS规范 CJS使用require引入模块,支持四种格式:
支持引入Nodejs内置模块,如http、os、fs、child_process等 支持引入第三方模块,如express、md5、koa等 支持引入自己编写的模块 支持引入C++扩展模块(.node文件) 支持引入json文件 1 2 3 4 5 const fs = require ('node:fs' ) const express = require ('express' ) const myModule = require ('./myModule.js' ) const nodeModule = require ('./myModule.node' ) const data = require ('./data.json' )
使用module.exports
导出模块
1 2 3 4 5 6 7 8 9 module .exports = { hello : function ( ) { console .log ('Hello, world!' ); } } module .exports = 123
ESModule规范 使用ESM模块前,需要在package.json设置"type": "module"
ESM使用import引入模块,而且import语句必须写在文件头部
1 2 3 4 5 6 import fs from 'node:fs' import { aaa } from './index.js' import { bbb as b } from './index2.js'
如果要引入json文件,则需要特殊处理:增加断言并指定类型为json,Node低版本不支持这一写法
前端项目中通常可以引入json文件,这是因为Vite或Webpack等拥有处理引入json的loader;而原生ESM确实不支持
1 2 3 import data from './data.json' assert { type : "json" }console .log (data)
也可以加载模块的整体对象,内含一个文件的所有导出
1 import * as all from 'xxx.js'
若需动态导入模块,使用import函数模式
1 2 3 4 if (true ){ import ('./test.js' ).then () }
使用default导出模块
1 2 3 4 5 6 7 export default { name : 'test' } export const a = 1
CJS和ESM的区别 CJS是基于运行时的同步加载;ESM是基于编译时的异步加载 CJS是可以修改值的;ESM值不可修改(可读的) CJS不可以tree shaking;ESM支持tree shaking(本质是因为第一条区别) CJS中顶层的this指向这个模块本身;而ESM中顶层this指向undefined 源码 处理.json文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Module ._extensions ['.json' ] = function (module , filename ) { const content = fs.readFileSync (filename, 'utf8' ); if (policy?.manifest ) { const moduleURL = pathToFileURL (filename); policy.manifest .assertIntegrity (moduleURL, content); } try { setOwnProperty (module , 'exports' , JSON Parse(stripBOM (content))); } catch (err) { err.message = filename + ': ' + err.message ; throw err; } };
处理.node文件:
1 2 3 4 5 6 7 8 9 10 Module ._extensions ['.node' ] = function (module , filename ) { if (policy?.manifest ) { const content = fs.readFileSync (filename); const moduleURL = pathToFileURL (filename); policy.manifest .assertIntegrity (moduleURL, content); } return process.dlopen (module , path.toNamespacedPath (filename)); };
处理.js文件:
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 62 Module ._extensions ['.js' ] = function (module , filename ) { const cached = cjsParseCache.get (module ); let content; if (cached?.source ) { content = cached.source ; cached.source = undefined ; } else { content = fs.readFileSync (filename, 'utf8' ); } if (StringPrototypeEndsWith (filename, '.js' )) { const pkg = readPackageScope (filename); if (pkg?.data ?.type === 'module' ) { const parent = moduleParentCache.get (module ); const parentPath = parent?.filename ; const packageJsonPath = path.resolve (pkg.path , 'package.json' ); const usesEsm = hasEsmSyntax (content); const err = new ERR_REQUIRE_ESM (filename, usesEsm, parentPath, packageJsonPath); if (Module ._cache [parentPath]) { let parentSource; try { parentSource = fs.readFileSync (parentPath, 'utf8' ); } catch { } if (parentSource) { const errLine = StringPrototypeSplit ( StringPrototypeSlice (err.stack , StringPrototypeIndexOf ( err.stack , ' at ' )), '\n' , 1 )[0 ]; const { 1 : line, 2 : col } = RegExpPrototypeExec (/(\d+):(\d+)\)/ , errLine) || []; if (line && col) { const srcLine = StringPrototypeSplit (parentSource, '\n' )[line - 1 ]; const frame = `${parentPath} :${line} \n${srcLine} \n${ StringPrototypeRepeat(' ' , col - 1 )} ^\n` ; setArrowMessage (err, frame); } } } throw err; } } module ._compile (content, filename); };
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 62 63 Module .prototype ._compile = function (content, filename ) { let moduleURL; let redirects; const manifest = policy?.manifest ; if (manifest) { moduleURL = pathToFileURL (filename); redirects = manifest.getDependencyMapper (moduleURL); manifest.assertIntegrity (moduleURL, content); } const compiledWrapper = wrapSafe (filename, content, this ); let inspectorWrapper = null ; if (getOptionValue ('--inspect-brk' ) && process._eval == null ) { if (!resolvedArgv) { if (process.argv [1 ]) { try { resolvedArgv = Module ._resolveFilename (process.argv [1 ], null , false ); } catch { assert (ArrayIsArray (getOptionValue ('--require' ))); } } else { resolvedArgv = 'repl' ; } } if (resolvedArgv && !hasPausedEntry && filename === resolvedArgv) { hasPausedEntry = true ; inspectorWrapper = internalBinding ('inspector' ).callAndPauseOnStart ; } } 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 (); if (inspectorWrapper) { result = inspectorWrapper (compiledWrapper, thisValue, exports , require , module , filename, dirname); } else { result = ReflectApply (compiledWrapper, thisValue, [exports , require , module , filename, dirname]); } hasLoadedAnyUserCJSModule = true ; if (requireDepth === 0 ) statCache = null ; return result; };
wrapSave方法:
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 function wrapSafe (filename, content, cjsModuleInstance ) { if (patched) { const wrapper = Module .wrap (content); const script = new Script (wrapper, { filename, lineOffset : 0 , importModuleDynamically : async (specifier, _, importAssertions) => { const loader = asyncESM.esmLoader ; return loader.import (specifier, normalizeReferrerURL (filename), importAssertions); }, }); if (script.sourceMapURL ) { maybeCacheSourceMap (filename, content, this , false , undefined , script.sourceMapURL ); } return script.runInThisContext ({ displayErrors : true , }); }
wrapSafe调用的wrap方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 let wrap = function (script ) { return Module .wrapper [0 ] + script + Module .wrapper [1 ]; }; const wrapper = [ '(function (exports, require, module, __filename, __dirname) { ' , '\n})' , ];