Node.js入门

Node.js入门

什么是Node.js

简单来说,Node.js是一个服务器端的javascript运行时环境,使得我们可以在服务器端运行javascript代码。

至于安装Node.js,Node.js官网有已编译的二进制文件,也有源文件包。我自己是下载源文件重新编译安装的。

Hello,World

方法一:直接执行无参数的node命令,进入node命令行界面,打印Hello, World。

屏幕快照 2015-04-12 下午2.38.20

方法二:执行node hello.js命令,调用写好的javacript脚本。

console.log('Hello, World');

屏幕快照 2015-04-12 下午2.39.06

一个基本的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命令

屏幕快照 2015-04-12 下午3.15.33

打开浏览器访问http://127.0.0.1:1337/

屏幕快照 2015-04-12 下午3.18.18

另外,注意到没,跟之前的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.');

屏幕快照 2015-04-12 下午7.37.19

看到输出没,按照传统思路,程序应该是先读取文件,打印文件内容,最后才打印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.');

屏幕快照 2015-04-12 下午7.50.32

 

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对象时候他们内存空间如下:

屏幕快照 2015-04-23 下午11.43.36 屏幕快照 2015-04-23 下午11.43.57屏幕快照 2015-04-23 下午11.45.53

调用原型继承函数util.inherits(SubClass, BaseClass),并定义了SubClass.prototype.talk2()方法后的内存空间:

屏幕快照 2015-04-23 下午11.48.47屏幕快照 2015-04-23 下午11.55.09屏幕快照 2015-04-23 下午11.53.40

由上图可以看出,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输出的是:

屏幕快照 2015-04-14 下午8.52.19

在这个例子可以看出,exports与module.exprots在导出成员变量与成员函数时作用是一样的。

我们可以看一下此时的myModule变量是什么东西:(截图来自WebStorm,一款javascript IDE,很方便我们调试javacript脚本,支持Node.js)

屏幕快照 2015-04-14 下午8.56.15

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变量在内存中是什么样:

屏幕快照 2015-04-14 下午9.42.52myModule是一个字符串。

可见,如果将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对象后,他们的内容是这样的:

屏幕快照 2015-04-14 下午10.34.57 屏幕快照 2015-04-14 下午10.35.15

 

什么是npm

npm是javascript软件包管理工具,它协助Node.js管理第三方javacript模块。在安装Node.js时候,默认会为我们装上npm。就像CentOS上的yum install xxx命令一样,使用npm install xxx命令就能下载第三方javascript模块。

ps:由于国内网络的原因,npm下载有时候会超慢,我们可以将npm的registry修改为国内淘宝镜像。

参考资料

One thought on “Node.js入门

Leave a Reply to 王富贵 Cancel reply

Your email address will not be published. Required fields are marked *

TOC