学习NodeJS第四天:初始化nodejs的历险之旅(下)

包加载

首先声明,原本 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

动态运行JavaScript代码

通过前面的介绍,我们已经知道,利用 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 Compile(const Arguments& args) { HandleScope scope; if (args.Length() < 2) { return ThrowException(Exception::TypeError( String::New("needs two arguments."))); } Local source = args[0]->ToString(); Local filename = args[1]->ToString(); TryCatch try_catch; Local script = v8::Script::Compile(source, filename); if (try_catch.HasCaught()) { // Hack because I can't get a proper stacktrace on SyntaxError ReportException(try_catch, true); exit(1); } Local result = script->Run(); if (try_catch.HasCaught()) { ReportException(try_catch, false); exit(1); } return scope.Close(result); }

特殊的Script对象

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 用实例方法和静态方法之间的分野来区分是否延时执行的标识,大家可以看看是不是。

module 机制如何工作?

结束本天的学习之前,有必要再说说 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行)。

你可能感兴趣的:(Node.js)