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 相关文章推荐
用nodejs写的一个简单项目打包工具
May 11 NodeJs
跟我学Nodejs(二)--- Node.js事件模块
May 21 NodeJs
Nodejs实现的一个静态服务器实例
Dec 06 NodeJs
nodejs实现获取当前url地址及url各种参数值
Jun 25 NodeJs
Nodejs全局安装和本地安装的不同之处
Jul 04 NodeJs
nodejs利用http模块实现银行卡所属银行查询和骚扰电话验证示例
Dec 30 NodeJs
Highcharts+NodeJS搭建数据可视化平台示例
Jan 01 NodeJs
详解nodejs微信公众号开发——1.接入微信公众号
Apr 10 NodeJs
原生nodejs使用websocket代码分享
Apr 07 NodeJs
nodejs中express入门和基础知识点学习
Sep 13 NodeJs
通过实例了解Nodejs模块系统及require机制
Jul 16 NodeJs
nodejs处理tcp连接的核心流程
Feb 26 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
php is_file()和is_dir()用于遍历目录时用法注意事项
2010/03/02 PHP
PHP操作文件类的函数代码(文件和文件夹创建,复制,移动和删除)
2011/11/10 PHP
根据判断浏览器类型屏幕分辨率自动调用不同CSS的代码
2007/02/22 Javascript
锋利的jQuery 要点归纳(三) jQuery中的事件和动画(上:事件篇)
2010/03/24 Javascript
再论Javascript下字符串连接的性能
2011/03/05 Javascript
jquery maxlength使用说明
2011/09/09 Javascript
javascript工具库代码
2012/03/29 Javascript
jQuery性能优化28条建议你值得借鉴
2013/02/16 Javascript
如何使用jQUery获取选中radio对应的值(一句代码)
2013/06/03 Javascript
JavaScript获取按钮所在form表单id的方法
2015/04/02 Javascript
javascript实现点击按钮弹出一个可关闭层窗口同时网页背景变灰的方法
2015/05/13 Javascript
在jQuery中使用$而避免跟其它库产生冲突的方法
2015/08/13 Javascript
js文本框走动跑马灯效果代码分享
2015/08/25 Javascript
基于zepto的移动端轻量级日期插件--date_picker
2016/03/04 Javascript
jQuery 获取屏幕高度、宽度的简单实现案例
2016/05/17 Javascript
JavaScript作用域示例详解
2016/07/07 Javascript
JavaScript将DOM事件处理程序封装为event.js 出现的低级错误问题
2016/08/03 Javascript
JS使用面向对象技术实现的tab选项卡效果示例
2017/02/28 Javascript
js实现登录框鼠标拖拽效果
2017/03/09 Javascript
Vue项目中设置背景图片方法
2018/02/21 Javascript
Easyui 关闭jquery-easui tab标签页前触发事件的解决方法
2019/04/28 jQuery
小程序实现录音功能
2020/09/22 Javascript
浅谈vue.watch的触发条件是什么
2020/11/07 Javascript
[06:35]2014DOTA2国际邀请赛 老男孩梦圆西雅图中国军团世界最强
2014/07/22 DOTA
[39:07]LGD vs VP 2018国际邀请赛淘汰赛BO3 第二场 8.21
2018/08/22 DOTA
跟老齐学Python之集成开发环境(IDE)
2014/09/12 Python
Python中import机制详解
2017/11/14 Python
python测试mysql写入性能完整实例
2018/01/18 Python
10个Python小技巧你值得拥有
2018/09/29 Python
英国浴室洗脸盆购物网站:Click Basin
2018/06/08 全球购物
大学自主招生推荐信
2014/05/10 职场文书
教师党员批评与自我批评
2014/10/15 职场文书
社区活动总结
2015/02/04 职场文书
2015年乡镇工会工作总结
2015/05/19 职场文书
解决Laravel使用验证时跳转到首页的问题
2021/11/17 PHP
python保存图片的四个常用方法
2022/02/28 Python