什么是Node.js
简单来说,Node.js是一个服务器端的javascript运行时环境,使得我们可以在服务器端运行javascript代码。
至于安装Node.js,Node.js官网有已编译的二进制文件,也有源文件包。我自己是下载源文件重新编译安装的。
Hello,World
方法一:直接执行无参数的node命令,进入node命令行界面,打印Hello, World。
方法二:执行node hello.js命令,调用写好的javacript脚本。
console.log('Hello, World');
一个基本的Web Server
这是nodejs.org的一个官方例子,使用Node.js建立一个web服务器。打开编辑器,建立一个名为app.js的文件,内容如下:
var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(1337, '127.0.0.1'); console.log('Server running at http://127.0.0.1:1337/');
然后运行node app.js命令
打开浏览器访问http://127.0.0.1:1337/
另外,注意到没,跟之前的hello.js脚本不一样,运行这个app.js脚本后,程序并没有自动退出,而是一直在等待http链接。这是因为在listen函数中创建了事件监听器。。。
Node.js的异步回调
我们写一个读取文件内容的脚本,来展示Node.js的异步回调机制。
var fs = require('fs'); fs.readFile('hello.js', 'utf-8', function(err, data) { if(err) { console.error(err); } else { console.log(data); } }); console.log('readfile end.');
看到输出没,按照传统思路,程序应该是先读取文件,打印文件内容,最后才打印readfile end。而Node.js的I/O默认是异步式的,当程序需要I/O操作时候,并不阻塞来等待I/O完成,而是将I/O请求交给系统,然后接着运行后续任务,系统执行完I/O操作后会事件的形式通知该程序,触发程序中定义的回调函数。如上面代码中的function(err, data) {…}。
Node.js也有定义阻塞式读取文件的API:
var fs = require('fs'); var data = fs.readFileSync('hello.js', 'utf-8'); console.log(data); console.log('readfile end.');
javascript的面向对象
学习Node.js的基础是要学好javascript。以前一直觉得javascript只要会调用jQuery以及各种开源插件就行了,从来没去研究过他们的实现方法,对javacript的高级用法也从来没兴趣研究过。但到了Node.js这里,感觉像duang~的一下,被摔一脸,简直门外汉。先来这里恶补一下javascript比较反人类的面向对象概念。
javascript并不是常规意义上的面向对象,因为它没有class,但却又有对象的概念。像C++这样的面向对象语言,对象是基于类(class)的,一个对象就是类的一个实例。而javascript的对象是基于原型(prototype)的,同一个构造函数创建的对象,都保留一个对原型对象prototype的引用。
原型继承函数util.inherits(subConstructor, baseConstructor)
var util = require('util'); function BaseClass() { this.name = 'base'; this.value = 2015; this.sayHi = function() { console.log('hi.'); }; } BaseClass.prototype.sayHello = function() { console.log('hello.'); } function SubClass() { this.name = 'sub'; } SubClass.prototype.talk1 = function() { console.log('------'); } var s1 = new SubClass(); util.inherits(SubClass, BaseClass); SubClass.prototype.talk2 = function() { console.log('++++++'); } var s2 = new SubClass(); s1.talk1(); //s1是用旧的SubClass构造的,所以有talk1方法 //s1.talk2(); //s1是用旧的SubClass构造的,所以没有talk2方法 //s1.sayHello(); //s1是用旧的SubClass构造的,所以没有sayHello方法 //s2.talk1(); //s2是用新的SubClass构造的,所以没有talk1方法 s2.talk2(); //s2是用新的SubClass构造的,所以有talk2方法 s2.sayHello(); //s2是用新的SubClass构造的,继承了BaseClass.sayHello方法 //s2.sayHi(); //util.inherits只继承父亲的prototype,不继承父构造函数内定义的属性与方法 BaseClass.prototype.sayWa = function() { console.log('wa!'); } s2.sayWa(); //BaseClass的prototype在s2的原型链上,所以s2可以获得sayWa方法
刚定义完BaseClass与SubClass以及s1对象时候他们内存空间如下:
调用原型继承函数util.inherits(SubClass, BaseClass),并定义了SubClass.prototype.talk2()方法后的内存空间:
由上图可以看出,util.inherits(SubClass, BaseClass)后,SubClass.prototype.__proto__由之前的基本Object对象变成了BaseClass.prototype,并且为SubClass定义了SubClass.super_属性,该属性指向BaseClass函数。
另外有个很重要的一点,在inherits(SubClass, BaseClass)之前定义的s1对象,它的原型仍然是旧的SubClass,而不是新的继承了BaseClass.prototype的SubClass。原因是因为javascript的对象是引用类型的。s1.__proto__指向对旧的SubClass.prototype的引用,而当inherits继承后,SubClass实际上是新建了一个prototype对象,旧的prototype则游离在内存中,仍被s1引用。而后所有新建的SubClass对象则是引用新的SubClass.prototype。并且新建的s2对象内部构成一个原型链:s2.__proto__(=SubClass.prototype).__proto__(=BaseClass.prototype).__proto__(=Object)。因此s2可以调用到SubClass.prototype上定义的属性和BaseClass.prototype上定义的属性。
下面是util.inherits的源码:
exports.inherits = function(ctor, superCtor) { ctor.super_ = superCtor; // 新建属性ctor.super_ 等于superCtor ctor.prototype = Object.create(superCtor.prototype, { constructor: { value: ctor, enumerable: false, writable: true, configurable: true } }); // 新建一个对象,该对象有__proto__与constructor两个属性,并将该对象赋值给ctor.prototype // 其中,该对象的__proto__=superCotr.prototype // 该对象的constructor=ctor,并且constructor不可枚举(enumerable),可以被修改(writable)和删除(configurable) };
util.inherits只是继承了“父类”的原型,并没有继承“父类”构造函数中定义的本地属性。这是因为当我们将函数作为构造函数调用时候,并不会追溯到其“父类”的构造函数中,这点可以参考javascript的面向对象概念这篇博文。那如果我想C++中的那样,构造“子类”对象时候会先调用“父类”构造函数,应该怎么办?这里我们参考一下对Node.js EventEmitter的继承,EventEmitter是Node.js中处理事件、回调的核心模块,很多对象(包括程序员自定义的对象)都要继承EventEmitter。
var util = require('util'); var EventEmitter = require('events').EventEmitter; var Connector = function() { EventEmitter.call(this); //主动调用EventEmitter构造函数,并将当前对象传入(即使用EventEmitter构造函数初始化this当前对象) this.xxx = xxx; //接下来定义Connector自己的本地属性 } util.inherits(Connector, EventEmitter); //原型继承EventEmitter.prototype Connector.prototype.start = function() { ... }
这里用到了javascript Function的call方法,类似的还有apply, bind方法,具体用法自己百度。
exports与require
Node.js中,一个文件就是一个模块,在文件中用exports标记模块的接口,然后在外部使用require加载该模块。我们做一个简单的例子
function sayHi() { console.log('hi, this is my module.'); }
var myModule = require('./myModule.js'); myModule.sayHi();
运行node testModule.js,结果出错,myModule对象没有sayHi方法,因为在myModule.js中未将sayHi方法标记为模块的接口。正确的写法应该是:
exports.sayHi = function() { console.log('hi, this is my module.'); }
接下来再来看一下exports与module.exports的区别:
exports.sayHi = function() { console.log('hi, this is sayHi method.'); } module.exports.name = 'tom'; exports.name = 'kate'; // 会覆盖上面的module.exprots.name module.exports.sayHi = function() { console.log('hi, this is sayHi method in module.'); } // 会覆盖上面的exprots.sayHi
var myModule = require('./myModule.js'); myModule.sayHi(); console.log(myModule.name);
运行node testModule.js输出的是:
在这个例子可以看出,exports与module.exprots在导出成员变量与成员函数时作用是一样的。
我们可以看一下此时的myModule变量是什么东西:(截图来自WebStorm,一款javascript IDE,很方便我们调试javacript脚本,支持Node.js)
myModule是一个对象,它有一个name成员属性,一个sayHi成员函数,以及一个__proto__原型对象。
再来看一下,如果我们直接给module.exports赋值,修改myModule.js文件为如下:
module.exports = 'hello world'; // 直接导出该字符串,其他exports.xxx以及module.exports.xxx都将无效 exports.sayHi = function() { console.log('hi, this is sayHi method.'); } module.exports.name = 'tom';
这时我们在运行node testModule.js就会出错,提示TypeError: Object hello world has no method ‘sayHi’,我们看一下此时的myModule变量在内存中是什么样:
可见,如果将module.exprots直接赋值为数值、字符串、数组、函数,那么在外部require该模块,得到的就会是相应类型的变量。而exports直接赋值的话,则不会被导出。这是因为require最终返回的其实是module.exports而不是exports,在模块中定义的所有exports.xxx都会追加到module.exports上。
如果我们希望require模块后得到的是一个对象,该对象有成员属性与成员函数作为接口,那就用exports.xxx =。(当然也可以用module.exports.xxx =,但这样略显繁琐一点)
如果我们希望reqiure模块后得到的是一个特定类型,比如说数值、字符串、数组、函数,那么就必须要使用module.exprots =。
一个模块作为一个“类”
在上面我们看到可以用module.exprots = function的方法导出函数,那么如果我们把这个函数作为构造函数呢,那这个模块不就相当于一个类的定义了嘛。看下面这个例子(看懂下面这个代码的前提是要了解javascript的面向对象):
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.talk = function() { console.log('hi, my name is %s, i\'m %d.', this.name, this.age); } module.exports = Person;
var Person = require('./Person.js'); // 此时Person是一个函数 var bill = new Person('bill', 21); // 将Person用作构造函数来调用,创建bill对象 var kate = new Person('kate', 18); // 将Person用作构造函数来调用,创建kate对象 kate.talk(); // 输出 hi, my name is kate, i'm 18. bill.talk(); // 输出 hi, my name is bill, i'm 21.
构造完bill与kate两个Person对象后,他们的内容是这样的:
什么是npm
npm是javascript软件包管理工具,它协助Node.js管理第三方javacript模块。在安装Node.js时候,默认会为我们装上npm。就像CentOS上的yum install xxx命令一样,使用npm install xxx命令就能下载第三方javascript模块。
ps:由于国内网络的原因,npm下载有时候会超慢,我们可以将npm的registry修改为国内淘宝镜像。
测试一下评论