模块加载

模块加载

基本知识

Node中的模块分为以下几类:

  • 核心模块, 如http fs path等
  • 以 . 或者 .. 开始的相对路径文件
  • 以 / 开始的绝对路径文件
  • 非路径形式的文件模块, 如自定义的connect模块

在模块加载时,Node会按照 .js .json .node的次序补足扩展名,依次尝试。对于第四种的自定义模块,Node在加载时会从当前文件目录下的node_modules文件开始,依次遍历父文件夹进行查找。 项目的目录为test,通过module.paths可以知道模块查找时可能遍历的路径:

模块查找路径
require源码解析

在Node中,需通过 var module = require("module") 这种形式调用模块,其内部实现逻辑如下:

// 模块加载入口
Module._load = function(request, parent, isMain) {
  // 返回文件名 会调用到__findpath
  var filename = Module._resolveFilename(request, parent);

  // 有缓存直接返回缓存
  var cachedModule = Module._cache[filename];
  if (cachedModule) {
    return cachedModule.exports;
  }
  ...
  // 加载文件
  try {
    module.load(filename);
    hadException = false;
  } finally {
    if (hadException) {
      delete Module._cache[filename];
    }
  }

  return module.exports;
};

// 根据参数 返回文件名, _findPath的逻辑是
// 1. 若模块的路径不以 / 结尾,则先检查该路径是否真实存在: 
// 2. 若存在且为一个文件,则直接返回文件路径作为结果。 
// 3. 若存在且为一个目录,则尝试读取该目录下的 package.json 中 main 属性所指向的文件路径。 
// 4. 判断该文件路径是否存在,若存在,则直接作为结果返回。 
// 5. 尝试在该路径后依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径。 
// 6. 尝试在该路径后依次加上 index.js index.json 和 index.node,判断是否存在,若存在则返回拼接后的路径。 
// 7. 若仍未返回,则为指定的模块路径依次加上 .js , .json 和 .node 后缀,判断是否存在,若存在则返回加上后缀后的路径
Module._resolveFilename = function(request, parent) {
  ...
  var filename = Module._findPath(request, paths);
  ...
  return filename;
};


// 加载一个文件
Module.prototype.load = function(filename) {
  ...
  Module._extensions[extension](this, filename);
  ...
};

// 以.js结尾的文件为例  load函数 会执行到_compile方法中去
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8');
  module._compile(internalModule.stripBOM(content), filename);
};

Module.prototype._compile = function(content, filename) {
  // 包裹脚本
  var wrapper = Module.wrap(content);
  var compiledWrapper = runInThisContext(wrapper,{ filename: filename, lineOffset: 0 });
  ...

  // 执行逻辑
  const args = [this.exports, require, this, filename, dirname];
  const result = compiledWrapper.apply(this.exports, args);
  return result;
};

// Module.wrap的逻辑  把脚本前后包括起来,形成一个函数
NativeModule.wrap = function(script) {
    return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
    '(function (exports, require, module, __filename, __dirname) { ',
    '\n});'
];

通过对模块加载流程的梳理,可知module是对象,而require是函数;且它俩实际上是一个函数的参数,并不是全局属性:

console.log(require) // Function  
console.log(module) // Object  
console.log(global.require) // undefined  
console.log(global.module) // undefined 

模块加载的问题

我们假设的场景是:test和test1是一个大工程的两个子工程,为了维护整个工程中模块的统一(版本一致、同步更新等),我们希望一个模块在工程中只有一份代码存在。两个子工程的文件夹名称和工程一样。如果在开发时,test工程想引用test1工程下的模块,那么我们可以采用如下方法:

  var moduleA = require("../test1/node_modules/moduleA")

这种方式有两个问题: 1. 写法比较丑 2. 难以维护。尤其是第二条,在真正的开发时,这是个比较令人头疼的地方,因此需要一个比较好的解决方法。

上述采用的是相对路径,我们可以通过全局的绝对路径进行实现:

global._rootTest1 = '/Users/ahu/test1/node_modules';
var path = require('path');
var moduleA = require(path.join(_rootTest1,'moduleA'));

这个和相对路径类似,有点换汤不换药的感觉,但也不失为一种方法。

模块加载优化

普通第三方模块加载只需要require进来就好,没有路径的问题;因此我们以此为目标考虑我们模块的加载优化。

var moduleA = require('moduleA');
修改module.paths

我们知道module.paths是模块查找时要遍历的文件夹路径,如果往其中添加模块所在的路径,那么就可以直接通过模块名加载到模块了:

module.paths.push('/Users/ahu/test1/node_modules');
console.log(module.paths);
var moduleA = require('moduleA');

Paste_Image.png

在考虑效率的情况下,需要依据路径下模块数决定其在module.paths的位置,数组位置越靠前,模块加载的优先级越高。

虽然基本目的达到了,但是对其原理不是很了解。我们从模块加载的源码进行探索:

// 初始化全局的依赖加载路径
Module._initPaths = function() {
  ...
  var paths = [path.resolve(process.execPath, '..', '..', 'lib', 'node')];

  ...
  // 我们需要着重关注此处,获取环境变量“NODE_PATH”
  var nodePath = process.env['NODE_PATH'];
  if (nodePath) {
    paths = nodePath.split(path.delimiter).concat(paths);
  }

  // modulePaths记录了全局加载依赖的根目录,在Module._resolveLookupPaths中有使用
  modulePaths = paths;
};

// @params: request为加载的模块名 
// @params: parent为当前模块(即加载依赖的模块)
Module._resolveLookupPaths = function(request, parent) {
  ...
 
  var start = request.substring(0, 2);
  // 若为引用模块名的方式,即require('moduleA')
  if (start !== './' && start !== '..') {
    // 此处的modulePaths即为Module._initPaths函数中赋值的变量
    var paths = modulePaths;
    if (parent) {
      if (!parent.paths) parent.paths = [];
      paths = parent.paths.concat(paths);
    }
    return [request, paths];
  } 
  ...
};

通过Node module加载的源码可知,影响模块加载的有以下几点:

  • NODE_PATH这个环境变量
  • Module的_initPaths方法,只执行一次

基于这两点我们进行尝试。

NODE_PATH

可以修改系统环境变量中的NODE_PATH,需要保证开发、测试、发布环境同步进行修改,比较麻烦;而且由于影响范围较大,可能影响程序的正常运行:

export NODE_PATH=/Users/ahu/test1/node_modules

也可以在服务启动时修改NODE_PATH,如下方式:

NODE_PATH=/Users/ahu/test1/node_modules  node test.js

这个影响访问小,发布环境中借组启动脚本可以比较优雅的实现,但是开发时有可能比较麻烦。

process.env

除了上面两种,可以在程序中修改process.env中的NODE_PATH进行实现,但是由于_initPaths只执行一次而且已经执行完毕,因此需要重新执行一边:

process.env.NODE_PATH='/Users/ahu/test1/node_modules';
require('module').Module._initPaths();

var moduleA = require('moduleA');

总结

本文提出的优化方法都是对 NODE_PATH 进行修改,包括对系统环境变量和程序运行环境变量修改两方面。app-module-path这个模块也通过类似的方法进行实现。

参考文章

module源码
nativeModule源码
通过源码解析 Node.js 中一个文件被 require 后所发生的故事
node模块加载层级优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容