NodeJS学习笔记之Module的简介


Posted in NodeJs onMarch 24, 2017

Node.js模块系统

Node.js有一个简单的模块加载系统。 在Node.js中,文件和模块是一一对应的(每个文件被视为单独的模块)。

例如,考虑下面这个名为 foo.js 的文件:

const circle = require('./circle.js');
console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

在第一行, foo.js 加载与 foo.js 同一目录的模块 circle.js 。

circle.js 的内容如下:

const PI = Math.PI;

exports.area = (r) => PI * r * r;

exports.circumference = (r) => 2* PI * r;

模块 circle.js 导出了函数 area() circumference() 。 要将函数和对象添加到模块的根目录,可以将它们赋值到特殊 exports 对象上。

模块内部的变量一定是私有的,因为模块被Node.js包裹在一个函数中(参见下面的模块包装器)。 在这个例子中,变量 PI 对于 circle.js 来说是私有变量。

如果你希望模块导出的是一个函数(如构造函数),或者是要导出完整的对象,而不是一次创建一个属性,则需要将其分配给 module.exports 而不是 exports 。

在下面的 bar.js 中,使用了 square 模块,它导出一个构造函数:

const square = require('./square.js');
var mySquare = square(2);
console.log(`The area of my square is ${mySquare.area()}`);

在 square.js 模块中定义一个 square 方法:

module.exports = (width) => {
  return {
    area: () => width * width;
  };
}

此外,模块系统在 require(“module”) 模块中实现。

『main』模块

当某个 module 直接从Node.js运行时,它会将 require.main 设置该 module 。 你可以通过这个来测试这个 module 是被直接运行的还是被 require 的。

require.main === module

就拿文件 foo.js 来说,如果运行 node foo.js 这个属性就是 true 。运行 require('./foo.js') 就是 false 。

因为 module 提供了一个 filename (通常相当于 __filename ),因此可以通过检查 require.main.filename 来获取当前应用程序的入口点。

包管理器的一些提示

Node.js的 require() 函数支持一些合理的目录结构。它让软件包管理器程序(如 dpkg , rpm 和 npm )可以从Node.js模块中直接去构建本地的包而不需要修改。

下面我们给出一个可以正常工作的建议目录结构:

假设我们希望在 /usr/lib/node/<some-package>/<some-version> 中的文件夹来指定版本的包。

此外,包还可以相互依赖。 比如你想安装 foo 包,而这个包有可能需要安装指定版本的 bar 包。而 bar 包也很有可能依赖其他的包,并且在某些特殊情况下,这些依赖包甚至可能会产生循环依赖。

由于Node.js会查找加载的所有模块的 realpath (即解析软链),然后再去node_modules文件夹中查找依赖的包,因此使用以下方案可以非常简单地解决此问题:

/usr/lib/node/foo/1.2.3/ - 包含 foo 包,版本是 1.2.3

/usr/lib/node/bar/4.3.2/ - 包含 foo 所依赖的 bar 包

/usr/lib/node/foo/1.2.3/node_modules/bar - 软链到 /usr/lib/node/bar/4.3.2/

/usr/lib/node/bar/4.3.2/node_modules/* - 软链到 bar 的依赖

因此,即使遇到循环依赖,或者是依赖冲突,每个模块都能加载到并使用自己所依赖指定版本的包。

当 foo 包中 require('bar') 时,它就可以软链到指定版本的 /usr/lib/node/foo/1.2.3/node_modules/bar 。然后,当 bar 包中的代码调用 require('quux') 时,它同样也可以软链到指定版本的 /usr/lib/node/bar/4.3.2/node_modules/quux 。

模块加载的全过程(重点,下面写的伪代码流程一定要记住)

要获取在调用 require() 将被加载的确切文件名,请使用 require.resolve() 函数。

以下是模块加载的全过程以及 require.resolve 的解析过程:

// 加载X模块
require(X) from module at path Y
1. If X is a core module.
  a. return the core module
  b. STOP
2. If X begins with './' or '/' or '../'
  a. LOAD_AS_FILE(Y + X)
  b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. THROW "not found"

// 加载X文件
// 加载过程:X -> X.js -> X.json -> X.node
LOAD_AS_FILE(X)
1. If [X] is a file, load [X] as JavaScript text. STOP
2. If [X.js] is a file, load [X.js] as JavaScript text. STOP
3. If [X.json] is a file, load [X.json] as JavaScript text. STOP
4. If [X.node] is a file, load [X.node] as JavaScript text. STOP

// 加载入口文件
// 加载过程:X -> X/index.js -> X/index.json -> X/index.node
LOAD_INDEX(X)
1. If [X/index.js] is a file, load [X/index.js] as JavaScript text. STOP
2. If [X/index.json] is a file, load [X/index.json] as JavaScript text. STOP
3. If [X/index.node] if a file, load [X/index.node] as JavaScript text. STOP

// 加载文件夹
LOAD_AS_DIRECTORY(X)
1. If [X/package.json] is a file.
  a. Parse [X/package.json], and look for "main" field
  b. let M = X + (json main field)
  c. LOAD_AS_FILE(M)
  d. LOAD_INDEX(M)
2. LOAD_INDEX(X)
 
// 加载node模块
LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS;
  a. LOAD_AS_FILE(DIR/X)
  b. LOAD_AS_DIRECTORY(DIR/X)

// 列出所有可能的node_modules路径
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START);
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I > 0
  a. If PARTS[I] = "node_modules" CONTINUE
  b. DIR = path join(PARTS[0 ... I] + "node_modules")
  c. DIRS = DIRS + DIR
  d. let I = I -1
5. return DIRS

模块缓存

所有的模块都会在第一次加载之后被缓存起来。 这意味着你每次调用 require('foo') 将得到完全相同的对象。

对 require('foo') 的多次调用可能并不会多次执行该模块的代码。 这是一个重要的功能。 使用它,可以返回“partially done”对象,从而允许根据依赖关系一层一层地加载模块,即使这样做可能会导致循环依赖。

如果要让某个模块在每次被加载时都去执行代码,则需要 exports 一个函数,并调用该函数即可。

模块缓存注意事项

模块是基于其解析出来的文件名进行缓存。根据调用模块的路径,被调用的模块可能会解析出不同的文件名(从node_modules文件夹加载)。如果解析出来的是不同的文件,它不保证每次 require('foo') 总是返回相同的对象。

另外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向相同的文件,但缓存仍将它们视为不同的模块,并将重新加载该文件多次。 例如, require('./ foo') 和 require('./ FOO') 返回两个不同的对象,而不管 ./foo 和 ./FOO 是否是同一个文件。

核心模块

Node.js有些模块被编译成二进制文件。 本文档中的其他部分将对这些模块进行更详细的描述。

核心模块在Node.js的源码 lib/ 文件夹中。

如果核心模块的模块标识传递给 require() ,则它们总是优先加载。 例如,即使有一个自定义模块叫 http ,我们去执行 require('http') 也将始终返回内置的 HTTP 模块,

循环引用

当循环引用 require() 时,返回模块可能并没有执行完成。

考虑这种情况:

a.js :

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js :

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

app.js :

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

当 app.js 加载 a.js 时, a.js 依次加载 b.js . 此时, b.js 尝试加载 a.js . 为了防止无限循环,将 a.js 导出对象的未完成副本返回到 b.js 模块。 b.js 然后完成加载,并将其导出对象提供给 a.js 模块。

当 app.js 加载了这两个模块时,它们都已经完成。 因此,该程序的输出将是:

$ node app.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
in main, a.done =true, b.done = true

模块包装器

在执行模块的代码之前,Node.js将使用一个函数包装器来将模块内容包裹起来,如下所示:

(function (exports, require, module, __filename, __dirname) {
  // 你的模块代码
});

通过这样做,Node.js实现了以下几点:

它将模块内部的顶级变量(定义为 var , const 或 let )的作用域范围限定为模块内部而不是全局。

它有助于给模块内部提供一些实际上只属于该模块的全局变量,例如:

module 和 exports 对象用来帮助从模块内部导出一些值

变量 __filename 和 __dirname 是当前模块最终解析出来的文件名和文件夹路径

module 对象签名

Object module {
  id: String, // 模块标识,为该模块文件在系统中的绝对路径
  exports: Object, // 该模块的导出对象
  parent: Object | undefined, // 引用该模块的父模块
  filename: String | null, // 最终解析的文件名称, 与__filename相同。
  loaded: Boolean, // 该模块是否已经加载
  children: Array, // 改模块的引用列表
  paths: Array // 模块加载路径
}

require 函数签名

Function require {
  [Function], // 函数体
  resolve: Function, // 根据模块标识解析模块,返回绝对路径
  main: undefined | Object, // 应用的主(main)模块
  extensions: {'.js':Function, '.json':Function, '.node':Function},
  cache: Object // 模块缓存,以模块的绝对路径为key
}
NodeJs 相关文章推荐
使用forever管理nodejs应用教程
Jun 03 NodeJs
Nodejs异步回调的优雅处理方法
Sep 25 NodeJs
基于Nodejs利用socket.io实现多人聊天室
Feb 22 NodeJs
详解NodeJS框架express的路径映射(路由)功能及控制
Mar 24 NodeJs
详解nodejs微信公众号开发——2.自动回复
Apr 10 NodeJs
NodeJs通过async/await处理异步的方法
Oct 09 NodeJs
NodeJS使用Range请求实现下载功能的方法示例
Oct 12 NodeJs
Nodejs异步流程框架async的方法
Jun 07 NodeJs
搭建一个nodejs脚手架的方法步骤
Jun 28 NodeJs
nodejs二进制与Buffer的介绍与使用
Jul 11 NodeJs
nodejs开发一个最简单的web服务器实例讲解
Jan 02 NodeJs
一文秒懂nodejs中的异步编程
Jan 28 NodeJs
详解nodejs中的process进程
Mar 19 #NodeJs
nodejs中使用HTTP分块响应和定时器示例代码
Mar 19 #NodeJs
nodejs中向HTTP响应传送进程的输出
Mar 19 #NodeJs
实例分析nodejs模块xml2js解析xml过程中遇到的坑
Mar 18 #NodeJs
nodejs中模块定义实例详解
Mar 18 #NodeJs
Nodejs基于LRU算法实现的缓存处理操作示例
Mar 17 #NodeJs
用nodeJS搭建本地文件服务器的几种方法小结
Mar 16 #NodeJs
You might like
ThinkPHP模板判断输出Present标签用法详解
2014/06/30 PHP
php好代码风格的阶段性总结
2016/06/25 PHP
php判断手机浏览还是web浏览,并执行相应的动作简单实例
2016/07/28 PHP
JMenuTab简单使用说明
2008/03/13 Javascript
过虑特殊字符输入的js代码
2010/08/05 Javascript
JS实现带有抽屉效果的产品类网站多级导航菜单代码
2015/09/15 Javascript
基于Jquery easyui 选中特定的tab
2015/11/17 Javascript
jQuery+formdata实现上传进度特效遇到的问题
2016/02/24 Javascript
第三章之Bootstrap 表格与按钮功能
2016/04/25 Javascript
使用bootstrapValidator插件进行动态添加表单元素并校验
2016/09/28 Javascript
使用JSON作为函数的参数的优缺点
2016/10/27 Javascript
js实现获取鼠标当前的位置
2016/12/14 Javascript
HTML页面定时跳转方法解析(2种任选)
2016/12/22 Javascript
Bootstrap进度条与AJAX后端数据传递结合使用实例详解
2017/04/23 Javascript
基于js中的原型(全面讲解)
2017/09/19 Javascript
angularJs利用$scope处理升降序的方法
2018/10/08 Javascript
video.js 一个页面同时播放多个视频的实例代码
2018/11/27 Javascript
详解小程序设置缓存并且不覆盖原有数据
2019/04/15 Javascript
ES6 class的应用实例分析
2019/06/27 Javascript
《javascript设计模式》学习笔记四:Javascript面向对象程序设计链式调用实例分析
2020/04/07 Javascript
JavaScript如何实现图片处理与合成
2020/05/29 Javascript
如何使用JavaScript实现无缝滚动自动播放轮播图效果
2020/08/20 Javascript
python paramiko实现ssh远程访问的方法
2013/12/03 Python
django使用LDAP验证的方法示例
2018/12/10 Python
对python借助百度云API对评论进行观点抽取的方法详解
2019/02/21 Python
关于Python3 类方法、静态方法新解
2019/08/30 Python
南非领先的在线旅行社:Travelstart南非
2016/09/04 全球购物
竞争性谈判邀请书
2014/02/06 职场文书
弘扬职业精神演讲稿
2014/03/20 职场文书
应用外语系自荐信
2014/06/26 职场文书
实施意见格式范本
2015/06/05 职场文书
标准演讲稿格式结尾应该怎么书写?
2019/07/17 职场文书
2019年消防宣传标语集锦
2019/11/21 职场文书
Python批量将csv文件转化成xml文件的实例
2021/05/10 Python
教你用python控制安卓手机
2021/05/13 Python
Python 中的单分派泛函数你真的了解吗
2021/06/22 Python