基于源码剖析nodejs模块系统

nodejs模块系统

简介

为了让Node.js的文件可以相互调用,Node.js提供了一个简单的模块系统。

模块是Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,

一个 Node.js 文件就是一个模块,这个文件可能是JavaScript 代码、JSON 或者编译过的C/C++ 扩展。

nodejs模块分类

  • 原生模块(核心模块):fs、http、net等
    在Node进程启动时,部分核心模块就被直接加载进内存中,这部分核心 模块引入时,文件定位和编译执行个步骤可以省略掉,并且在路径分析 中优先判断,所以它的加载速度是最快的。
  • 文件模块:用户编写的模块
    文件模块是运行时动态加载,需要完整的路径分析、文件定位、编译执行 过程,速度比核心模块慢。
  • 第三方模块:art-template、通过npm下载的

模块系统关键字

  • require
  • module.exports/exports

Node.js中没有全局作用域,只有模块作用域

​ ——外部访问不到内部,内部访问不到外部

node模块require引入分析

模块引⼊三部曲:

  • 路径分析
  • ⽂件定位
  • 编译执⾏

引入规则

var 自定义变量名称 = require 模块

1、加载文件模块,并执行里面的代码;

2、拿到被加载的文件模块导出的模块对象。

系统模块引入

var fs = require(“fs”);

文件模块引入

require('./文件名');//相对路径
require('../文件名')

如果直接引入会怎样呢?var test = require(“test”);

引入规则

  • 如果有“./”从当前目录查找
  • 如果没有“./”,先从系统模块,再从node_modules下查找

路径分析&文件定位

模块标识符分析:对于不同的标识符,模块的查找和定位不同。

  • 核心模块, 如http、fs、path等
  • “.”或“..”开始的相对路径文件模块
  • 以“/”开始的绝对路径文件模块
  • 非路径形式的文件模块,如che-ui模块

require()方法会将路径解析为真 实路径,并以真实路径进行加 载编译

文件定位:

  • 文件扩展名分析
  • 目录分析和包

代码追踪栈:

Module.prototype.require –> Module. load –> Module.resolveFilename –>

Module. resolveLookupPaths –> Module._fifindPath –> fifileName(⽂件绝对路径)

1、Module.prototype.require require入口

通过给定的path加载⼀个模块,并返回该模块的exports属性。

const assert = require('assert').ok;
...
// Loads a module at the given file path. Returns that module's 'exports'
property
Module.prototype.require = function(path) {
assert(path, "missing path");//path不能为空
assert(typeof path === "string", "path must be a string");//path必须是字
符串类型
return Module._load(path, this, false);//加载模块并返回exports
}

assert

assert 是Node.js中的断⾔模块: 提供简单的断⾔测试功能,主要⽤于内部使⽤,也可以

require(‘assert’) 后在外部进⾏使⽤。

模块⽅法:

  • assert(value[,message]) == assert.ok(value[,message])
  • 如果value的值为true,那么什么也不会发⽣;如果value为false,将抛出⼀个信息为message的错误。

实例:

2、加载⽂件⽅法Module._load

调⽤Module._resolveFilename获取⽂件绝对路径,并且根据该绝对路径添加缓存以及编译模块。

Module._load = function(request, parent, isMain) {
//...
var filename = Module.resolveFilename(request, parent); //路径解析,绝对路径
//...
}

3、解析路径⽅法Module._resolveFilename

获取⽂件绝对路径。

Module._resolveFilename = function(request, parent){
//是原⽣模块并且不是原⽣内部模块则直接返回
if(NativeModule.nonInternalExists(request)){
return request;
}
//计算所有可能的路径
var resolvedModule = Module._resolveLookupPaths(request, parent);
var id = resolvedModule[0];
var paths = resolvedModule[1];
//计算⽂件的绝对路径
var filename = Module._findPath(request, paths);
if(!filename){
var err = new Error(`Cannot find module '${request}'`);
err.code = "MODULE_NOT_FOUND";
throw err;
}
//返回⽂件绝对路径
return filename;
}

NativeModule.nonInternalExists

nonInternalExists是Node.js原⽣模块提供的⽅法,⽤于判断:是原⽣模块并且不是原⽣内部模块。

实现⽅法⾃⾏欣赏:

NativeModule.nonInternalExists = function(id){
return NativeModule.exists(id) && !NativeModule.isInternal(id);
}
NativeModule.isInternal = function(id){
return id.startsWith('internal/');
}

node/lib/module.js ⽂件开头引⼊的两个原⽣内部模块 const internalModule =require(‘internal/module’); //internal/module 即是路径名也是id const internalUtil =require(‘internal/util’);

也就是说在我们⾃⼰的代码⾥⾯是请求不到Node.js源码⾥⾯ lib/internal/*.js 这些⽂件的,⽐如 require("internal/module") 运⾏时会报错 Error: Cannot find module'internal/module'

特例 require("internal/repl") 可以执⾏,具体什么应⽤场景,请⾃⾏查找。

写个测试⽂件,在⾥⾯打印 process.moduleLoadList ,可以查看已经加载的原⽣模块。

4、Module._resolveLookupPaths

计算所有可能的路径,对于核⼼模块、相对路径、绝对路径、⾃定义模块返回不同的数组。实现代码相对较复杂不做分析,只看执⾏结果

5、Module._fifindPath

根据⽂件可能路径定位⽂件绝对路径,包括后缀的补全(.js , .json, .node)

Module._findPath = function(request, paths){
//绝对路径,将 paths 清空
if(path.isAbsolute(request)){
paths = [''];
}
//第⼀步:如果当前路径已在缓存中,直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
}
//获取后缀名:.js, .json, .node
const exts = Object.keys(Module._extensions);
//模块路径是否以/结尾,如果路径以/结尾,那么就是⽂件夹
const trailingSlash = request.slice(-1) === '/';
// 第⼆步,依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
// Don't search further if path doesn't exist
if (paths[i] && stat(paths[i]) < 1) continue;var basePath = path.resolve(paths[i], request);
var filename;
if (!trailingSlash) { // 模块路径⾮“/”结尾,那么可能是⽂件,也可能是⽂件夹
const rc = stat(basePath); // 判断⽂件类型,是⼀个⽂件还是⽬录
if (rc === 0) {
//a. 如果是⼀个⽂件,则转换为真实路径
filename = toRealPath(basePath);
} else if (rc === 1) {
//b. 如果是⼀个⽬录,则调⽤tryPackage⽅法读取该⽬录下的
package.json⽂件,把⾥⾯的 main属性设置为filename
filename = tryPackage(basePath, exts);
}
//c. 如果没有读到路径上的⽂件,则通过tryExtensions尝试在该路径后依次加上.js,.json 和.node后            缀,判断是否存在,若存在则返回加上后缀后的路径
if (!filename) {
filename = tryExtensions(basePath, exts);
}
}
//第三步:如果依然不存在,则同样调⽤tryPackage⽅法读取该⽬录下的package.json⽂件,把⾥⾯的           main属性设置为filename
if (!filename) {
filename = tryPackage(basePath, exts);
}
//第四步: 如果依然不存在,则尝试在该路径后依次加上index.js,index.json和index.node,判断是 否 存在,若存在则返回拼接后的路径。
if (!filename) {
// try it with each of the extensions at "index"
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
}
//第五步:若解析成功,则把解析得到的⽂件名cache起来,下次require就不⽤再次解析了
if (filename) {
// Warn once if '.' resolved outside the module dir
if (request === '.' && i > 0) {
warned = internalUtil.printDeprecationMessage(
'warning: require(\'.\') resolved outside the package ' +
'directory. This functionality is deprecated and will be
removed ' +'soon.', warned);
}
Module._pathCache[cacheKey] = filename;
return filename;
}
}
//第六步: 若解析失败,则返回false
return false;
}
//tryPackage
function tryPackage(requestPath, exts, isMain) {
var pkg = readPackage(requestPath);
if (!pkg) return false;
var filename = path.resolve(requestPath, pkg);
return tryFile(filename, isMain) || //直接判断这个⽂件是否存在并返回
tryExtensions(filename, exts, isMain) || //判断分别以js,json,node等后缀结尾的⽂件是否存在
tryExtensions(path.resolve(filename, 'index'), exts, isMain); //判断分别以${filename}/index.(js|json|node)等后缀结尾的⽂件是否存在
}
//tryExtensions
function tryExtensions(p, exts, isMain) {
for (var i = 0; i < exts.length; i++) {
const filename = tryFile(p + exts[i], isMain);
if (filename) {
return filename;
}
}
return false;
}
//tryFile
function tryFile(requestPath) {
const rc = stat(requestPath);
return rc === 0 && toRealPath(requestPath);
}
//toRealPath
function toRealPath(requestPath) {
return fs.realpathSync(requestPath, Module._realpathCache);
}

查找策略

  1. require()传入的字符串最后一个字符不是/时:

    1. 如果是个文件,直接返回这个文件的路径
    2. 如果是个文件夹,则查找该文件夹下是否有package.json文件,以及这个文件 当中的main字段对应的路径(对应源码当中的方法为tryPackage):

      1. 如果main字段对应的路径是一个文件且存在,直接返回这个路径
      2. 在main字段对应的路径后依次加上 .js , .json 和 .node 后缀,判断是否 存在,若存在则返回加上后缀后的路径。
      3. 在main字段对应的路径后依次加上 index.js ,index.json 和 index.node, 判断是否存在,若存在则返回拼接后的路径。
    3. 对文件路径后分别添加.js,.json,.node后缀,判断是否存在,若存在则返回 加上后缀后的路径。
  2. require()传入的字符串最后一个字符是/时,即require的是一个文件夹时:

    1. 查询该文件夹下的package.json文件中的main字段对应的路径,步骤如1.2
    2. 该路径后依次加上 index.js ,index.json 和 index.node,判断是否存在,若 存在则返回拼接后的路径。

6、路径解析完毕,再次返回Module._load

Module._load = function(request, parent, isMain) {
//解析⽂件绝对路径
//第⼀步: 先检查是否在⽂件模块缓存中,如果有缓存,直接取缓存,Module._cache存放⽂件模块
var filename = Module.resolveFilename(request, parent);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
//第⼆步: 检测是否是原⽣模块,如果是,使⽤原⽣模块的加载⽅法
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
//第三步: 判断⽆缓存且⾮原⽣模块后,新建模块实例
var module = new Module(filename, parent);
if (isMain) {
process.mainModule = module;
module.id = '.';
}
//加载模块前,就将模块缓存
Module._cache[filename] = module;
var hadException = true;
//第四步: 加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename]; //加载失败,删除缓存
}
}
return module.exports;
}

NativeModule.require

主要⽤来加载Node.js的⼀些原⽣模块。

源码:

NativeModule.require = function(id){
//1、判断是否是⾃身
if(id == 'native_module'){
return NativeModule
}
//2、是否有缓存,原⽣模块存放在NativeModule._cache中
var cached = NativeModule.getCached(id);
if(cached){
return cached.exports;
}
//3、是否是原⽣模块
if(!NativeModule.exists(id)){
throw new Error('No such native module ' + id);
}
//4、存放在模块加载列表⾥
process.moduleLoadList.push('NativeModule ' + id);
//5、载⼊该原⽣模块、缓存、编译、返回
var nativeModule = new NativeModule(id);
nativeModule.cache();
nativeModule.compile();
return nativeModule.exports;
}
NativeModule.prototype.compile = function() {
var source = NativeModule.getSource(this.id);
source = NativeModule.wrap(source);
var fn = runInThisContext(source, { filename: this.filename });
fn(this.exports, NativeModule.require, this, this.filename);
this.loaded = true;
};
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) {\n','\n});'
];
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};

编译执⾏

通过步骤5找到对应的文件后Node会新建一个模块对象,定义如下:

function Module (id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
if (parent && parent.children) {
parent.children.push(this);
}
this.filename = null;
this.loaded = false;
this.children = [];
}

根据路径载入并编译。对于不同的文件扩展名,其载入方法不同:

  • .js文件,通过fs模块同步读取文件后编译执行。
  • .node文件。
  • .json文件,通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • 其余扩展名文件,它们都被当作.js文件载入。

JS模块编译

Node对获取的JavaScript文件内容进行头尾包装

  • 头部: “(function (exports, require, module, __filename, _dirname {\n”
  • 尾部:“})”

2、包装后的代码会通过vm原生模块的runInThisContext()方法,返回一个具体的 function对象。

3、将当前模块对象的exports属性、require()方法、module(模块对象自身)以及 在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。执 行后,模块的exports属性被返回给调用方。

7、加载模块Module.prototype.load

Module.prototype.load = function(filename){
assert(!this.loaded);
this.filename = filename;
//获取这个module路径上所有可能的node_modules路径
this.paths = Module._nodeModulePaths(path.dirname(filename);
var extension = path.extname(filename) || ".js";
if(!Module._extensions[extension]) extension = ".js";
Module._extensions[extension](this, filename);
this.loaded = true;
}

调⽤ Module._extension ⽅法加载不同格式的⽂件

以下为js⽂件:

Module._extensions[".js"] = function(module, filename){
var content = fs.readFilSync(filename, 'utf8'); //同步读取⽂件的⽂本内容
module._compile(internalModule.stripBOM(content), filename); //编译
}

stripBOM内部原⽣模块的⽅法

function stripBOM(content){
//检测第⼀额字符是否为BOM;
//BOM:它常被⽤来当做标示⽂件是以UTF-8、UTF-16或UTF-32编码的记号。
if(content.charCodeAt(0) === 0xFEFF){
content = content.slice(1);
}
return content;
}

8、编译⽅法Module.prototype._compile

Module.prototype._compile = function(content, filename){
/**
*⽂件头部
*Module.wrapper = NativeModule.wrapper;
*Module.wrap = NativeModule.wrap;
*/
var wrapper = Module.wrap(content);
// vm.runInThisContext在⼀个v8的虚拟机内部执⾏wrapper后的代码,类似于eval
var compiledWrapper = runInThisContext(wrapper, {
filename: filename,
lineOffset: 0
})
//...
const dirname = path.dirname(filename);
/**
*这个require并⾮是Module.prototype.require⽅法,
*⽽是通过internalModule.makeRequireFunction重新构造出来的,
*这个⽅法内部还是依赖Module.prototype.require⽅法去加载模块的,
*同时还对这个require⽅法做了⼀些拓展。
*/
const require = internalModule.makeRequireFunction.call(this);
const args = [this.exports, require, this, filename, dirname];
const result = compiledWrapper.apply(this.exports, args);
return result;
}
function makeRequireFunction() {
const Module = this.constructor;
const self = this;
function require(path) {
try {
exports.requireDepth += 1;
return self.require(path);
} finally {
exports.requireDepth -= 1;
}
}
function resolve(request) {
return Module._resolveFilename(request, self);
}
require.resolve = resolve;
require.main = process.mainModule;
// Enable support to add extra extension types.
require.extensions = Module._extensions;require.cache = Module._cache;
return require;
}
  • require(): 加载外部模块
  • require.resolve():将模块名解析到⼀个绝对路径
  • require.main:指向主模块
  • require.cache:指向所有缓存的模块
  • require.extensions:根据⽂件的后缀名,调⽤不同的执⾏函数

9、扩展

以node index.js的形式启动,模块如何加载?

其实node启动的原理跟require是⼀样的,src/node.cc中的node::LoadEnvironment函数会被调⽤,

在该函数内则会接着调⽤lib/internal/bootstrap_node.js中的代码,并执⾏startup函数,startup函

数会执⾏Module.runMain⽅法,⽽Module.runMain⽅法会执⾏Module._load⽅法,参数就是命令

⾏的第⼀个参数(⽐如: node index.js),如此,跟前⾯介绍的require就⾛到⼀起了。

// bootstrap main module.
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};

10、流程图

Node模块导出

  • Node.js中是模块作用域 ,默认文件中的所有成员只在当前文件中有效(关闭原则)
  • 对于希望可以访问的模块成员,需将其挂载到module.exports 或 exports

在 NodeJS 中想要导出模块中的变量或者函数有三种方式

  • 通过exports.xxx = xxx 导出

a.js

let name = "it6666.top";
function sum(a, b) {
return a + b;
}
exports.str = name;
exports.fn = sum;

b.js

let aModule = require("./07-a");
console.log(aModule);
console.log(aModule.str);
console.log(aModule.fn(10, 20));

运行结果如下所示:

  • 通过 module.exports.xxx = xxx 导出

a.js

let name = "it6666.top";
function sum(a, b) {
return a + b;
}
module.exports.str = name;
module.exports.fn = sum;

b.js 其实可以不动的,我把返回值单独的接收了一下然后在输出打印。

let aModule = require("./07-a");
console.log(aModule);
console.log(aModule.str);
let res = aModule.fn(10, 20);
console.log(res);

运行结果如下所示:

  • 通过 global.xxx = xxx 导出

a.js

let name = "it6666.top";
function sum(a, b) {
return a + b;
}
global.str = name;
global.fn = sum;

b.js

let aModule = require("./07-a");
console.log(str);
let res = fn(10, 20);
console.log(res);

运行结果如下所示:

源码:

https://github.com/nodejs/nod…

https://github.com/nodejs/nod…

https://github.com/nodejs/nod…

SegmentFault博客
我还没有学会写个人说明!
上一篇

不改一行代码!快速迁移 Express 应用上云

下一篇

《赛博朋克2077》1.06更新主机版对比 改善不明显

你也可能喜欢

评论已经被关闭。

插入图片