Node.js模块加载详解


Posted in Javascript onAugust 16, 2014

JavaScript是世界上使用频率最高的编程语言之一,它是Web世界的通用语言,被所有浏览器所使用。JavaScript的诞生要追溯到Netscape那个时代,它的核心内容被仓促的开发出来,用以对抗Microsoft,参与当时白热化的浏览器大战。由于过早的发布,无可避免的造成了它的一些不太好的特性。

尽管它的开发时间很短,但是JavaScript依然具备了很多强大的特性,不过,每个脚本共享一个全局命名空间这个特性除外。

一旦Web页面加载了JavaScript代码,它就会被注入到全局命名空间,会和其他所有已加载的脚本公用同一个地址空间,这会导致很多安全问题,冲突,以及一些常见问题,让bug即难以跟踪又很难解决。

不过谢天谢地,Node为服务器端JavaScript定了一些规范,还实现了CommonJS的模块标准,在这个标准里,每个模块有自己的上下文,和其他模块相区分。这意味着,模块不会污染全局作用域,因为根本就没有所谓的全局作用域,模块之间也不会相互干扰。

本章,我们将了解几种不同的模块以及如何加载它们。

把代码拆分成一系列定义良好的模块可以帮你掌控你的应用程序,下面我们将学习如何创建和使用你自己的模块。

了解Node如何加载模块

Node里,可以通过文件路径来引用模块,也可以通过模块名引用,如果用名称引用非核心模块,Node最终会把模块名影射到对应的模块文件路径。而那些包含了核心函数的核心模块,会在Node启动时被预先加载。

非核心模块包括使用NPM(Node Package Manager)安装的第三方模块,以及你或你的同事创建的本地模块。

每个被当前脚本导入的模块都会向程序员暴露一组公开API,使用模块前,需要用require函数来导入它,像这样:

var module = require(‘module_name')

上面的代码会导入一个名为module_name的模块,它可能是个核心模块,也可以是用NPM安装的模块,require函数返回一个包含模块所有公共API的对象。随模块的不同,返回的对象可能是任何JavaScript值,可以是一个函数,也可以是个包含了一系列属性(函数,数组或者任何JavaScript对象)的对象。

导出模块

CommonJS模块系统是Node下文件间共享对象和函数的唯一方式。对于一个很复杂的程序,你应该把一些类,对象或者函数重构成一系列良好定义的可重用模块。对于模块使用者来说,模块仅对外暴露出那些你指定的代码。

在下面的例子里你将会了解到,在Node里文件和模块是一一对应的,我们创建了一个叫circle.js的文件,它仅对外导出了Circle构造函数。

function Circle(x, y, r) {
       function r_squared() {
              return Math.pow(r, 2);
       }
       function area() {
              return Math.PI * r_squared();
       }
       return {area: area};
}
module.exports = Circle;

代码里最重要的是最后一行,它定义了模块对外导出了什么内容。module是个特殊的变量,它代表当前模块自身,而module.exports是模块对外导出的对象,它可以是任何对象,在这个例子里,我们把Circle的构造函数导出了,这样模块使用者就可以用这个模块来创建Circle实例。

你也可以导出一些复杂的对象,module.exports被初始化成一个空对象,你把任何你想暴露给外界的内容,作为module.exports对象的属性来导出。比如,你设计了一个模块,它对外暴露了一组函数:

                  function printA() {
         console.log('A');
}
function printB() {
         console.log('B');
}
function printC() {
         console.log('C');
}
module.exports.printA = printA;
module.exports.printB = printB;
module.exports.pi = Math.PI;

这个模块导出了两个函数(printA和printB)和一个数字(pi),调用代码看起来像这样:

var myModule2 = require('./myModule2');
myModule2.printA(); // -> A
myModule2.printB(); // -> B
console.log(myModule2.pi); // -> 3.141592653589793

加载模块

前面提到过,你可以使用require函数来加载模块,不用担心在代码里调用require会影响全局命名空间,因为Node里就没有全局命名空间这个概念。如果模块存在且没有任何语法或初始化错误,require函数就会返回这个模块对象,你还可以这个对象赋值给任何一个局部变量。

模块有几种不同的类型,大概可以分为核心模块,本地模块和通过NPM安装的第三方模块,根据模块的类型,有几种引用模块的方式,下面我们就来了解下这些知识。

加载核心模块

Node有一些被编译到二进制文件里的模块,被称为核心模块,它们不能通过路径来引用,只能用模块名。核心模块拥有最高的加载优先级,即使已经有了一个同名的第三方模块,核心模块也会被优先加载。

比如,如果你想加载和使用http核心模块,可以这样做:

         var http = require('http');

这将返回一个包含了http模块对象,它包含了Node API文档里定义的那些htpp模块的API。

加载文件模块

你也可以使用绝对路径从文件系统里加载模块:

var myModule = require('/home/pedro/my_modules/my_module');

也可以用一个基于当前文件的相对路径:
var myModule = require('../my_modules/my_module');
var myModule2 = require('./lib/my_module_2');

注意上面的代码,你可以省略文件名的扩展名,如果Node找不到这个文件,会尝试在文件名后加上js后缀再次查找(译者注:其实除了js,还会查找json和node,具体可以看官网文档),因此,如果在当前目录下存在一个叫my_module.js的文件,会有下面两种加载方式:

var myModule = require('./my_module');
var myModule = require('./my_module.js');

加载目录模块

你还可以使用目录的路径来加载模块:

var myModule = require('./myModuleDir');

Node会假定这个目录是个模块包,并尝试在这个目录下搜索包定义文件package.json。

如果没找到,Node会假设包的入口点是index.js文件(译者注:除了index.js还会查找index.node,.node文件是Node的二进制扩展包,具体见官方文档),以上面代码为例,Node会尝试查找./myModuleDir/index.js文件。

反之,如果找到了package.json文件,Node会尝试解析它,并查找包定义里的main属性,然后把main属性的值当作入口点的相对路径。以本例来说,如果package.json定义如下:

                   {
                            "name" : "myModule",
                            "main" : "./lib/myModule.js"
                   }

Node就会尝试加载./myModuleDir/lib/myModule.js文件

从node_modules目录加载

如果require函数的参数不是相对路径,也不是核心模块名,Node会在当前目录的node_modules子目录下查找,比如下面的代码,Node会尝试查找文件./node_modules/myModule.js:

var myModule = require('myModule.js');

如果没找到,Node会继续在上级目录的node_modules文件夹下查找,如果还没找到就继续向上层目录查找,直到找到对应的模块或者到达根目录。

你可以使用这个特性来管理node_modules目录的内容或模块,不过最好还是把模块的管理任务交给NPM(见第一章),本地node_modules目录是NPM安装模块的默认位置,这个设计把Node和NPM关联在了一起。通常,作为开发人员不必太关心这个特性,你可以简单的使用NPM安装,更新和删除包,它会帮你维护node_modules目录

缓存模块

模块在第一次成功加载后会被缓存起来,就是说,如果模块名被解析到同一个文件路径,那么每次调用require(‘myModule')都确切地会返回同一个模块。

比如,有一个叫my_module.js的模块,包含下面的内容:

console.log('module my_module initializing...');
module.exports = function() {
         console.log('Hi!');
};
console.log('my_module initialized.');

然后用下面的代码加载这个模块:

var myModuleInstance1 = require('./my_module');

它会产生下面的输出:

module my_module initializing...
my_module initialized

如果我们两次导入它:

var myModuleInstance1 = require('./my_module');
var myModuleInstance2 = require('./my_module');

输出依然是:

module my_module initializing...
my_module initialized

也就是说,模块的初始化代码仅执行了一次。当你构建自己的模块时,如果模块的初始化代码里含有可能产生副作用的代码,一定要特别注意这个特性。

小结

Node取消了JavaScript的默认全局作用域,转而采用CommonJS模块系统,这样你可以更好的组织你的代码,也因此避免了很多安全问题和bug。可以使用require函数来加载核心模块,第三方模块,或从文件及目录加载你自己的模块

还可以用相对路径或者绝对路径来加载非核心模块,如果把模块放到了node_modules目录下或者对于用NPM安装的模块,你还可以直接使用模块名来加载。

 译者注:

建议读者把官方文档的模块章节阅读一遍,个人感觉比作者讲得更清晰明了,而且还附加了一个非常具有代表性的例子,对理解Node模块加载会很有很大帮助。下面把那个例子也引用过来:

用require(X) 加载路径Y下的模块
1. 如果X是核心模块,
   a. 加载并返回核心模块
   b. 结束
2. 如果X以 './' or '/' or '../ 开始'
   a. LOAD_AS_FILE(Y + X)
   b. LOAD_AS_DIRECTORY(Y + X)
3. LOAD_NODE_MODULES(X, dirname(Y))
4. 抛出异常:"not found"
LOAD_AS_FILE(X)
1. 如果X是个文件,把 X作为JavaScript 脚本加载,加载完毕后结束
2. 如果X.js是个文件,把X.js 作为JavaScript 脚本加载,加载完毕后结束
3. 如果X.node是个文件,把X.node 作为Node二进制插件加载,加载完毕后结束
LOAD_AS_DIRECTORY(X)
1. 如果 X/package.json文件存在,
   a. 解析X/package.json, 并查找 "main"字段.
   b. 另M = X + (main字段的值)
   c. LOAD_AS_FILE(M)
2. 如果X/index.js文件存在,把 X/index.js作为JavaScript 脚本加载,加载完毕后结束
3. 如果X/index.node文件存在,把load X/index.node作为Node二进制插件加载,加载完毕后结束
LOAD_NODE_MODULES(X, START)
1. 另DIRS=NODE_MODULES_PATHS(START)
2. 对DIRS下的每个目录DIR做如下操作:
   a. LOAD_AS_FILE(DIR/X)
   b. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. 另PARTS = path split(START)
2. 另ROOT = index of first instance of "node_modules" in PARTS, or 0
3. 另I = count of PARTS - 1
4. 另DIRS = []
5. while I > ROOT,
   a. 如果 PARTS[I] = "node_modules" 则继续后续操作,否则下次循环
   c. DIR = path join(PARTS[0 .. I] + "node_modules")
   b. DIRS = DIRS + DIR
   c. 另I = I - 1
6. 返回DIRS
Javascript 相关文章推荐
jQuery 全选效果实现代码
Mar 23 Javascript
jquery $.each 和for怎么跳出循环终止本次循环
Sep 27 Javascript
Javascript仿PHP $_GET获取URL中的参数
May 12 Javascript
从零学JSON之JSON数据结构
May 19 Javascript
详解JavaScript中常用的函数类型
Nov 18 Javascript
原生js实现淘宝购物车功能
Jun 23 Javascript
js实现密码强度检验
Jan 15 Javascript
微信小程序 天气预报开发实例代码源码
Jan 20 Javascript
JS中IP地址与整数相互转换的实现代码
Apr 10 Javascript
SVG动画vivus.js库使用小结(实例代码)
Sep 14 Javascript
vue中使用element ui的弹窗与echarts之间的问题详解
Oct 25 Javascript
利用vue3+ts实现管理后台(增删改查)
Oct 30 Javascript
JS遍历Json字符串中键值对先转成JSON对象再遍历
Aug 15 #Javascript
手机端网页点击链接触发自动拨打或保存电话的示例代码
Aug 15 #Javascript
Node.js中使用事件发射器模式实现事件绑定详解
Aug 15 #Javascript
Node.js中使用计时器定时执行函数详解
Aug 15 #Javascript
javascript中实现兼容JAVA的hashCode算法代码分享
Aug 11 #Javascript
javascript实现锁定网页、密码解锁效果(类似系统屏幕保护效果)
Aug 15 #Javascript
javascript使用window.open提示“已经计划系统关机”的原因
Aug 15 #Javascript
You might like
DedeCMS dede_channeltype表字段注释
2010/04/07 PHP
Joomla语言翻译类Jtext用法分析
2016/05/05 PHP
php微信公众号开发之现金红包
2018/04/16 PHP
PHP调用接口用post方法传送json数据的实例
2018/05/31 PHP
php输出形式实例整理
2020/05/05 PHP
jquery 获取json数据实现代码
2009/04/27 Javascript
基于jquery的高性能td和input切换并可修改内容实现代码
2011/01/09 Javascript
JS阻止用户多次提交示例代码
2014/03/26 Javascript
Javascript添加监听与删除监听用法详解
2014/12/19 Javascript
jQuery支持动态参数将函数绑定到事件上的方法
2015/03/17 Javascript
深入php面向对象、模式与实践
2016/02/16 Javascript
JS事件添加和移出的兼容写法示例
2016/06/20 Javascript
JS封装通过className获取元素的函数示例
2016/12/20 Javascript
使用BootStrap实现表格隔行变色及hover变色并在需要时出现滚动条
2017/01/04 Javascript
Angularjs处理页面闪烁的解决方法
2017/03/09 Javascript
js排序与重组的实例讲解
2017/08/28 Javascript
bootstrap select下拉搜索插件使用方法详解
2017/11/23 Javascript
PHP自动加载autoload和命名空间的应用小结
2017/12/01 Javascript
D3.js实现拓扑图的示例代码
2018/06/30 Javascript
jQuery中使用validate插件校验表单功能
2019/05/24 jQuery
JavaScript 继承 封装 多态实现及原理详解
2019/07/29 Javascript
[02:54]DOTA2英雄基础教程 暗影牧师戴泽
2013/12/05 DOTA
[01:08:44]NB vs VP 2018国际邀请赛小组赛BO2 第一场 8.18
2018/08/19 DOTA
[01:20:38]完美世界DOTA2联赛 GXR vs IO 第一场 11.07
2020/11/09 DOTA
Python抽象类的新写法
2015/06/18 Python
python使用Plotly绘图工具绘制气泡图
2019/04/01 Python
python实现随机漫步方法和原理
2019/06/10 Python
python递归调用中的坑:打印有值, 返回却None
2020/03/16 Python
python+selenium+Chrome options参数的使用
2020/03/18 Python
使用Keras加载含有自定义层或函数的模型操作
2020/06/10 Python
html5是什么_动力节点Java学院整理
2017/07/07 HTML / CSS
L’urv官网:精品女性运动服品牌
2019/07/07 全球购物
Shop Apotheke瑞士:您的健康与美容网上商店
2019/10/09 全球购物
JBL加拿大官方商店:扬声器、耳机等
2020/10/23 全球购物
如何使用flask将模型部署为服务
2021/05/13 Python
Pygame如何使用精灵和碰撞检测
2021/11/17 Python