node加载模块

昨晚睡觉前纠结了一下,在考虑之后这个系列应该怎么写下去,我发现现在学习nodejs的难点就是官网没有提供一些简单的tutorial,只是提供了api的接口说明和一些示例代码,这让我们这些初学者在刚看的时候的确是丈二和尚摸不着头脑。所以我决定了今后的章节我会凭借我个人对于接口的理解进行比较合理的编排,来便于大家对于整个node的接口有一个系统的了解吧。 

首先我们先来说说modules机制。先说这个模块的API有两个原因吧,一个是这个模块我算是比较熟悉,另外一个原因是我觉得大家对这个模块了解清楚了的话,就可以自己去翻其他插件的源代码了。为什么我会对这个模块比较熟呢?其实我一直都很喜欢一个叫dojotoolkit的javascript框架,我也知道google曾经有一段时间在使用这套框架来构件他们的GWT这套mvc框架。dojo是最早提出了javascript模块化管理这个概念的,而其后这台机制在commonjs的模块接口中得以规范化。所以node的这一块如果我的猜测没错的话也是这样一路走过来的。 

简单示例 
那究竟node的模块管理是怎么实现的呢?首先我们先来看一个模块调用的示例: 
loadModuleExample.js 

Javascript代码   收藏代码
  1. //加载全局模块http  
  2. var http = require("http");  
  3.   
  4. //加载当前目录下的duowan.js模块里面的myname  
  5. var myname = require("./duowan").myname  
  6.   
  7. //加载绝对路径/node/modules下面的common.js模块  
  8. var common = require("/node/modules/common")  


上面的例子里我们说明了三个事情: 
  • 通过require(moduleName)我们可以加载其它的模块
  • 如果模块名不含有/或者或者./就会去加载通用模块,如果带有./或者../则按照相对路径规则从当前文件所在文件夹开始寻找模块,如果以/开头则从系统根目录开始找
  • 模块引入后可以把整个模块赋予到一个变量中,也可以直接暴露模块中的某个对象或者属性或者方法。其实这点和javascript一样。


模块文件类型  
node可以加载的文件类型有三种:.js,  .json, .node 
.js就是文本格式的javascript文件,而.json则是json格式对象,这两者加载的时候被视为javascript被动态编译和处理使用,而.node则是预编译好的插件模块,会在以后说plugin的时候说明。 

核心模块和文件模块  
需要补充说明一下的是惯于上面的第二点说法中关于全局模块的说明。这个说明其实不太正确。下面我们就按照官方API的说明来正确地解释一下当你的模块名里面不带有./或者../时会怎么去找包: 

首先,node会最优先加载核心模块。所谓的核心模块存在于node安装目录下的lib目录里面,他们的加载是在node容器初始化的时候就加载完成的,不需要在加载过程中进行编译,例如http这个模块。另外这些模块不会被覆盖,关于这点下面会有额外说明。 

如果加载的不是核心模块,则会从当前目录寻找node_module里面的同名模块;如果当前目录没有该模块,则往上一级目录寻找node_module中的同名模块;一直找到系统根目录的node_module中有同名模块为止,没有则会抛出一个MODULE_NOT_FOUND异常。例如有以下的目录结构: 

|_ node_module 
|          |_ common.js 
|_ app 
       |_ node_module 
       |           |_ http.js 
       |           |_ hello.js 
       |_ index.js 

如果index.js里面require("hello.js"),则加载的将会是/app/node_module/hello.js这个文件。如果加载的是require("common.js"),则会加载/node_module/common.js这个文件。但是如果require("http"),加载的将是核心模块http,而不是/app/node_module/http.js这个文件,因为核心模块不允许被重写。 

目录模块  
除了上述两种模块以外,node还允许你使用目录模块。 
例如有以下目录结构: 
/ app 
       |_ myModules 
       |           |_ index.js 
       |_ index.js 
如果我们在index.js里面引入require("./myModules"),则会加载/app/myModules/index.js。如果我们引入的是一个目录而不是一个文件,node会尝试加载这个目录下面的index.js或者index.node文件。 

但是如果你在目录下定一个叫package.json文件,那么你就可以改变这一默认模块加载行为。在这个文件里面可以保存一个json对象,而里面最关键的一个属性main可以让你定义这个目录下的哪个文件是模块的主文件。例如以下的目录结构中: 
/app 
       |_ myModules 
       |           |_ package.json 
       |           |_ myname.js 
       |           |_ index.js 
       |_ index.js 

其中packages.json定义如下: 
Javascript代码   收藏代码
  1. {main:"./myname.js"}  


如果我们在index.js中require("./myModules"),则会加载/app/myModules/myname.js,而不是/app/myModules/index.js 

当然这个package.json可不单单能够做这个事情,里面还能够定义很多像依赖管理之类的事情。由于这里是基础篇,我就先不在这里展开讨论了,等之后我们遇到相关的场景再作介绍吧。 

模块缓存  
对于node来说,你在主进程加载过一次的模块,就会被永久缓存起来。即使你修改过模块文件的内容,然后再在主进程尝试调用require进行重新加载,但是你会发现最后还是拿到了第一次加载的那个对象。只能通过重启node,或者通过删除保存在require.cache中的模块缓存对象,才能够重新加载模块内容。 

暴露模块属性  
如果我们引入一个模块,其实node就是简单地在当前环境调用某个模块文件并且解释一遍。例如下面的例子: 
文件名: hello.js  
Javascript代码   收藏代码
  1. console.log("Hello World!");  


文件名: test.js  
Javascript代码   收藏代码
  1. var hello = require("./hello");  
  2. console.log(hello);  


我们执行node test你会发现直接打印出Hello World!来。而第二行则会打印出一个空对象。 

但是如果我们希望暴露出模块中的一些方法或对象供外部调用者使用,那应该怎么做呢?很简单,就是利用给module.exports赋值。我们来修改一下我们的 hello.js: 
文件名: hello.js  
Javascript代码   收藏代码
  1. module.exports={  
  2.     "name":"Justice",  
  3.     "myName":function(){  
  4.         return this.name;  
  5.     }  
  6. }  


然后再次执行node test,我们可以看到输出了一个{ name: 'Justice', myName: [Function] }对象。 

值得注意的是exports的定义必须是在主进程里面完成,不可以延迟加载,即以下代码中,timeout部分无效: 
Javascript代码   收藏代码
  1. setTimeout(function(){  
  2.     module.exports = {"a":"Test"};  
  3. },50);  

原因我猜测是因为node只会在调用require那一刹那对脚本进行解释,但是setTimeout会在主线程完成工作以后才会调用,那个时候由于module已经被编译好并且缓存下来了,所以是无法生效的。 

最后提出一些小建议。大家想下如果我在一个模块中给module.exports赋值两遍会有什么后果呢?看看我们的hello.js的又一次改造版本: 
文件名: hello.js  
Javascript代码   收藏代码
  1. module.exports={  
  2.     "name":"Justice",  
  3.     "myName":function(){  
  4.         return this.name;  
  5.     }  
  6. }  
  7.   
  8. module.exports={"name":"Moc"}  

无容置疑,最后模块导出的就只有{"name":"Moc"}这个对象了。所以如果大家要调用module.exports,最好放到你的文件尾部来调用,以防止这种覆盖问题。 

module的其它API  
module的API还有以下的属性: 
module.id = 模块的ID,通常是当前模块文件路径,含文件名 
module.filename = 当前模块文件路径,含文件名 
module.loaded = 判断模块当前是否已加载 
module.parent = 加载当前脚本的模块对象。 
module.children = 当前模块加载的模块对象集合,是一个数组 

未完成的内容  
说过了这里作为基础篇,我就暂时不作进阶话题的讨论,这章里面还有以下的一些问题没有解决的,请同学们自己去思考一下,我以后有机会也会补充进来: 
  • 模块间的互相引用。例如a引用b, b又引用a会怎么办? node的官网文档中有这个问题的解决方案详述,http://www.nodejs.org/api/modules.html#modules_cycles
  • 闭包。如刚刚我们看到的一样,我们的hello.js里面的myName方法返回了this.name,这里马上就涉及到了javascript中一个坑爹的主题:闭包问题。究竟node的这套闭包机制是怎么回事呢?大家有兴趣的话可以自己做做试验。我发现node其实并不存在闭包这一说,它的this就是指exports的赋值对象。不过很危险的事情是你可以直接在外部修改这个exports对象,而且这些对exports对象的修改是可以互相影响的。这是一件很可怕的事情,所以我建议大家最好不要在外部对已加载模块的内容进行修改,否则出问题的时候就会很难调试了。
官方网址: http://www.nodejs.org/api/modules.html  
好了,今天就到此为止了,之后我要先考虑下下一个模块写什么比较好。多些各位收看。


Node.js中模块可以通过文件路径或名字获取模块的引用。模块的引用会映射到一个js文件路径,除非它是一个Node内置模块。Node的内置模块公开了一些常用的API给开发者,并且它们在Node进程开始的时候就预加载了。

 

其它的如通过NPM安装的第三方模块(third-party modules)或本地模块(local modules),每个模块都会暴露一个公开的API。以便开发者可以导入。如

1
var  mod = require( 'module_name' )

此句执行后,Node内部会载入内置模块或通过NPM安装的模块。require函数会返回一个对象,该对象公开的API可能是函数,对象,或者属性如函数,数组,甚至任意类型的JS对象。

 

这里列下node模块的载入及缓存机制

  1. 载入内置模块(A Core Module)
  2. 载入文件模块(A File Module)
  3. 载入文件目录模块(A Folder Module)
  4. 载入node_modules里的模块
  5. 自动缓存已载入模块

 

一、载入内置模块

Node的内置模块被编译为二进制形式,引用时直接使用名字而非文件路径。当第三方的模块和内置模块同名时,内置模块将覆盖第三方同名模块。因此命名时需要注意不要和内置模块同名。如获取一个http模块

1
var  http = require( 'http' )

返回的http即是实现了HTTP功能Node的内置模块。

 

二、载入文件模块

绝对路径的

1
var  myMod = require( '/home/base/my_mod' )

 

或相对路径的

1
var  myMod = require( './my_mod' )

 

注意,这里忽略了扩展名“.js”,以下是对等的

1
2
var  myMod = require( './my_mod' )
var  myMod = require( './my_mod.js' )

  

三、载入文件目录模块

可以直接require一个目录,假设有一个目录名为folder,如

1
var  myMod = require( './folder' )

此时,Node将搜索整个folder目录,Node会假设folder为一个包并试图找到包定义文件package.json。如果folder目录里没有包含package.json文件,Node会假设默认主文件为index.js,即会加载index.js。如果index.js也不存在,那么加载将失败。

 

假如目录结构如下

 

package.json定义如下

1
2
3
4
{
     "name" "pack" ,
     "main" "modA.js"
}

此时 require('./folder') 将返回模块modA.js。如果package.json不存在,那么将返回模块index.js。如果index.js也不存在,那么将发生载入异常。

 

四、载入node_modules里的模块

如果模块名不是路径,也不是内置模块,Node将试图去当前目录的node_modules文件夹里搜索。如果当前目录的node_modules里没有找到,Node会从父目录的node_modules里搜索,这样递归下去直到根目录。

不必担心,npm命令可让我们很方便的去安装,卸载,更新node_modules目录。

 

五、自动缓存已载入模块

对于已加载的模块Node会缓存下来,而不必每次都重新搜索。下面是一个示例

modA.js

1
2
3
4
5
console.log( '模块modA开始加载...' )
exports =  function () {
     console.log( 'Hi' )
}
console.log( '模块modA加载完毕' )

 

init.js

1
2
3
var  mod1 = require( './modA' )
var  mod2 = require( './modA' )
console.log(mod1 === mod2)

 

命令行执行:

node init.js 

 

输入如下

 

可以看到虽然require了两次,但modA.js仍然只执行了一次。mod1和mod2是相同的,即两个引用都指向了同一个模块对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值