基于源码剖析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 net = require(“net”);

var fs = require(“fs”);

文件模块引入

require(’/文件名’);//绝对路径

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

require(’…/文件名’)

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

image-20201225162128528

引入规则
  • 如果有“./”从当前目录查找
  • 如果没有“./”,先从系统模块,再从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的错误。

实例:

image-20201225111727573

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,可以查看已经加载的原⽣模块。

4Module._resolveLookupPaths

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

image-20201225113943994

5Module._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、流程图

image-20201225154755972

image-20201225154847678

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));

运行结果如下所示:

img

  • 通过 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);

运行结果如下所示:

img

  • 通过 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);

运行结果如下所示:

img

源码:

https://github.com/nodejs/node/blob/v5.x/lib/module.js

https://github.com/nodejs/node/blob/v5.x/lib/internal/module.js

https://github.com/nodejs/node/blob/v5.x/lib/internal/bootstrap_node.js

目 录 第一章 开发环境 1.1 Qt 简介5 1.2 下载安装 Qt Creator 6 1.3 第一个程序 Hello World 7 第二章 窗体应用 1.1 窗体基类说明 12 1.2 控制窗体大小 13 1.3 窗体初始位置及背景色 13 1.4 修改标题栏图标 14 1.5 移动无边框窗体 16 1.6 去掉标题栏中最大化、最小化按钮 17 1.7 多窗体调用 18 1.8 字体形状窗体 20 第三章 控件应用 1.1 QPushButton按钮 23 1.2 QLabel标签 23 1.3 QLineEdit单行文本 24 1.4 QTextEdit多行文本 25 1.5 QPlainTextEdit多行文本 26 1.6 QComboBox下拉列表框 26 1.7 QFontComboBox字体下拉列表框 27 1.8 QSpinBox控件 28 1.9 QTimeEdit时间控件 29 1.10 QDateEdit日期控件 30 1.11 QScrollBar控件 30 1.12 QRadioButton单选按钮 31 1.13 QCheckBox复选框 32 1.14 QListView 列表控件 34 1.15 QTreeView树控件 34 1.16 QTableView表格控件 35 1.17 QHBoxLayout横向布局 36 1.18 QGridLayout网格布局 37 1.19 QGroupBox控件 38 1.20 QTabWidget控件 39 1.21 QMenu、QToolBar控件 41 1.22 任务栏托盘菜单 43 第四章 组件应用 1.1日历组件 47 1.2登录窗口 48 1.3文件浏览对话框 50 1.4颜色选择对话框 51 1.5进度条实例53 1.6Timer实时更新时间 54 第五章 文件操作 1.1创建文件夹 57 1.2写入文件 58 1.3修改文件内容 60 1.4删除文件 62 1.5修改文件名 63 1.6 INI文件写入操作 65 1.7 INI文件读取操作 68 1.8创建XML文件 71 1.9读取XML文件 72 第六章 图形图像操作 1.1绘制文字 75 1.2绘制线条 75 1.3绘制椭圆 77 1.4显示静态图像 78 1.5显示动态图像 78 1.6图片水平移动 79 1.7图片翻转 80 1.8图片缩放 82 1.9图片中加文字 84 1.10图像扭曲 85 1.11模糊效果 85 1.12着色效果 86 1.13阴影效果 87 1.14透明效果 87 第七章 多媒体应用 1.1音频、视频播放器 90 1.2播放Flash动画 94 1.3播放图片动画 95 第八章 系统操作 1.1获取屏幕分辨率 98 1.2获取本机名、IP地址 98 1.3根据网址获取IP地址 99 1.4判断键盘按下键值 100 1.5获取系统环境变量 101 1.6执行系统命令 102 第九章 注册表 1.0简要说明注册表 105 1.1写入注册表 105 1.2查找注册表 106 1.3修改IE浏览器的默认主页 107 第十章 数据库基础 1.1查询数据库驱动 109 1.2Qodbc连接Access数据库 109 1.3插入数据 111 1.4数据列表 112 1.5操作SQLite数据库 113 1.6SQLite数据库视图管理器 115 第十一章 网络开发 1.1点对点聊天服务端 119 1.2点对点聊天客户端 123 1.3局域网广播聊天 128 1.4SMTP协议发送邮件 148 1.5调用系统DLL判断网络连接状态 152 第十二章 进程与线程 1.1进程管理器 155 1.2线程QThread应用 158 1.3线程QRunnable应用 159 第十三章 数据安全 1.1 QByteArray加密数据 163 1.2 AES加密数据 164 1.3 MD5 加密数据 165 1.4 生成随机数 166 第十四章 打包部署 1.1 FilePacker 打包 169 1.2 Inno Setup 打包 174
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值