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服务器(9):实现非阻塞操作
Dec 18 NodeJs
NodeJS学习笔记之FS文件模块
Jan 13 NodeJs
NodeJS学习笔记之(Url,QueryString,Path)模块
Jan 13 NodeJs
nodeJS删除文件方法示例
Dec 25 NodeJs
详解nodeJS中读写文件方法的区别
Mar 06 NodeJs
nodejs入门教程二:创建一个简单应用示例
Apr 24 NodeJs
nodejs实现大文件(在线视频)的读取
Oct 16 NodeJs
详解从NodeJS搭建中间层再谈前后端分离
Nov 13 NodeJs
Nodejs异步流程框架async的方法
Jun 07 NodeJs
监控Nodejs的性能实例代码
Jul 02 NodeJs
通过实例了解Nodejs模块系统及require机制
Jul 16 NodeJs
NodeJS配置CORS实现过程详解
Dec 02 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
CakePHP去除默认显示的标题及图标的方法
2008/10/22 PHP
typecho插件编写教程(一):Hello World
2015/05/28 PHP
PHP Static延迟静态绑定用法分析
2016/03/16 PHP
PHP入门教程之PHP操作MySQL的方法分析
2016/09/11 PHP
php 生成Tab键或逗号分隔的CSV
2016/09/24 PHP
PHP get_html_translation_table()函数用法讲解
2019/02/16 PHP
PHP pthreads v3在centos7平台下的安装与配置操作方法
2020/02/21 PHP
用js实现随机返回数组的一个元素
2007/08/13 Javascript
jquery获取自定义属性(attr和prop)实例介绍
2013/04/21 Javascript
js不能获取隐藏的div的宽度只能先显示后获取
2014/09/04 Javascript
JavaScript lastIndexOf方法入门实例(计算指定字符在字符串中最后一次出现的位置)
2014/10/17 Javascript
AngularJS模块管理问题的非常规处理方法
2015/04/29 Javascript
Markdown+Bootstrap图片自适应属性详解
2016/05/21 Javascript
Node.js的Koa框架上手及MySQL操作指南
2016/06/13 Javascript
Javascript中document.referrer隐藏来源的方法
2017/01/16 Javascript
BootStrap table删除指定行的注意事项(笔记整理)
2017/02/05 Javascript
vue axios用法教程详解
2017/07/23 Javascript
nodejs socket服务端和客户端简单通信功能
2017/09/14 NodeJs
jQuery实现动态显示select下拉列表数据的方法
2018/02/05 jQuery
vue实现a标签点击高亮方法
2018/03/17 Javascript
详解vue中点击空白处隐藏div的实现(用指令实现)
2018/04/19 Javascript
js+canvas实现纸牌游戏
2020/03/16 Javascript
Webpack5正式发布,有哪些新特性
2020/10/12 Javascript
Python算法之求n个节点不同二叉树个数
2017/10/27 Python
用python介绍4种常用的单链表翻转的方法小结
2020/02/24 Python
Windows 平台做 Python 开发的最佳组合(推荐)
2020/07/27 Python
Vs Code中8个好用的python 扩展插件
2020/10/12 Python
分享unittest单元测试框架中几种常用的用例加载方法
2020/12/02 Python
HTML5+JS实现俄罗斯方块原理及具体步骤
2013/11/29 HTML / CSS
Linux内核产生并发的原因
2016/11/08 面试题
J2EE系统只能是基于web
2015/09/08 面试题
群众路线问题查摆对照检查材料
2014/10/04 职场文书
家长反馈意见及建议
2015/06/03 职场文书
30岁前绝不能错过的10本书
2019/08/08 职场文书
Vue3.0中Ref与Reactive的区别示例详析
2021/07/07 Vue.js
详解MySQL的主键查询为什么这么快
2022/04/03 MySQL