require用法及源码解析
一.require()的基本用法
require语句内部逻辑:
当 Node 遇到 require(X) 时,按下面的顺序处理。
(1)如果 X 是内置模块(比如 require('http'))
a. 返回该模块。
b. 不再继续执行。(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头
a. 根据 X 所在的父模块,确定 X 的绝对路径。
b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
- X
- X.js
- X.json
- X.node
c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。
- X/package.json(main字段)
- X/index.js
- X/index.json
- X/index.node
(3)如果 X 不带路径
a. 根据 X 所在的父模块,确定 X 可能的安装目录。
b. 依次在每个目录中,将 X 当成文件名或目录名加载。(4) 抛出 "not found"
请看一个例子。
当前脚本文件 /home/ry/projects/foo.js 执行了 require('bar') ,这属于上面的第三种情况。Node 内部运行过程如下。
首先,确定 x 的绝对路径可能是下面这些位置,依次搜索每一个目录。
/home/ry/projects/node_modules/bar /home/ry/node_modules/bar /home/node_modules/bar /node_modules/bar
搜索时,Node 先将 bar 当成文件名,依次尝试加载下面这些文件,只要有一个成功就返回。
bar bar.js bar.json bar.node
如果都不成功,说明 bar 可能是目录名,于是依次尝试加载下面这些文件。
bar/package.json(main字段) bar/index.js bar/index.json bar/index.node
如果在所有目录中,都无法找到 bar 对应的文件或目录,就抛出一个错误。
二.Module构造函数
源码:
require的源码早Node的lib/module.js文件.下面是简化过的:
1 function Module(id, parent) { 2 this.id = id; 3 this.exports = {}; 4 this.parent = parent; 5 this.filename = null; 6 this.loaded = false; 7 this.children = []; 8 } 9 10 module.exports = Module; 11 12 var module = new Module(filename, parent);
上面代码中,Node定义了一个构造函数Module,所有的模块都是Module的实例.可以看到,当前模块(module.js)也是Module的一个实例.
每个实例都要自己的属性.新建一个脚本看看这些属性的值是什么.
1 // a.js 2 3 console.log('module.id: ', module.id); 4 console.log('module.exports: ', module.exports); 5 console.log('module.parent: ', module.parent); 6 console.log('module.filename: ', module.filename); 7 console.log('module.loaded: ', module.loaded); 8 console.log('module.children: ', module.children); 9 console.log('module.paths: ', module.paths);
运行这个脚本:
1 $ node a.js 2 3 module.id: . 4 module.exports: {} 5 module.parent: null 6 module.filename: /home/ruanyf/tmp/a.js 7 module.loaded: false 8 module.children: [] 9 module.paths: [ '/home/ruanyf/tmp/node_modules', 10 '/home/ruanyf/node_modules', 11 '/home/node_modules', 12 '/node_modules' ]
可以看到,如果没有父模块,直接调用当前模块,parent属性就是null,id属性就是一个点.filename属性是模块的绝对路径,path属性是一个数组,包含模块肯的位置
另外,输出这些内容时,模块还没有全部加载,所以loaded属性为false.
新建另一个脚本文件b.js让其调用a.js
1 // b.js 2 3 var a = require('./a.js'); 4 5 //运行b.js 6 7 $ node b.js 8 9 module.id: /home/ruanyf/tmp/a.js 10 module.exports: {} 11 module.parent: { object } 12 module.filename: /home/ruanyf/tmp/a.js 13 module.loaded: false 14 module.children: [] 15 module.paths: [ '/home/ruanyf/tmp/node_modules', 16 '/home/ruanyf/node_modules', 17 '/home/node_modules', 18 '/node_modules' ]
上面代码中,由于a.js被b.js调用,所以parent属性指向b.js模块,id属性和filename属性一直,都是模块的绝对路径.
三.模块实例的require方法
每个模块实例都有一个require方法.
Module.prototype.require =funtion (path){
return Module._load(path,this);
};
由此可知,require并不是全局命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用requrie命令(额日益例外是REPL环境).
另外,requrie其实内部调用Module._load方法.
下面来看Module._load的源码.
1 Module._load = function(request, parent, isMain) { 2 3 // 计算绝对路径 4 var filename = Module._resolveFilename(request, parent); 5 6 // 第一步:如果有缓存,取出缓存 7 var cachedModule = Module._cache[filename]; 8 if (cachedModule) { 9 return cachedModule.exports; 10 11 // 第二步:是否为内置模块 12 if (NativeModule.exists(filename)) { 13 return NativeModule.require(filename); 14 } 15 16 // 第三步:生成模块实例,存入缓存 17 var module = new Module(filename, parent); 18 Module._cache[filename] = module; 19 20 // 第四步:加载模块 21 try { 22 module.load(filename); 23 hadException = false; 24 } finally { 25 if (hadException) { 26 delete Module._cache[filename]; 27 } 28 } 29 30 // 第五步:输出模块的exports属性 31 return module.exports; 32 };
上面代码中,首先解析出模块的绝对路径(filename),以作为模块的识别符.然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块.
因此,Module._load的关键步骤是两个.
Module._resovleFilename():确定模块的绝对路径
module.load():加载模块
四.模块的绝对路径
下面是Module._resolveFilename方法的源码.
1 Module._resolveFilename = function(request, parent) { 2 3 // 第一步:如果是内置模块,不含路径返回 4 if (NativeModule.exists(request)) { 5 return request; 6 } 7 8 // 第二步:确定所有可能的路径 9 var resolvedModule = Module._resolveLookupPaths(request, parent); 10 var id = resolvedModule[0]; 11 var paths = resolvedModule[1]; 12 13 // 第三步:确定哪一个路径为真 14 var filename = Module._findPath(request, paths); 15 if (!filename) { 16 var err = new Error("Cannot find module '" + request + "'"); 17 err.code = 'MODULE_NOT_FOUND'; 18 throw err; 19 } 20 return filename; 21 };
上面代码中.在Module.resolveFilename方法内部,又调用了两个方法Module.resolveLookupPaths()和Module._findPath(),前者用来列出可能的路径,猴子用来确认
哪一个路径为真.
Module._resolveLookupPaths()的运行结果.
1 [ '/home/ruanyf/tmp/node_modules', 2 '/home/ruanyf/node_modules', 3 '/home/node_modules', 4 '/node_modules' 5 '/home/ruanyf/.node_modules', 6 '/home/ruanyf/.node_libraries', 7 '$Prefix/lib/node' ]
上面的模块就是模块所有可能的路径.基本是是从当前路径上一级向上寻找node_modules子目录.最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了.
有了可能的路径以后,下面就是Module._findPath()的源码,用来确定到底哪一个是正确路径.
1 Module._findPath = function(request, paths) { 2 3 // 列出所有可能的后缀名:.js,.json, .node 4 var exts = Object.keys(Module._extensions); 5 6 // 如果是绝对路径,就不再搜索 7 if (request.charAt(0) === '/') { 8 paths = ['']; 9 } 10 11 // 是否有后缀的目录斜杠 12 var trailingSlash = (request.slice(-1) === '/'); 13 14 // 第一步:如果当前路径已在缓存中,就直接返回缓存 15 var cacheKey = JSON.stringify({request: request, paths: paths}); 16 if (Module._pathCache[cacheKey]) { 17 return Module._pathCache[cacheKey]; 18 } 19 20 // 第二步:依次遍历所有路径 21 for (var i = 0, PL = paths.length; i < PL; i++) { 22 var basePath = path.resolve(paths[i], request); 23 var filename; 24 25 if (!trailingSlash) { 26 // 第三步:是否存在该模块文件 27 filename = tryFile(basePath); 28 29 if (!filename && !trailingSlash) { 30 // 第四步:该模块文件加上后缀名,是否存在 31 filename = tryExtensions(basePath, exts); 32 } 33 } 34 35 // 第五步:目录中是否存在 package.json 36 if (!filename) { 37 filename = tryPackage(basePath, exts); 38 } 39 40 if (!filename) { 41 // 第六步:是否存在目录名 + index + 后缀名 42 filename = tryExtensions(path.resolve(basePath, 'index'), exts); 43 } 44 45 // 第七步:将找到的文件路径存入返回缓存,然后返回 46 if (filename) { 47 Module._pathCache[cacheKey] = filename; 48 return filename; 49 } 50 } 51 52 // 第八步:没有找到文件,返回false 53 return false; 54 };
经过上面代码,就可以找到模块的绝对路径了.
有时在项目代码中,需要调用模块的绝对路径,那么除了module.filename,Node还提供一个require.resolve方法,供外部调用,用于从模块名渠道绝对路径.
1 require.resolve = function(request) { 2 return Module._resolveFilename(request, self); 3 }; 4 5 // 用法 6 require.resolve('a.js') 7 // 返回 /home/ruanyf/tmp/a.js
五.加载模块
有了模块的绝对路径,就可以加载该模块了.下面是module.load方法的源码.
1 Module.prototype.load = function(filename) { 2 var extension = path.extname(filename) || '.js'; 3 if (!Module._extensions[extension]) extension = '.js'; 4 Module._extensions[extension](this, filename); 5 this.loaded = true; 6 };
上面代码中,首先确定模块的后缀名,不同的后缀名对应不同的加载方法.下面是.js和.json后缀名对应的处理方法.
1 Module._extensions['.js'] = function(module, filename) { 2 var content = fs.readFileSync(filename, 'utf8'); 3 module._compile(stripBOM(content), filename); 4 }; 5 6 Module._extensions['.json'] = function(module, filename) { 7 var content = fs.readFileSync(filename, 'utf8'); 8 try { 9 module.exports = JSON.parse(stripBOM(content)); 10 } catch (err) { 11 err.message = filename + ': ' + err.message; 12 throw err; 13 } 14 };
这里只讨论js文件的加载.首先,将模块文件读取成字符串,然后剥离utf8编码特有的BOM文件头,最后编译该模块.
module._compile方法用于模块的编译.
1 Module.prototype._compile = function(content, filename) { 2 var self = this; 3 var args = [self.exports, require, self, filename, dirname]; 4 return compiledWrapper.apply(self.exports, args); 5 };
上面的代码等同于下面的形式.
1 (function (exports, require, module, __filename, __dirname) { 2 // 模块源码 3 });
也就是说,模块的加载实质上就是注入exports,requrie,module三个全局变量,然后执行的源码,然后将模块的export编码的值输出.
原文地址:http://www.ruanyifeng.com/blog/2015/05/require.html