JavaScript简明教程之Node.js

Node.js是目前很是火热的技术,可是它的诞生经历却很奇特。javascript

众所周知,在Netscape设计出JavaScript后的短短几个月,JavaScript事实上已是前端开发的惟一标准。css

后来,微软经过IE击败了Netscape后一统桌面,结果几年时间,浏览器毫无进步。(2001年推出的古老的IE 6到今天仍然有人在使用!)html

没有竞争就没有发展。微软认为IE6浏览器已经很是完善,几乎没有可改进之处,而后解散了IE6开发团队!而Google却认为支持现代Web应用的新一代浏览器才刚刚起步,尤为是浏览器负责运行JavaScript的引擎性能还可提高10倍。前端

先是Mozilla借助已壮烈牺牲的Netscape遗产在2002年推出了Firefox浏览器,紧接着Apple于2003年在开源的KHTML浏览器的基础上推出了WebKit内核的Safari浏览器,不过仅限于Mac平台。java

随后,Google也开始建立自家的浏览器。他们也看中了WebKit内核,因而基于WebKit内核推出了Chrome浏览器。node

Chrome浏览器是跨Windows和Mac平台的,而且,Google认为要运行现代Web应用,浏览器必须有一个性能很是强劲的JavaScript引擎,因而Google本身开发了一个高性能JavaScript引擎,名字叫V8,以BSD许可证开源。jquery

现代浏览器大战让微软的IE浏览器远远地落后了,由于他们解散了最有经验、战斗力最强的浏览器团队!回过头再追赶却发现,支持HTML5的WebKit已经成为手机端的标准了,IE浏览器今后与主流移动端设备绝缘。web

浏览器大战和Node有何关系?ajax

话说有个叫Ryan Dahl的歪果仁,他的工做是用C/C++写高性能Web服务。对于高性能,异步IO、事件驱动是基本原则,可是用C/C++写就太痛苦了。因而这位仁兄开始设想用高级语言开发Web服务。他评估了不少种高级语言,发现不少语言虽然同时提供了同步IO和异步IO,可是开发人员一旦用了同步IO,他们就再也懒得写异步IO了,因此,最终,Ryan瞄向了JavaScript。算法

由于JavaScript是单线程执行,根本不能进行同步IO操做,因此,JavaScript的这一“缺陷”致使了它只能使用异步IO。

选定了开发语言,还要有运行时引擎。这位仁兄曾考虑过本身写一个,不过明智地放弃了,由于V8就是开源的JavaScript引擎。让Google投资去优化V8,咱只负责改造一下拿来用,还不用付钱,这个买卖很划算。

因而在2009年,Ryan正式推出了基于JavaScript语言和V8引擎的开源Web服务器项目,命名为Node.js。虽然名字很土,可是,Node第一次把JavaScript带入到后端服务器开发,加上世界上已经有无数的JavaScript开发人员,因此Node一会儿就火了起来。

在Node上运行的JavaScript相比其余后端开发语言有何优点?

最大的优点是借助JavaScript天生的事件驱动机制加V8高性能引擎,使编写高性能Web服务垂手可得。

其次,JavaScript语言自己是完善的函数式语言,在前端开发时,开发人员每每写得比较随意,让人感受JavaScript就是个“玩具语言”。可是,在Node环境下,经过模块化的JavaScript代码,加上函数式编程,而且无需考虑浏览器兼容性问题,直接使用最新的ECMAScript 6标准,能够彻底知足工程上的需求。

我还据说过io.js,这又是什么鬼?

由于Node.js是开源项目,虽然由社区推进,但幕后一直由Joyent公司资助。因为一群开发者对Joyent公司的策略不满,于2014年从Node.js项目fork出了io.js项目,决定单独发展,但二者其实是兼容的。

然而中国有句古话,叫作“分久必合,合久必分”。分家后没多久,Joyent公司表示要和解,因而,io.js项目又决定回归Node.js。

具体作法是未来io.js将首先添加新的特性,若是你们测试用得爽,就把新特性加入Node.js。io.js是“尝鲜版”,而Node.js是线上稳定版,至关于Fedora Linux和RHEL的关系。

安装Node.js和npm

因为Node.js平台是在后端运行JavaScript代码,因此,必须首先在本机安装Node环境。

安装Node.js

目前Node.js的最新版本是6.2.x。首先,从Node.js官网下载对应平台的安装程序,网速慢的童鞋请移步国内镜像

在Windows上安装时务必选择所有组件,包括勾选Add to Path

安装完成后,在Windows环境下,请打开命令提示符,而后输入node -v,若是安装正常,你应该看到v6.2.0这样的输出:

C:\Users\IEUser>node -vv6.2.0

继续在命令提示符输入node,此刻你将进入Node.js的交互环境。在交互环境下,你能够输入任意JavaScript语句,例如100+200,回车后将获得输出结果。

要退出Node.js环境,连按两次Ctrl+C

在Mac或Linux环境下,请打开终端,而后输入node -v,你应该看到以下输出:

$ node -vv6.2.0

若是版本号不是v6.2.x,说明Node.js版本不对,后面章节的代码不保证能正常运行,请从新安装最新版本。

npm

在正式开始Node.js学习以前,咱们先认识一下npm

npm是什么东东?npm实际上是Node.js的包管理工具(package manager)。

为啥咱们须要一个包管理工具呢?由于咱们在Node.js上开发时,会用到不少别人写的JavaScript代码。若是咱们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,很是繁琐。因而一个集中管理的工具应运而生:你们都把本身开发的模块打包后放到npm官网上,若是要使用,直接经过npm安装就能够直接用,不用管代码存在哪,应该从哪下载。

更重要的是,若是咱们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块X和模块Y,npm能够根据依赖关系,把全部依赖的包都下载下来并管理起来。不然,靠咱们本身手动管理,确定既麻烦又容易出错。

讲了这么多,npm究竟在哪?

其实npm已经在Node.js安装的时候顺带装好了。咱们在命令提示符或者终端输入npm -v,应该看到相似的输出:

C:\>npm -v3.8.9

若是直接输入npm,你会看到相似下面的输出:

C:\> npmUsage: npm <command>where <command> is one of: ...

上面的一大堆文字告诉你,npm须要跟上命令。如今咱们不用关心这些命令,后面会一一讲到。目前,你只须要确保npm正确安装了,能运行就行。

第一个Node程序

在前面的全部章节中,咱们编写的JavaScript代码都是在浏览器中运行的,所以,咱们能够直接在浏览器中敲代码,而后直接运行。

从本章开始,咱们编写的JavaScript代码将不能在浏览器环境中执行了,而是在Node环境中执行,所以,JavaScript代码将直接在你的计算机上以命令行的方式运行,因此,咱们要先选择一个文本编辑器来编写JavaScript代码,而且把它保存到本地硬盘的某个目录,才可以执行。

那么问题来了:文本编辑器到底哪家强?

推荐两款文本编辑器:

一个是Sublime Text,无偿使用,可是不付费会弹出提示框:


hello.js

一个是Notepad++,无偿使用,有中文界面:


notepad-hello.js

请注意,用哪一个都行,可是绝对不能用Word和写字板,Windows自带的记事本也强烈不推荐使用。Word和写字板保存的不是纯文本文件,而记事本会自做聪明地在文件开始的地方加上几个特殊字符(UTF-8 BOM),结果常常会致使程序运行出现莫名其妙的错误。

安装好文本编辑器后,输入如下代码:

'use strict'; console.log('Hello, world.');

第一行老是写上'use strict';

是由于咱们老是以严格模式运行JavaScript代码,避免各类潜在陷阱。而后,选择一个目录,例如C:\Workspace,把文件保存为hello.js,就能够打开命令行窗口,把当前目录切换到hello.js所在目录,而后输入如下命令运行这个程序了:

C:\Workspace>node hello.js Hello, world.

也能够保存为别的名字,好比first.js,可是必需要以.js结尾。此外,文件名只能是英文字母、数字和下划线的组合。

若是当前目录下没有hello.js这个文件,运行node hello.js就会报错:

C:\Workspace>node hello.jsmodule.js:338 throw err; ^Error: Cannot find module 'C:\Workspace\hello.js' at Function.Module._resolveFilename at Function.Module._load at Function.Module.runMain at startup at node.js

报错的意思就是,没有找到hello.js这个文件,由于文件不存在。这个时候,就要检查一下当前目录下是否有这个文件了。

命令行模式和Node交互模式

请注意区分命令行模式和Node交互模式。看到相似C:\>是在Windows提供的命令行模式:


run-node-hello

在命令行模式下,能够执行node进入Node交互式环境,也能够执行node hello.js运行一个.js文件。看到>是在Node交互式环境下:


node-interactive-env

在Node交互式环境下,咱们能够输入JavaScript代码并马上执行。此外,在命令行模式运行.js文件和在Node交互式环境下直接运行JavaScript代码有所不一样。Node交互式环境会把每一行JavaScript代码的结果自动打印出来,可是,直接运行JavaScript文件却不会。

例如,在Node交互式环境下,输入:

> 100 + 200 + 300; 600

直接能够看到结果600。

可是,写一个calc.js的文件,内容以下:

100 + 200 + 300;

而后在命令行模式下执行:

C:\Workspace>node calc.js

发现什么输出都没有。这是正常的。想要输出结果,必须本身用console.log()打印出来。把calc.js改造一下:

console.log(100 + 200 + 300);

再执行,就能够看到结果:

C:\Workspace>node calc.js 600

用文本编辑器写JavaScript程序,而后保存为后缀为.js的文件,就能够用node直接运行这个程序了。

Node的交互模式和直接运行.js文件有什么区别呢?

直接输入node进入交互模式,至关于启动了Node解释器,可是等待你一行一行地输入源代码,每输入一行就执行一行。

直接运行node hello.js文件至关于启动了Node解释器,而后一次性把hello.js文件的源代码给执行了,你是没有机会以交互的方式输入源代码的。

在编写JavaScript代码的时候,彻底能够一边在文本编辑器里写代码,一边开一个Node交互式命令窗口,在写代码的过程当中,把部分代码粘到命令行去验证,事半功倍!前提是得有个27'的超大显示器!

模块

在计算机程序的开发过程当中,随着程序代码越写越多,在一个文件里代码就会愈来愈长,愈来愈不容易维护。

为了编写可维护的代码,咱们把不少函数分组,分别放到不一样的文件里,这样,每一个文件包含的代码就相对较少,不少编程语言都采用这种组织代码的方式。在Node环境中,一个.js文件就称之为一个模块(module)。

使用模块有什么好处?

最大的好处是大大提升了代码的可维护性。其次,编写代码没必要从零开始。当一个模块编写完毕,就能够被其余地方引用。咱们在编写程序的时候,也常常引用其余模块,包括Node内置的模块和来自第三方的模块。

使用模块还能够避免函数名和变量名冲突。相同名字的函数和变量彻底能够分别存在不一样的模块中,所以,咱们本身在编写模块时,没必要考虑名字会与其余模块冲突。

在上一节,咱们编写了一个hello.js文件,这个hello.js文件就是一个模块,模块的名字就是文件名(去掉.js后缀),因此hello.js文件就是名为hello的模块。

咱们把hello.js改造一下,建立一个函数,这样咱们就能够在其余地方调用这个函数:

'use strict'; var s = 'Hello'; function greet(name) { console.log(s + ', ' + name + '!'); } module.exports = greet;

函数greet()是咱们在hello模块中定义的,你可能注意到最后一行是一个奇怪的赋值语句,它的意思是,把函数greet做为模块的输出暴露出去,这样其余模块就可使用greet函数了。

问题是其余模块怎么使用hello模块的这个greet函数呢?咱们再编写一个main.js文件,调用hello模块的greet函数:

'use strict'; // 引入hello模块: var greet = require('./hello'); var s = 'Michael'; greet(s); // Hello, Michael!

注意到引入hello模块用Node提供的require函数:

var greet = require('./hello');

引入的模块做为变量保存在greet变量中,那greet变量究竟是什么东西?其实变量greet就是在hello.js中咱们用module.exports = greet;输出的greet函数。因此,main.js就成功地引用了hello.js模块中定义的greet()函数,接下来就能够直接使用它了。

在使用require()引入模块的时候,请注意模块的相对路径。由于main.js
hello.js位于同一个目录,因此咱们用了当前目录.:

var greet = require('./hello'); // 不要忘了写相对目录!

若是只写模块名:

var greet = require('hello');

则Node会依次在内置模块、全局模块和当前模块下查找hello.js,你极可能会获得一个错误:

module.js throw err; ^Error: Cannot find module 'hello' at Function.Module._resolveFilename at Function.Module._load ... at Function.Module._load at Function.Module.runMain

遇到这个错误,你要检查:

  • 模块名是否写对了
  • 模块文件是否存在
  • 相对路径是否写对了

CommonJS规范

这种模块加载机制被称为CommonJS规范。在这个规范下,每一个.js文件都是一个模块,它们内部各自使用的变量名和函数名都互不冲突,例如,hello.js
main.js都申明了全局变量var s = 'xxx',但互不影响。

一个模块想要对外暴露变量(函数也是变量),能够用module.exports = variable;,一个模块要引用其余模块暴露的变量,用

var ref = require('module_name');

就拿到了引用模块的变量。

要在模块中对外输出变量,用:module.exports = variable;

输出的变量能够是任意对象、函数、数组等等。要引入其余模块输出的对象,用:var foo = require('other_module');

引入的对象具体是什么,取决于引入模块输出的对象。

深刻了解模块原理

若是你想详细地了解CommonJS的模块实现原理,请继续往下阅读

当咱们编写JavaScript代码时,咱们能够申明全局变量:

var s = 'global';

在浏览器中,大量使用全局变量可很差。若是你在a.js中使用了全局变量s,那么,在b.js中也使用全局变量s,将形成冲突,b.js中对s赋值会改变a.js的运行逻辑。

也就是说,JavaScript语言自己并无一种模块机制来保证不一样模块可使用相同的变量名。

那Node.js是如何实现这一点的?

其实要实现“模块”这个功能,并不须要语法层面的支持。Node.js也并不会增长任何JavaScript语法。实现“模块”功能的奥妙就在于JavaScript是一种函数式编程语言,它支持闭包。若是咱们把一段JavaScript代码用一个函数包装起来,这段代码的全部“全局”变量就变成了函数内部的局部变量。

请注意咱们编写的hello.js代码是这样的:

var s = 'Hello'; var name = 'world'; console.log(s + ' ' + name + '!');

Node.js加载了hello.js后,它能够把代码包装一下,变成这样执行:

(function () { // 读取的hello.js代码: var s = 'Hello'; var name = 'world'; console.log(s + ' ' + name + '!'); // hello.js代码结束 })();

这样一来,原来的全局变量s如今变成了匿名函数内部的局部变量。若是Node.js继续加载其余模块,这些模块中定义的“全局”变量s也互不干扰。

因此,Node利用JavaScript的函数式编程的特性,垂手可得地实现了模块的隔离。

可是,模块的输出module.exports怎么实现?

这个也很容易实现,Node能够先准备一个对象module:

// 准备module对象: var module = { id: 'hello', exports: {} }; var load = function (module) { // 读取的hello.js代码: function greet(name) { console.log('Hello, ' + name + '!'); } module.exports = greet; // hello.js代码结束 return module.exports; }; var exported = load(module); // 保存module: save(module, exported);

可见,变量module是Node在加载js文件前准备的一个变量,并将其传入加载函数,咱们在hello.js中能够直接使用变量module缘由就在于它其实是函数的一个参数:module.exports = greet;

经过把参数module传递给load()函数,hello.js就顺利地把一个变量传递给了Node执行环境,Node会把module变量保存到某个地方。

因为Node保存了全部导入的module,当咱们用require()获取module时,Node找到对应的module,把这个moduleexports变量返回,这样,另外一个模块就顺利拿到了模块的输出:

var greet = require('./hello');

以上是Node实现JavaScript模块的一个简单的原理介绍。

module.exports vs exports

不少时候,你会看到,在Node环境中,有两种方法能够在一个模块中输出变量:

方法一:对module.exports赋值:

// hello.js function hello() { console.log('Hello, world!'); } function greet(name) { console.log('Hello, ' + name + '!'); } function hello() { console.log('Hello, world!'); } module.exports = { hello: hello, greet: greet};

方法二:直接使用exports:

// hello.js function hello() { console.log('Hello, world!'); } function greet(name) { console.log('Hello, ' + name + '!'); } function hello() { console.log('Hello, world!'); } exports.hello = hello; exports.greet = greet;

可是你不能够直接对exports赋值:

// 代码能够执行,可是模块并无输出任何变量: exports = { hello: hello, greet: greet };

若是你对上面的写法感到十分困惑,不要着急,咱们来分析Node的加载机制:

首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:

var module = { id: 'hello', exports: {} };

load()函数最终返回module.exports

var load = function (exports, module) { // hello.js的文件内容 ... // load函数返回: return module.exports; }; var exported = load(module.exports, module);

也就是说,默认状况下,Node准备的exports变量和module.exports变量其实是同一个变量,而且初始化为空对象{},因而,咱们能够写:

exports.foo = function () { return 'foo'; }; exports.bar = function () { return 'bar'; };

也能够写:

module.exports.foo = function () { return 'foo'; }; module.exports.bar = function () { return 'bar'; };

换句话说,Node默认给你准备了一个空对象{},这样你能够直接往里面加东西。

可是,若是咱们要输出的是一个函数或数组,那么,只能给module.exports
赋值:

module.exports = function () { return 'foo'; };

exports赋值是无效的,由于赋值后,module.exports仍然是空对象{}

结论:
若是要输出一个键值对象{},能够利用exports这个已存在的空对象{},并继续在上面添加新的键值;

若是要输出一个函数或数组,必须直接对module.exports对象赋值。

因此咱们能够得出结论:直接对module.exports赋值,能够应对任何状况:

module.exports = { foo: function () { return 'foo'; } };

或者:

module.exports = function () { return 'foo'; };

最终,咱们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只须要记忆一种方法。

基本模块

由于Node.js是运行在服务区端的JavaScript环境,服务器程序和浏览器程序相比,最大的特色是没有浏览器的安全限制了,并且,服务器程序必须能接收网络请求,读写文件,处理二进制内容,因此,Node.js内置的经常使用模块就是为了实现基本的服务器功能。这些模块在浏览器环境中是没法被执行的,由于它们的底层代码是用C/C++在Node.js运行环境中实现的。

global

在前面的JavaScript课程中,咱们已经知道,JavaScript有且仅有一个全局对象,在浏览器中,叫window对象。而在Node.js环境中,也有惟一的全局对象,但不叫window,而叫global,这个对象的属性和方法也和浏览器环境的window不一样。进入Node.js交互环境,能够直接输入:

> global.console Console { log: [Function: bound ], info: [Function: bound ], warn: [Function: bound ], error: [Function: bound ], dir: [Function: bound ], time: [Function: bound ], timeEnd: [Function: bound ], trace: [Function: bound trace], assert: [Function: bound ], Console: [Function: Console] }

process

process也是Node.js提供的一个对象,它表明当前Node.js进程。经过process对象能够拿到许多有用信息:

> process === global.process; true > process.version; 'v5.2.0' > process.platform; 'darwin' > process.arch; 'x64' > process.cwd(); //返回当前工做目录 '/Users/michael' > process.chdir('/private/tmp'); // 切换当前工做目录 undefined > process.cwd(); '/private/tmp'

JavaScript程序是由事件驱动执行的单线程模型,Node.js也不例外。Node.js不断执行响应事件的JavaScript函数,直到没有任何响应事件的函数能够执行时,Node.js就退出了。

若是咱们想要在下一次事件响应中执行代码,能够调用process.nextTick()

// test.js // process.nextTick()将在下一轮事件循环中调用: process.nextTick(function () { console.log('nextTick callback!'); }); console.log('nextTick was set!');

用Node执行上面的代码node test.js,你会看到,打印输出是:

nextTick was set!
nextTick callback!

这说明传入process.nextTick()的函数不是马上执行,而是要等到下一次事件循环。

Node.js进程自己的事件就由process对象来处理。若是咱们响应exit事件,就能够在程序即将退出时执行某个回调函数:

// 程序即将退出时的回调函数: process.on('exit', function (code) { console.log('about to exit with code: ' + code); });

判断JavaScript执行环境

有不少JavaScript代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序自己须要判断本身究竟是在什么环境下执行的,经常使用的方式就是根据浏览器和Node环境提供的全局变量名称来判断:

if (typeof(window) === 'undefined') { console.log('node.js'); } else { console.log('browser'); }

后面,咱们将介绍Node.js的经常使用内置模块。

fs

Node.js内置的fs模块就是文件系统模块,负责读写文件。

和全部其它JavaScript模块不一样的是,fs模块同时提供了异步和同步的方法。

回顾一下什么是异步方法。由于JavaScript的单线程模型,执行IO操做时,JavaScript代码无需等待,而是传入回调函数后,继续执行后续JavaScript代码。好比jQuery提供的getJSON()操做:

$.getJSON('http://example.com/ajax', function (data) { console.log('IO结果返回后执行...'); }); console.log('不等待IO结果直接执行后续代码...');

而同步的IO操做则须要等待函数返回:

// 根据网络耗时,函数将执行几十毫秒~几秒不等: var data = getJSONSync('http://example.com/ajax');

同步操做的好处是代码简单,缺点是程序将等待IO操做,在等待时间内,没法响应其它任何事件。而异步读取不用等待IO操做,但代码较麻烦。

异步读文件

按照JavaScript的标准,异步读取一个文本文件的代码以下:

'use strict'; var fs = require('fs'); fs.readFile('sample.txt', 'utf-8', function (err, data) { if (err) { console.log(err); } else { console.log(data); } });

请注意,sample.txt文件必须在当前目录下,且文件编码为utf-8。

异步读取时,传入的回调函数接收两个参数,当正常读取时,err参数为nulldata参数为读取到的String。当读取发生错误时,err参数表明一个错误对象,dataundefined

这也是Node.js标准的回调函数:第一个参数表明错误信息,第二个参数表明结果。后面咱们还会常常编写这种回调函数。

因为err是否为null就是判断是否出错的标志,因此一般的判断逻辑老是:

if (err) { // 出错了 } else { // 正常 }

若是咱们要读取的文件不是文本文件,而是二进制文件,怎么办?

下面的例子演示了如何读取一个图片文件:

'use strict'; var fs = require('fs'); fs.readFile('sample.png', function (err, data) { if (err) { console.log(err); } else { console.log(data); console.log(data.length + ' bytes'); } });

当读取二进制文件时,不传入文件编码时,回调函数的data参数将返回一个Buffer对象。在Node.js中,Buffer对象就是一个包含零个或任意个字节的数组(注意和Array不一样)。

Buffer对象能够和String做转换,例如,把一个Buffer对象转换成String:

// Buffer -> String var text = data.toString('utf-8'); console.log(text);

或者把一个String转换成Buffer

// String -> Buffer var buf = new Buffer(text, 'utf-8'); console.log(buf);

同步读文件

除了标准的异步读取模式外,fs也提供相应的同步读取函数。同步读取的函数和异步函数相比,多了一个Sync后缀,而且不接收回调函数,函数直接返回结果。

fs模块同步读取一个文本文件的代码以下:

'use strict'; var fs = require('fs'); var data = fs.readFileSync('sample.txt', 'utf-8'); console.log(data);

可见,原异步调用的回调函数的data被函数直接返回,函数名须要改成readFileSync,其它参数不变。

若是同步读取文件发生错误,则须要用try...catch捕获该错误:

try { var data = fs.readFileSync('sample.txt', 'utf-8'); console.log(data); } catch (err) { // 出错了 }

写文件

将数据写入文件是经过fs.writeFile()实现的:

'use strict'; var fs = require('fs'); var data = 'Hello, Node.js'; fs.writeFile('output.txt', data, function (err) { if (err) { console.log(err); } else { console.log('ok.'); } });

writeFile()的参数依次为文件名、数据和回调函数。若是传入的数据是String,默认按UTF-8编码写入文本文件,若是传入的参数是Buffer,则写入的是二进制文件。回调函数因为只关心成功与否,所以只须要一个err参数。

readFile()相似,writeFile()也有一个同步方法,叫writeFileSync()

'use strict'; var fs = require('fs'); var data = 'Hello, Node.js'; fs.writeFileSync('output.txt', data);

stat

若是咱们要获取文件大小,建立时间等信息,可使用fs.stat(),它返回一个Stat对象,能告诉咱们文件或目录的详细信息:

'use strict'; var fs = require('fs'); fs.stat('sample.txt', function (err, stat) { if (err) { console.log(err); } else { // 是不是文件: console.log('isFile: ' + stat.isFile()); // 是不是目录: console.log('isDirectory: ' + stat.isDirectory()); if (stat.isFile()) { // 文件大小: console.log('size: ' + stat.size); // 建立时间, Date对象: console.log('birth time: ' + stat.birthtime); // 修改时间, Date对象: console.log('modified time: ' + stat.mtime); } } });

运行结果以下:

isFile: true isDirectory: false size: 181 birth time: Fri Dec 11 2015 09:43:41 GMT+0800 (CST) modified time: Fri Dec 11 2015 12:09:00 GMT+0800 (CST)

stat()也有一个对应的同步函数statSync(),请试着改写上述异步代码为同步代码。

异步仍是同步

fs模块中,提供同步方法是为了方便使用。那咱们究竟是应该用异步方法仍是同步方法呢?

因为Node环境执行的JavaScript代码是服务器端代码,因此,绝大部分须要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码,不然,同步代码在执行时期,服务器将中止响应,由于JavaScript只有一个执行线程。

服务器启动时若是须要读取配置文件,或者结束时须要写入到状态文件时,可使用同步代码,由于这些代码只在启动和结束时执行一次,不影响服务器正常运行时的异步执行。

stream

stream是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。

什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就能够从某个地方(例如自来水厂)源源不断地到达另外一个地方(好比你家的洗手池)。

咱们也能够把数据当作是数据流,好比你敲键盘的时候,就能够把每一个字符依次连起来,当作字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流(stdin)。

若是应用程序把字符一个一个输出到显示器上,这也能够当作是一个流,这个流也有名字:标准输出流(stdout)。流的特色是数据是有序的,并且必须依次读取,或者依次写入,不能像Array那样随机定位。


nodejs-stream

有些流用来读取数据,好比从文件读取数据时,能够打开一个文件流,而后从文件流中不断地读取数据。有些流用来写入数据,好比向文件写入数据时,只须要把数据不断地往文件流中写进去就能够了。

在Node.js中,流也是一个对象,咱们只须要响应流的事件就能够了:data事件表示流的数据已经能够读取了,end事件表示这个流已经到末尾了,没有数据能够读取了,error事件表示出错了。

下面是一个从文件流读取文本内容的示例:

'use strict'; var fs = require('fs'); // 打开一个流: var rs = fs.createReadStream('sample.txt', 'utf-8'); rs.on('data', function (chunk) { console.log('DATA:') console.log(chunk); }); rs.on('end', function () { console.log('END'); }); rs.on('error', function (err) { console.log('ERROR: ' + err); });

要注意,data事件可能会有屡次,每次传递的chunk是流的一部分数据。

要以流的形式写入文件,只须要不断调用write()方法,最后以end()结束:

'use strict'; var fs = require('fs'); var ws1 = fs.createWriteStream('output1.txt', 'utf-8'); ws1.write('使用Stream写入文本数据...\n'); ws1.write('END.'); ws1.end(); var ws2 = fs.createWriteStream('output2.txt'); ws2.write(new Buffer('使用Stream写入二进制数据...\n', 'utf-8')); ws2.write(new Buffer('END.', 'utf-8')); ws2.end();

全部能够读取数据的流都继承自stream.Readable,全部能够写入的流都继承自stream.Writable

pipe

就像能够把两个水管串成一个更长的水管同样,两个流也能够串起来。一个Readable流和一个Writable流串起来后,全部的数据自动从Readable
流进入Writable流,这种操做叫pipe

在Node.js中,Readable流有一个pipe()方法,就是用来干这件事的。

让咱们用pipe()把一个文件流和另外一个文件流串起来,这样源文件的全部数据就自动写入到目标文件里了,因此,这其实是一个复制文件的程序:

'use strict'; var fs = require('fs'); var rs = fs.createReadStream('sample.txt'); var ws = fs.createWriteStream('copied.txt'); rs.pipe(ws);

默认状况下,当Readable流的数据读取完毕,end事件触发后,将自动关闭Writable流。若是咱们不但愿自动关闭Writable流,须要传入参数:

readable.pipe(writable, { end: false });

http

Node.js开发的目的就是为了用JavaScript编写Web服务器程序。由于JavaScript实际上已经统治了浏览器端的脚本,其优点就是有世界上数量最多的前端开发人员。若是已经掌握了JavaScript前端开发,再学习一下如何将JavaScript应用在后端开发,就是名副其实的全栈了。

HTTP协议

要理解Web服务器程序的工做原理,首先,咱们要对HTTP协议有基本的了解。若是你对HTTP协议不太熟悉,先看一看HTTP协议简介

HTTP服务器

要开发HTTP服务器程序,从头处理TCP链接,解析HTTP是不现实的。这些工做实际上已经由Node.js自带的http模块完成了。应用程序并不直接和HTTP协议打交道,而是操做http模块提供的requestresponse对象。

request对象封装了HTTP请求,咱们调用request对象的属性和方法就能够拿到全部HTTP请求的信息;

response对象封装了HTTP响应,咱们操做response对象的方法,就能够把HTTP响应返回给浏览器。

用Node.js实现一个HTTP服务器程序很是简单。咱们来实现一个最简单的Web程序hello.js,它对于全部请求,都返回Hello world!

'use strict'; // 导入http模块: var http = require('http'); // 建立http server,并传入回调函数: var server = http.createServer(function (request, response) { // 回调函数接收request和response对象, // 得到HTTP请求的method和url: console.log(request.method + ': ' + request.url); // 将HTTP响应200写入response, 同时设置Content-Type: text/html: response.writeHead(200, {'Content-Type': 'text/html'}); // 将HTTP响应的HTML内容写入response: response.end('<h1>Hello world!</h1>'); }); // 让服务器监听8080端口: server.listen(8080); console.log('Server is running at http://127.0.0.1:8080/');

在命令提示符下运行该程序,能够看到如下输出:

$ node hello.js Server is running at http://127.0.0.1:8080/

不要关闭命令提示符,直接打开浏览器输入http://localhost:8080,便可看到服务器响应的内容:


http-hello-sample

同时,在命令提示符窗口,能够看到程序打印的请求信息:

GET: /GET: /favicon.ico

这就是咱们编写的第一个HTTP服务器程序!

文件服务器

让咱们继续扩展一下上面的Web程序。咱们能够设定一个目录,而后让Web程序变成一个文件服务器。要实现这一点,咱们只须要解析request.url中的路径,而后在本地找到对应的文件,把文件内容发送出去就能够了。

解析URL须要用到Node.js提供的url模块,它使用起来很是简单,经过parse()将一个字符串解析为一个Url对象:

'use strict'; var url =require('url'); console.log(url.parse('http://user:pass@host.com:8080/path/to/file?query=string#hash'));

结果以下:

Url { protocol: 'http:', slashes: true, auth: 'user:pass', host: 'host.com:8080', port: '8080', hostname: 'host.com', hash: '#hash', search: '?query=string', query: 'query=string', pathname: '/path/to/file', path: '/path/to/file?query=string', href: 'http://user:pass@host.com:8080/path/to/file?query=string#hash' }

处理本地文件目录须要使用Node.js提供的path模块,它能够方便地构造目录:

'use strict'; var path = require('path'); // 解析当前目录: var workDir = path.resolve('.'); // '/Users/michael' // 组合完整的文件路径:当前目录+'pub'+'index.html': var filePath = path.join(workDir, 'pub', 'index.html'); // '/Users/michael/pub/index.html'

使用path模块能够正确处理操做系统相关的文件路径。在Windows系统下,返回的路径相似于C:\Users\michael\static\index.html,这样,咱们就不关心怎么拼接路径了。

最后,咱们实现一个文件服务器file_server.js

'use strict'; var fs = require('fs'), url = require('url'), path = require('path'), http = require('http'); // 从命令行参数获取root目录,默认是当前目录: var root = path.resolve(process.argv[2] || '.'); console.log('Static root dir: ' + root); // 建立服务器: var server = http.createServer(function (request, response) { // 得到URL的path,相似 '/css/bootstrap.css': var pathname = url.parse(request.url).pathname; // 得到对应的本地文件路径,相似 '/srv/www/css/bootstrap.css': var filepath = path.join(root, pathname); // 获取文件状态: fs.stat(filepath, function (err, stats) { if (!err && stats.isFile()) { // 没有出错而且文件存在: console.log('200 ' + request.url); // 发送200响应: response.writeHead(200); // 将文件流导向response: fs.createReadStream(filepath).pipe(response); } else { // 出错了或者文件不存在: console.log('404 ' + request.url); // 发送404响应: response.writeHead(404); response.end('404 Not Found'); } }); }); server.listen(8080); console.log('Server is running at http://127.0.0.1:8080/');

没有必要手动读取文件内容。因为response对象自己是一个Writable Stream,直接用pipe()方法就实现了自动读取文件内容并输出到HTTP响应。

在命令行运行node file_server.js /path/to/dir,把/path/to/dir改为你本地的一个有效的目录,而后在浏览器中输入

http://localhost:8080/index.html

http-index-page

只要当前目录下存在文件index.html,服务器就能够把文件内容发送给浏览器。观察控制台输出:

200 /index.html 200 /css/uikit.min.css 200 /js/jquery.min.js 200 /fonts/fontawesome-webfont.woff2

第一个请求是浏览器请求index.html页面,后续请求是浏览器解析HTML后发送的其它资源请求。

crypto

crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会很是慢。Nodejs用C/C++实现这些算法后,经过cypto这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。

MD5和SHA1

MD5是一种经常使用的哈希算法,用于给任意数据一个“签名”。这个签名一般用一个十六进制的字符串表示:

const crypto = require('crypto'); const hash = crypto.createHash('md5'); // 可任意屡次调用update(): hash.update('Hello, world!'); hash.update('Hello, nodejs!'); console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544

update()方法默认字符串编码为UTF-8,也能够传入Buffer

若是要计算SHA1,只须要把'md5'改为'sha1',就能够获得SHA1的结果

1f32b9c9932c02227819a4151feed43e131aca40

还可使用更安全的sha256和sha512

Hmac

Hmac算法也是一种哈希算法,它能够利用MD5或SHA1等哈希算法。不一样的是,Hmac还须要一个密钥:

const crypto = require('crypto'); const hmac = crypto.createHmac('sha256', 'secret-key'); hmac.update('Hello, world!'); hmac.update('Hello, nodejs!'); console.log(hmac.digest('hex')); // 80f7e22570...

只要密钥发生了变化,那么一样的输入数据也会获得不一样的签名,所以,能够把Hmac理解为用随机数“加强”的哈希算法。

AES

AES是一种经常使用的对称加密算法,加解密都用同一个密钥。crypto模块提供了AES支持,可是须要本身封装好函数,便于使用:

const crypto = require('crypto'); function aesEncrypt(data, key) { const cipher = crypto.createCipher('aes192', key); var crypted = cipher.update(data, 'utf8', 'hex'); crypted += cipher.final('hex'); return crypted; } function aesDecrypt(data, key) { const decipher = crypto.createDecipher('aes192', key); var decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } var data = 'Hello, this is a secret message!'; var key = 'Password!'; var encrypted = aesEncrypt(data, key); var decrypted = aesDecrypt(encrypted, key); console.log('Plain text: ' + data); console.log('Encrypted text: ' + encrypted); console.log('Decrypted text: ' + decrypted);

运行结果以下:

Plain text: Hello, this is a secret message! Encrypted text: 8a944d97bdabc157a5b7a40cb180e7... Decrypted text: Hello, this is a secret message!

能够看出,加密后的字符串经过解密又获得了原始内容。

注意到AES有不少不一样的算法,如aes192aes-128-ecbaes-256-cbc等,AES除了密钥外还能够指定IV(Initial Vector),不一样的系统只要IV不一样,用相同的密钥加密相同的数据获得的加密结果也是不一样的。

加密结果一般有两种表示方法:hexbase64,这些功能Nodejs所有都支持,可是在应用中要注意,若是加解密双方一方用Nodejs,另外一方用Java、PHP等其它语言,须要仔细测试。若是没法正确解密,要确认双方是否遵循一样的AES算法,字符串密钥和IV是否相同,加密后的数据是否统一为hex或base64格式。

Diffie-Hellman

DH算法是一种密钥交换协议,它可让双方在不泄漏密钥的状况下协商出一个密钥来。DH算法基于数学原理,好比小明和小红想要协商一个密钥,能够这么作:

小明先选一个素数和一个底数,例如,素数p=23,底数g=5(底数能够任选),再选择一个秘密整数a=6,计算A=g^a mod p=8,而后大声告诉小红:p=23,g=5,A=8

小红收到小明发来的p,g,A后,也选一个秘密整数b=15,而后计算B=g^b mod p=19,并大声告诉小明:B=19

小明本身计算出s=B^a mod p=2,小红也本身计算出s=A^b mod p=2
,所以,最终协商的密钥s为2。

在这个过程当中,密钥2并非小明告诉小红的,也不是小红告诉小明的,而是双方协商计算出来的。第三方只能知道p=23g=5A=8B=19,因为不知道双方选的秘密整数a=6b=15,所以没法计算出密钥2。

用crypto模块实现DH算法以下:

const crypto = require('crypto'); // xiaoming's keys: var ming = crypto.createDiffieHellman(512); var ming_keys = ming.generateKeys(); var prime = ming.getPrime(); var generator = ming.getGenerator(); console.log('Prime: ' + prime.toString('hex')); console.log('Generator: ' + generator.toString('hex')); // xiaohong's keys: var hong = crypto.createDiffieHellman(prime, generator); var hong_keys = hong.generateKeys(); // exchange and generate secret: var ming_secret = ming.computeSecret(hong_keys); var hong_secret = hong.computeSecret(ming_keys); // print secret: console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex')); console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));

运行后,能够获得以下输出:

$ node dh.js Prime: a8224c...deead3 Generator: 02 Secret of Xiao Ming: 695308...d519be Secret of Xiao Hong: 695308...d519be

注意每次输出都不同,由于素数的选择是随机的。

证书

crypto模块也能够处理数字证书。数字证书一般用在SSL链接,也就是Web的https链接。通常状况下,https链接只须要处理服务器端的单向认证,如无特殊需求(例如本身做为Root给客户发认证证书),建议用反向代理服务器如Nginx等Web服务器去处理证书。

相关文章
相关标签/搜索