注: 1. 本文涉及的nodejs源码如无特别说明则全部基于v10.14.1欢迎关注公众号:前端情报局Nodejs 中对模块的实现本节主要基于NodeJs源码,对其模块的实现做一个简要的概述,如有错漏,望诸君不吝指正。当我们使用require引入一个模块的时候,概况起来经历了两个步骤:路径分析和模块载入路径分析路径分析其实就是模块查找的过程,由_resolveFilename函数实现。我们通过一个例子,展开说明:const http = require(‘http’);const moduleA = requie(’./parent/moduleA’);这个例子中,我们引入两种不同类型的模块:核心模块-http和自定义模块moduleA对于核心模块而言,_resolveFilename会跳过查找步骤,直接返回,交给下一步处理if (NativeModule.nonInternalExists(request)) { // 这里的request 就是模块名称 ‘http’ return request;}而对于自定义模块而言,存在以下几种情况(_findPath)文件模块目录模块从node_modules目录加载全局目录加载这些在官方文档中已经阐述的很清楚了,这里就不再赘述。如果模块存在,那么_resolveFilename会返回该模块的绝对路径,比如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js。载入模块获取到模块地址后,Node就开始着手载入模块。首先,Node会查看模块是否存在缓存中:// filename 即模块绝对路径var cachedModule = Module._cache[filename];if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports;}存在则返回对应缓存内容,不存在则进一步判断该模块是否是核心模块:if (NativeModule.nonInternalExists(filename)) { return NativeModule.require(filename);}如果模块既不存在于缓存中也非核心模块,那么Node会实例化一个全新的模块对象function Module(id, parent){ // 通常是模块绝对路径 this.id = id; // 要导出的内容 this.exports = {}; // 父级模块 this.parent = parent; this.filename = null; // 是否已经加载成功 this.loaded = false; // 子模块 this.children = [];}var module = new Module(filename, parent);而后Node会根据路径尝试载入。function tryModuleLoad(module, filename) { var threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } }}对于不同的文件扩展名,其载入方法也有所不同。.js文件(_compile)通过fs同步读取文件内容后将其包裹在指定函数中:Module.wrapper = [ ‘(function (exports, require, module, __filename, __dirname) { ‘, ‘\n});’];调用执行此函数:compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);.json文件通过fs同步读取文件内容后,用JSON.parse解析并返回内容var content = fs.readFileSync(filename, ‘utf8’);try { module.exports = JSON.parse(stripBOM(content));} catch (err) { err.message = filename + ‘: ’ + err.message; throw err;}.node这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。return process.dlopen(module, path.toNamespacedPath(filename));.mjs这是用于处理ES6模块的扩展文件,是NodeJs在v8.5.0后新增的特性。对于这类扩展名的文件,只能使用ES6模块语法import引入,否则将会报错(启用 –experimental-modules的情况下)throw new ERR_REQUIRE_ESM(filename);如果一切顺利,就会返回附加在exports对象上的内容return module.exports;模块循环依赖接下来我们来探究一下模块循环依赖的问题:模块1依赖模块2,模块2依赖模块1,会发生什么?这里只探究commonjs的情况为此,我们创建了两个文件,module-a.js和module-b.js,并让他们相互引用:module-a.jsconsole.log(’ 开始加载 A 模块’);exports.a = 2;require(’./module-b.js’);exports.b = 3;console.log(‘A 模块加载完毕’);module-b.jsconsole.log(’ 开始加载 B 模块’);let moduleA = require(’./module-a.js’);console.log(moduleA.a,moduleA.b)console.log(‘B 模块加载完毕’);运行module-a.js,可以看到控制台输出:开始加载 A 模块开始加载 B 模块2 undefinedB 模块加载完毕A 模块加载完毕这时因为每个require都是同步执行的,在module-a完全加载前需要先加载./module-b,此时对于module-a而言,其exports对象上只附加了属性a,属性b是在./module-b加载完成后才赋值的。QA如何删除模块缓存?可以通过delete require.cache(moduleId)来删除对应模块的缓存,其中moduleId表示的是模块的绝对路径,一般的,如果我们需要对某些模块进行热更新,可以使用此特性,举个例子:// hot-reload.jsconsole.log(’this is hot reload module’);// index.jsconst path = require(‘path’);const fs = require(‘fs’);const hotReloadId = path.join(__dirname,’./hot-reload.js’);const watcher = fs.watch(hotReloadId);watcher.on(‘change’,(eventType,filename)=>{ if(eventType === ‘change’){ delete require.cache[hotReloadId]; require(hotReloadId); }});Node中可以使用ES6 模块吗?从8.5.0版本开始,NodeJs开始支持原生ES6模块,启用该功能需要两个条件:所有使用ES6模块的文件扩展名都必须是.mjs命令行选项–experimental-modulesnode –experimental-modules index.mjsnode –experimental-modules index.mjs但是截止到NodeJs v10.15.0,ES6模块的支持依旧是实验性的,笔者并不推荐在公司项目中使用参考nodejs-loader.js朴灵. 深入浅出Node.js