最近在试图加载npm全局下的express模块的时候,出现Error: Cannot find module 'express'的报错,按理说我已经将express安装到了全局的node_modules目录,为什么还会加载不到这个模块呢?
本文将以该问题作为切入点,记录一下node的module加载机制,算是自我学习吧。

NPM

NPM(node package manager)是NODE下的一款包管理工具,它的主要功能就是管理node包,包括:安装、卸载、更新、查看、搜索、发布等。它将开发者从繁琐的包管理工作中解放出来,极大的便利了我们的开发。

NPM有两种安装方式:

  • npm install –global xxx 属于全局安装(示例:npm install -g express)
  • npm install xxx 属于本地安装

全局安装是把包安装到了(npm config get prefix)lib/node_modules这个全局目录下
本地安装则是把包安装到了当前目录下的./node_modules目录下。

NODE_PATH

NODE_PATH是NODE中用来寻找模块所提供的路径注册环境变量,像操作系统中都会有一个PATH环境变量,当系统调用一个命令的时候,就会在PATH变量中注册的路径中寻找,如果注册的路径中有就调用,否则就提示命令没找到,NODE_PATH类似如此。

# 将 /usr/bin 追加到 PATH 变量中
export PATH=$PATH: 

# 指定 NODE_PATH 变量,并用:分隔了多个不同的目录
export NODE_PATH="/usr/lib/node_modules:/usr/local/lib/node_modules" 

通过上面的方法指定NODE_PATH环境变量后,文章开始遇到的问题就迎刃而解了,哦,原来是没有设置NODE_PATH环境变量的缘故(在Windows中,NODE_PATH用分号而不是冒号分隔)。

在unix系统下,可以通过如下方式添加NODE_PATH环境变量

vi ~/.bash_profile

如果在用户目录下没有.bash_profile文件,则新建一个,然后填写如下内容

export NODE_PATH="/usr/lib/node_modules:/usr/local/lib/node_modules"

最后,通过 source .bash_profile命令使其立即生效。

模块缓存

模块可以多次加载,但只会在第一次加载的时候运行一次,然后运行结果(module.exports)就被缓存了,以后再加载,就直接读取缓存结果。

模块加载

NODE模块加载(查找)是从项目的根位置递归搜寻 node_modules 目录,直到文件系统根目录的 node_modules,如果还没有查找到指定模块的话,就会去NODE_PATH中注册的路径中查找。

在Node.js中,对于每一个被加载的文件模块,创建这个模块对象的时候,这个模块便会有一个paths属性,其值根据当前文件的路径计算得到。我们创建modulepath.js这样一个文件,其内容为:

console.log(module.paths);

执行 node modulepath.js, 将得到如下输出结果:

[ '/Users/sobird/node_modules',
'/Users/node_modules',
'/node_modules' ]

如上,Node.js的模块加载正式按照上面给出的目录集合按照顺序进行加载。
如果require是绝对路径,则直接加载,无需查找,速度最快。

具体如下:

  1. 从module.paths数组中取出第一个目录作为查找基准
  2. 直接从目录中查找该文件,如果文件存在则结束查找,否则进入下一条查找
  3. 尝试加载.js,.node, .json后缀之后查找,如果文件存在则结束查找,否则进入下一条查找
  4. 尝试将require的参数作为一个包来进行查找,读取目录下的package.json文件,取得main参数指定的文件
  5. 尝试查找该文件,如果文件存在则结束查找,否则进行第三条查找
  6. 如果继续查找失败,则取出module.paths数组中的下一目录作为基准查找,循环第1-5步骤
  7. 如果继续查找失败,循环第1-6步骤,直到module.paths中的最后一个值
  8. 如果继续查找失败,则抛出异常

动态更改NODE_PATH

明白了上面module.paths的作用,我们在代码编写的时候,就可以随意指定模块加载路径,比如,现在有模块a.js 和 b.js,他们在磁盘中的路径分别是 /path/a/a.js, /path/b/b.js。

如果我们想在a模块中加载b模块的依赖c,可以通过下面的写法实现:

// 加载模块绝对路径
var b = require('/path/b/node_modules/c.js');

那么,如果通过相对路径的方式如何加载?

// 在a模块中这方式是加载不到b模块的
// 修改module.paths的目录集合,将b模块的node_modules目录push进来
module.paths = module.paths.push('/path/b/node_modules');
var b = require('c.js');

如上,这样就可以加载到b模块的依赖c模块了~

参考