首先声明,原本 JS 是没有模块库机制(module)的,这必然为创建实质项目带来很大的麻烦,毕竟,我们的目标,还是要创造一个系统的、科学现代的、规范合理的控制各逻辑代码的边界,否则如果是一种落后的管理机制那就是一块明显的短板。nodejs 本身没有发明一种“新的”模块管理方式,而是直接继承自志愿者组织的 CommonJS 规范作为模块管理的规范,所以开发 nodejs 的开发者都应遵守该规范。Nodejs 本身就有几个已编译到进程之中的模块。
要使用这些定义好的模块,一般的做法是调用 require(包名称的字符串),分到某个局部变量。我们经常使用的sys.puts(“foo bar”)方法其定义在“sys的命名空间”,sys 即是 nodejs 内建自带的一个系统包。虽然如此,如果我们要使用到sys.puts方法,还必须在 js 源码顶部声明引用 sys 模块:
var sys = require('sys'); sys.puts(“Hello world!”); // 也可以这样用: require('sys').print(“Hello world!”); var md = require(__dirname + '/ext/markdown'); // __dirname 为全局变量,表示所在的目录
引用后,可以在当前上下文语句中访问 sys 之内的任何公开成员(对象、方法或属性)。当然 sys 提供 sys.puts()/* 带换行的conslose输出*/、sys.log(); /* 不带换行的conslose输出 */、sys.debug();/*与打点debugger类似*/、sys.inspect ();/*列出对象信息*// 这几个方法(上一期的学习调试时也曾有讲)。控制台输出对象 conslose 离不开全局对象 process.stdout。process.stdout 是 nodejs 内置对象,不需要用加载包来获取,那是一个经由V8暴露出来的底层对象。进入核心源码 node.js 发现,调用代码语句process.binding('stdio') 还不能直接使用,nodejs 在 js 多一层封装。通过下面的两种内部用法可以更真实地接触控制台的输出:
process.stdout.write(sys.inspect(object) + '/n'); process.binding('stdio').writeError(x + "/n");
与process.stdout (在第611行起)相对应的就是 process.stdoin,定义 I/O 流一出一进,在代码634行。
CommonJS 所规范的 require 方法使用相当简单(到底让人感觉多少有 java import 语句/ C# using 语句的影子),需要哪个就引入哪个包,某个包之中也可以在引用别的包。要加载一个自定义的包一点也不困难。假定一个自定义包用于计算圆周和面积,如下circle.js:
var PI = 3.14; exports.area = function (r) { return PI * r * r; }; exports.circumference = function (r) { return 2 * PI * r; };
如何引用这个包?将 circle.js 放置在与 node.exe 同一个目录下,就可以用 require('./circle') 在你的代码引入这个包。执行 nodejs 程序如下:
var circle = require('./circle'); console.log( 'The area of a circle of radius 4 is '+ circle.area(4));
注意此时前缀“./”不能省略,否则被视为内置的 nodejs 包。下面则是一个多层目录的例子。
var circle = require('./edk/base/core');
如果位于一个目录,要加入 js 文件的数量很多可怎么办,能不能以整个目录下的所有文件去批量加入?是的,nodejs 考虑这点并支持的。为此,nodejs 利用一个全局数组require.paths 维护着搜索源文件的路径,使得用户可以加入新的目录。因为这是一个标准的JavaScript数组,所以用操控 JS 数组的方法即可。
require.paths.unshift('/usr/local/node'); console.log(require.paths); // /usr/local/node,/Users/mjr/.node_libraries
通过前面的介绍,我们已经知道,利用 require() 可以加载磁盘的任意一份 Javascript 源文件到 nodejs 运行时 Runtime 之中。甚至,连下面的方式也是允许的:
// 精简加载 if(miniCoreOnly){ require('./edk/server/core'); }else if(loadFull){ // 完全加载 require('./edk/server/full'); }
尽管这样子——无论实际上还是理论上都被支持,但是过头了,显得怪怪的,是不是太 hack 了啊? 呵呵,既然那样子不好,就应该使用更正规的做法,我们要写规范的代码!
若希望动态执行 JavaScript 代码,按需加载,nodejs 可让我们选择的途径有几种,prcess进程对象身上的complie()便是其中的一种。process.compile(code, filename) 和我们熟知 JS 的动态执行的 eval() 函数非常相似,相似的就是动态执行 JS 代码。虽然动态语言中 eval 的“恶名”昭著,其中成份有好的也有不好的,但不管怎么样 nodejs 还是考虑到了,而且进而提供比 eval 更强大的封装。回到 process.compile() 与 eval 的比较上,两者也有着明显的不同,区别在于 eval() 是在当前作用域运行的,与当前作用域有关,你可修改涉及当前作用域上的一切成员,拥有这种的权限,eval() 的代码和当前的代码都是“透明的”,但是——process.compile() 执行的时候,就对当前作用域是隔绝的,非透明的,换句话说,在 process.compile() 运行的代码它是对当前作用域的看不见的(cannot see the local scope)。我们接着可以通过下面的这个给出例子看到它们之间的区别哪里:
// 文档中对比eval()和process.compile()的例子 var localVar = 123, compiled, evaled; compiled = process.compile('localVar = 1;', 'myfile.js'); console.log('localVar: ' + localVar + ', compiled: ' + compiled); evaled = eval('localVar = 1;'); console.log('localVar: ' + localVar + ', evaled: ' + evaled); // localVar: 123, compiled: 1 // localVar: 1, evaled: 1
由于 process.compile() 没有访问本地的作用域空间,所以 localVar 没有发生变化,若执行 eval() 的时候可访问本身上下文的作用域空间,故所以使得 lcoalVar 改变了。如果process.compile 中的运行代码错误,process.compile 则退出当前 node 结束了该独立过程,不影响本地代码运行。
怎么去理解这个本地作用域(local scope)呢?要理解“本地作用域”这点概念,最好引入一个相对于“本地”的这么一个“异地”的参照物,换句话说,相当于在新加入代码“外层”加多一层壳 function(){...},产生一个“异地”的 scope,scope chain 不就被改了嘛。最后这个 function(){...} 返回后就是 process.compile 返回的结果。
但是必须强调一点,还是在同一个全局空间中,所以,上面的测试代码,实际是 Script.runInThisContext('localVar = 1;', 'myfile.js'); 建立了全局变量 localVar 影响了本地的 var localVar 才会改变 var localVar 为1,覆盖掉了。
另外的一个参数 filename,它是可选参数,用于输出异常或者错误的信息。
前面说过,process.* 很多的方法都源自于 node 对 v8 的扩展,简单说仅仅调用 C++方法而已;按 C++ 源码迹象亦表明是调用了 V8 原生的方法。摘录 process.complie() 其C++ 的源码如下(位于 lib/node.cc):
Handle
Script “脚本”这一特定的词汇居然在 Nodejs 中称作是“类”的一种,那么这个 Script 类将有什么作用呢?用途还是在于运行 JavaScript 代码。我们可以使用 binding('evals') 绑定evals 模块中 Script,首先用 var Script = process.binding('evals').Script; 把它的类引用调出来。
首先是 Script.runInThisContext(code, [filename]) 方法,参数 code 是要执行的 JavaScript 实际代码,filename 是可选参数,用于输出异常或者错误的信息。从文档给出的代码看,与 process.compile() 的结果一样,依旧不会访问本地作用域,然全局空间仍开放着的——之所以叫做 “runInThisContext” 还是基本符合原意的。
下面是它们对比的例子:
var localVar = 123, usingscript, evaled, Script = process.binding('evals').Script; usingscript = Script.runInThisContext('localVar = 1;', 'myfile.js'); onsole.log('localVar: ' + localVar + ', usingscript: ' + usingscript); evaled = eval('localVar = 1;'); console.log('localVar: ' + localVar + ', evaled: ' + evaled); // localVar: 123, usingscript: 1 // localVar: 1, evaled: 1
但是比起 process.complie() 或 Script.runInThisContext(),Script.runInNewContext() 就严格多了,引入代码与正在运行的代码之间则不能通过全局空间来相会影响,不开放全局空间。因为 Script.runInNewContext() 会生成一个全新的 JS 运行空间,与本地代码完全隔绝。这种隔绝的程度,几乎可以用沙箱(sandbox)描述之。沙箱本来是安全方面话题的概念,这不,在 nodejs 中使用 Script.runInThisContext() 的话,代码之间的安全性是最高的。人为的限制一下还是有必要的。
不过,是不是两种代码之间就不能相互访问,老死不相往来呢?也没那么绝对的,不过是绕一点,折中去做。让程序员或有企图的代码不能轻易跨越这种障碍。Script.runInThisContext(code, sandboxObj) 可通过其第二个参数 sandboxObj,这是一个对象类型的参数。对于动态代码而言,这个 New Context 就是动态代码全局对象。我们可以透过 sandboxObj 就可达到两个运行空间之间交互数据之目的。
用法:
var sys = require('sys'), Script = process.binding('evals').Script, scriptObj, i, sandbox = { animal: 'cat', count: 2 }; scriptObj = new Script( 'count += 1; name = "kitty"', 'myfile.js'); for (i = 0; i < 10 ; i += 1) { scriptObj.runInNewContext(sandbox); } console.log(sys.inspect(sandbox)); // { animal: 'cat', count: 12, name: 'kitty' }
顺便说说,像 sandbox 的东东记得在 Adobe AJAX AIR里也有过,也借助某个对象互通往来——总让人感觉有点异曲同工!
Sciript 对象本小节最后要说说的是Script类本身了。实例化 Script 可得到也是叫做 runInThisContext() 和 runInNewContext() 一模一样的方法,但由于是实例方法(注意是小写的 script.runInThisContext(),按文档上的提示……),必须实例化 Script 之后才能使用,所以其定位也是有所区别。区别在于不是立刻执行 JS 代码,runInThisContext() 和runInNewContext() 调用的时候才真正调用 JS,也就是分开来加载 JS 的运行 JS 两个步骤,至于其他什么 Context 的不同就跟前面的一样了,如出一撤,只是在于是否延时的不同,参照一下前面的大家 reuse 思维即可,补充一句的就是 nodejs 用实例方法和静态方法之间的分野来区分是否延时执行的标识,大家可以看看是不是。
结束本天的学习之前,有必要再说说 nodeje 包是怎么的一个种“包”。module 的功能代码从 node.js 的第65行开始起。实际上 nodejs 会为 require 中所有的代码设置“顶层”的上下文空间。打开node.js源码观察,只要环境变量 NODE_MODULE_CONTEXTS=1 便可以初始化顶层上下文空间(contextLoad = true)。这样的结果,导致定义包中的 var成员即为私有成员,而通过 exports 或 this 对象身上绑定的就是暴露公共成员。如上例 circle,用户通过在 exports 上定义了两个方法 area() 和 circumference(),允许外界可以访问这两个方法。一般静态方式加入源码大概是这样,应该还有上述的 Script 动态加载方式。不管哪一种方式,在运行时 Runtime 执行阶段,全体*.js会透过Script.runInThisContext() 或 process.compile(code, filename) 进入到V8解析器(第421行开始),原理上大致如此。
有一个影响性能的问题,就是多次声明的包引用会不会产生重复加载?——应该不会。因为 nodejs 有缓存的机制,遇到重复的包不需要重新加载,使用缓存中的即可。JavaScript 中使用所谓的缓存 cache 说开了是非常简单、非常朴素、一贯以之的概念,相当于一个 hash 结构,key/value 命中匹配的 target。好像 nodejs 中的 var internalModuleCache = {} 与 var extensionCache = {} 分别代表着内部模块的 cache 和外部模板的 cache。这两对象一般都不会被GC回收。
通盘考虑模块的机制比较复杂,但的确其占了 node.js 很大的一个篇幅。如果对 require() 原理有兴趣的朋友可以看看 src/node.js 源码Module部分(最主要看 module 类的 _compile() 部分,第390行)。