详解Node模块加载机制


一.require()时发生了什么?

Node.js 中,模块加载过程分为 5 步:
详解Node模块加载机制

  1. 路径解析(Resolution):根据模块标识找出对应模块(入口)文件的绝对路径

  2. 加载(Loading):如果是 JSON 或 JS 文件,就把文件内容读入内存。如果是内置的原生模块,将其共享库动态链接到当前 Node.js 进程

  3. 包装(Wrapping):将文件内容(JS 代码)包进一个函数,建立模块作用域,exports, require, module等作为参数注入

  4. 执行(Evaluation):传入参数,执行包装得到的函数

  5. 缓存(Caching):函数执行完毕后,将module缓存起来,并把module.exports作为require()的返回值返回

其中,模块标识(Module Identifiers)就是传入require(id)的第一个字符串参数id,例如require('./myModule')中的'./myModule',无需指定后缀名(但带上也无碍)

对于.、..、/开头的文件路径,尝试当做文件、目录来匹配,具体过程如下:

  1. 若路径存在并且是个文件,就当做 JS 代码来加载(无论文件后缀名是什么,require(./myModule.abcd)完全正确)

  2. 若不存在,依次尝试拼上.js、.json、.node(Node.js 支持的二进制扩展)后缀名

  3. 如果路径存在并且是个文件夹,就在该目录下找package.json,取其main字段,并加载指定的模块(相当于一次重定向)

  4. 如果没有package.json,就依次尝试index.js、index.json、index.node

对于模块标识不是文件路径的,先看是不是 Node.js 原生模块(fs、path等)。如果不是,就从当前目录开始,逐级向上在各个node_modules下找,一直找到顶层的/node_modules,以及一些全局目录:

  • NODE_PATH环境变量中指定的位置

  • 默认的全局目录:$HOME/.node_modules、$HOME/.node_libraries和$PREFIX/lib/node

P.S.关于全局目录的更多信息,见Loading from the global folders

找到模块文件后,读取内容,并包一层函数:

(function(exports, require, module, __filename, __dirname) {// Module code actually lives in here});

(摘自The module wrapper)

执行时从外部注入这些模块变量(exports, require, module, filename, dirname),模块导出的东西通过module.exports带出来,并将整个module对象缓存起来,最后返回require()结果

循环依赖
特殊的,模块之间可能会出现循环依赖,对此,Node.js 的处理策略非常简单:

// module1.jsexports.a = 1;require('./module2');exports.b = 2;exports.c = 3;// module2.jsconst module1 = require('./module1');console.log('module1 is partially loaded here', module1);

module1.js执行中引用了module2.js,module2又引了module1,此时module1尚未加载完(exports.b = 2; exports.c = 3;还没执行)。而在 Node.js 里,只加载了一部分的模块也可以正常引用:

When there are circular require() calls, a module might not have finished executing when it is returned.

所以module1.js执行结果是:

module1 is partially loaded here { a: 1 }

P.S.关于循环引用的更多信息,见Cycles

二.Node.js 内部是怎么实现的?
实现上,模块加载的绝大多数工作都是由module模块来完成的:

const Module = require('module');console.log(Module);

Module是个函数/类:

function Module(id = '', parent) {  this.id = id;  this.path = path.dirname(id);  // 即module.exports  this.exports = {};  this.parent = parent;  updateChildren(parent, this, false);  this.filename = null;  this.loaded = false;  this.children = [];}

每加载一个模块都创建一个Module实例,模块文件执行完后,该实例仍然保留,模块导出的东西依附于Module实例存在

模块加载的所有工作都是由module原生模块来完成的,包

Module._load、Module.prototype._compileModule._load

Module._load()负责加载新模块、管理缓存,具体如下:

Module._load = function(request, parent, isMain) {  // 0.解析模块路径  const filename = Module._resolveFilename(request, parent, isMain);  // 1.优先找缓存 Module._cache  const cachedModule = Module._cache[filename];  // 2.尝试匹配原生模块  const mod = loadNativeModule(filename, request, experimentalModules);  // 3.未命中缓存,也没匹配到原生模块,就创建一个新的 Module 实例  const module = new Module(filename, parent);  // 4.把新实例缓存起来  Module._cache[filename] = module;  // 5.加载模块  module.load(filename);  // 6.如果加载/执行出错了,就删掉缓存  if (threw) {    delete Module._cache[filename];  }  // 7.返回 module.exports  return module.exports;};Module.prototype.load = function(filename) {  // 0.判定模块类型  const extension = findLongestRegisteredExtension(filename);  // 1.按类型加载模块内容  Module._extensions[extension](this, filename);};

支持的类型有.js、.json、.node3 种:

// Native extension for .jsModule._extensions['.js'] = function(module, filename) {  // 1.读取JS文件内容  const content = fs.readFileSync(filename, 'utf8');  // 2.包装、执行  module._compile(content, filename);};// Native extension for .jsonModule._extensions['.json'] = function(module, filename) {  // 1.读取JSON文件内容  const content = fs.readFileSync(filename, 'utf8');  // 2.直接JSON.parse()完事  module.exports = JSONParse(stripBOM(content));};// Native extension for .nodeModule._extensions['.node'] = function(module, filename) {  // 动态加载共享库  return process.dlopen(module, path.toNamespacedPath(filename));};P.S.process.dlopen具体见process.dlopen(module, filename[, flags])Module.prototype._compileModule.prototype._compile = function(content, filename) {  // 1.包一层函数  const compiledWrapper = wrapSafe(filename, content, this);  // 2.把要注入的参数准备好  const dirname = path.dirname(filename);  const require = makeRequireFunction(this, redirects);  const exports = this.exports;  const thisValue = exports;  const module = this;  // 3.注入参数、执行  compiledWrapper.call(thisValue, exports, require, module, filename, dirname);};

包装部分的实现如下:

function wrapSafe(filename, content, cjsModuleInstance) {  let compiled = compileFunction(    content,    filename,    0,    0,    undefined,    false,    undefined,    [],    [      'exports',      'require',      'module',      '__filename',      '__dirname',    ]  );  return compiled.function;}

P.S.模块加载的完整实现见node/lib/internal/modules/cjs/loader.js

三.知道这些有什么用?
知道了模块的加载机制,在一些需要扩展篡改加载逻辑的场景很有用,比如用来实现虚拟模块、模块别名等

虚拟模块
比如,VS Code 插件通过require('vscode')来访问插件 API:

// The module 'vscode' contains the VS Code extensibility APIimport * as vscode from 'vscode';

而vscode模块实际上是不存在的,是个运行时扩展出来的虚拟模块:

// ref: src/vs/workbench/api/node/extHost.api.impl.tsfunction defineAPI() {  const node_module = <any>require.__$__nodeRequire('module');  const original = node_module._load;  // 1.劫持 Module._load  node_module._load = function load(request, parent, isMain) {    if (request !== 'vscode') {      return original.apply(this, arguments);    }    // 2.注入虚拟模块 vscode    // get extension id from filename and api for extension    const ext = extensionPaths.findSubstr(parent.filename);    let apiImpl = extApiImpl.get(ext.id);    if (!apiImpl) {      apiImpl = factory(ext);      extApiImpl.set(ext.id, apiImpl);    }    return apiImpl;  };}

具体见API 注入机制及插件启动流程_VSCode 插件开发笔记 2,这里不再赘述

模块别名
类似的,可以通过重写Module._resolveFilename来实现模块别名,比如把proj/src中的@lib/my-module模块引用映射到proj/lib/my-module:

// src/index.jsrequire('./patchModule');const myModule = require('@lib/my-module');console.log(myModule);

patchModule具体实现如下:

const Module = require('module');const path = require('path');const _resolveFilename =  Module._resolveFilename;Module._resolveFilename = function(request) {  const args = Array.from(arguments);  // 别名映射  const LIB_PREFIX = '@lib/';  if (request.startsWith(LIB_PREFIX)) {    console.log(request);    request = path.resolve(__dirname, '../' + request.slice(1));    args[0] = request;    console.log(` => ${request}`);  }  return _resolveFilename.apply(null, args);}

P.S.当然,一般不需要这样做,可以通过Webpack等构建工具来完成

清掉缓存
默认 Node.js 模块加载过就有缓存,而有些时候可能想要禁掉缓存,强制重新加载一个模块,比如想要读取能被用户频繁修改的 JS 文件(如webpack.config.js)

此时可以手动删掉挂在require.cache身上的module.exports缓存:

delete require.cache[require.resolve('./b.js')]

然而,如果b.js还引用了其它外部(非原生)模块,也需要一并删除:

const mod = require.cache[require.resolve('./b.js')];// 把引用树上所有模块缓存全都删掉(function traverse(mod) {  mod.children.forEach((child) => {    traverse(child);  });  console.log('decache ' + mod.id);  delete require.cache[mod.id];}(mod));

P.S.或者采用decache模块

参考资料
Node.js, TC-39, and Modules:以及译文

The Node.js Way – How require() Actually Works

Requiring modules in Node.js: Everything you need to know

Deep Dive Into Node.js Module Architecture

node.js require() cache – possible to invalidate?

更多相关文章

  1. 5 图看懂 Node 模块加载原理
  2. 模块_Haskell笔记2
  3. MyBatis 延迟加载、一二级缓存、架构设计的面试题(常问,重点了解)
  4. 每日学习-ansible yum模块
  5. 每日学习-ansible firewalld模块
  6. Spring【DAO模块】知识要点
  7. Spring【AOP模块】就这么简单
  8. 图书管理系统【用户、购买、订单模块、添加权限】

随机推荐

  1. 【Android(安卓)Training - UserInfo】记
  2. android手机定位不准的问题
  3. Android休眠机制
  4. 关于启动Android模拟器时,运行时,会再弹出
  5. Android虚拟平台的编译和整合
  6. Android(安卓)以图找图功能
  7. Android 基础总结:(二)Android APP基础及组
  8. Android能赢得开发者吗?
  9. IOS和Android OpenGL游戏引擎的集成AdMob
  10. Android系列教程(2):为 TextView组件加上边