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

引入规则

  • 如果有“./”从当前目录查找
  • 如果没有“./”,先从系统模块,再从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,可以查看已经加载的原⽣模块。

4Module._resolveLookupPaths

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

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, .nodeconst 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 existif (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属性设置为filenamefilename = tryPackage(basePath, exts);}//c. 如果没有读到路径上的⽂件,则通过tryExtensions尝试在该路径后依次加上.js,.json 和.node后         缀,判断是否存在,若存在则返回加上后缀后的路径if (!filename) {filename = tryExtensions(basePath, exts);} }//第三步:如果依然不存在,则同样调⽤tryPackage⽅法读取该⽬录下的package.json⽂件,把⾥⾯的          main属性设置为filenameif (!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 dirif (request === '.' && i > 0) {warned = internalUtil.printDeprecationMessage('warning: require(\'.\') resolved outside the package ' +'directory. This functionality is deprecated and will beremoved ' +'soon.', warned);}Module._pathCache[cacheKey] = filename;return filename;} }//第六步: 若解析失败,则返回falsereturn 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后的代码,类似于evalvar 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 programprocess._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/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

基于源码剖析nodejs模块系统相关推荐

  1. [Abp vNext 源码分析] - 2. 模块系统的变化

    一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...

  2. Dubbo协议模块源码剖析

    Dubbo协议模块源码剖析 目录 概 述 RPC协议报文编码与实现详解 RPC 传输实现: 拆包与粘包解决办法: Dubbo 报文格式 分析: 小结: 参考资料和推荐阅读 LD is tigger f ...

  3. Python envoy 模块源码剖析

    Kenneth Reitz 是公认的这个世界上 Python 代码写得最好的人之一.抱着学习的心态,我阅读了 Reitz 写的 envoy 模块的源码,将笔记记录如下. 介绍 和 requests 模 ...

  4. Python猫荐书系统之四:《Python源码剖析》

    大家好,新一期的荐书栏目如期跟大家见面了. 先来看看今天的主角是谁:<Python源码剖析--深度探索动态语言核心技术>,2008年出版,作者 @陈儒 ,评分8.7分. 是的,你没看错,出 ...

  5. Python个人网盘源码、云盘系统源程序,基于Django+Mysql

    Python个人网盘源码.云盘系统源程序,基于Django+Mysql 1.安装依赖 pip install -r requirements.txt 2.检查配置文件,修改邮箱和数据库配置 # myc ...

  6. GDAL源码剖析(一)

    前言:一直在使用和研究GDAL的相关东西,发现网上对GDAL的内容倒是不少,但是很少有系统的介绍说明,以及内部的一些结构说明,基于这些原因,将本人的一些粗浅的理解放在此处,形成一个系列,暂时名为< ...

  7. Chrome源码剖析、上--多线程模型、进程通信、进程模型

    Chrome源码剖析.上 原著:duguguiyu. 整理:July. 时间:二零一一年四月二日. 出处:http://blog.csdn.net/v_JULY_v. 说明:此Chrome源码剖析很大 ...

  8. Chrome源码剖析 上--多线程模型 进程通信 进程模型

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! Chro ...

  9. CHROME源码剖析 上《转》

    转自:http://www.blogjava.net/xiaomage234/archive/2012/02/16/370122.html 原著:duguguiyu. 整理:July. 时间:二零一一 ...

最新文章

  1. 怎么修改网页服务器数据库连接,如何修改网页服务器数据库连接
  2. Logback配置输出sql
  3. python利用win32com读取doc和pdf内容,并保存到文件
  4. TransactionProducer(事务消息)
  5. 安装MySql出现Error Nr.1045的解决办法
  6. 【翻译】C#表达式中的动态查询
  7. 如何验证某个 string 是否为合法的 GUID ?
  8. php cdi_使用Fabric8在CDI管理的bean中注入Kubernetes Services
  9. 如何在CentOS / RHEL上使用yum命令
  10. python实现嵌套功能_我应该如何在Python中实现“嵌套”子命令?
  11. HttpComponents之httpclient基本使用方法
  12. JAVA中JDK环境变量配置
  13. 关于hive报错expression not in group by key ‘.....‘
  14. linux系统怎么禁用键盘,Linux之禁用笔记本键盘
  15. android手机传感器,安卓手机传感器
  16. 用VC++5.0播放AVI文件的两种方法
  17. 并行查询的执行计划解读
  18. python输入一个数字n、计算1到n的和_怎么用python求1到n所有整数的和
  19. 花了一晚上时间,终于把Python的基本用法归纳好了
  20. android 调试原理

热门文章

  1. KPN iTV的敏捷转型之旅
  2. mysql 导出表数据表结构
  3. Oracle Sharding
  4. 使用C#客户端访问FTP服务的一个解决方案
  5. LVM原理、创建、扩容、缩减、快照详解
  6. Coolite Toolkit学习笔记一:AjaxEvent、AjaxMethod和Listeners
  7. 去掉 RHEL AS 3 内存检测达不到256MB的警告
  8. OpenCV从Mat中提取某些行或列
  9. python中文件打开的合法模式组合_以下选项中,不是Python文件打开的合法模式组合是:...
  10. 计算机自动控制论文,电气自动化控制人工智能技术研究-人工智能论文-计算机论文.docx...