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平台是在后端运行JavaScript代码,因此,必须首先在本机安装Node环境。
目前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版本不对,后面章节的代码不保证能正常运行,请从新安装最新版本。
在正式开始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
正确安装了,能运行就行。
在前面的全部章节中,咱们编写的JavaScript代码都是在浏览器中运行的,所以,咱们能够直接在浏览器中敲代码,而后直接运行。
从本章开始,咱们编写的JavaScript代码将不能在浏览器环境中执行了,而是在Node环境中执行,所以,JavaScript代码将直接在你的计算机上以命令行的方式运行,因此,咱们要先选择一个文本编辑器来编写JavaScript代码,而且把它保存到本地硬盘的某个目录,才可以执行。
那么问题来了:文本编辑器到底哪家强?
推荐两款文本编辑器:
一个是Sublime Text,无偿使用,可是不付费会弹出提示框:
一个是Notepad++,无偿使用,有中文界面:
请注意,用哪一个都行,可是绝对不能用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交互模式。看到相似C:\>
是在Windows提供的命令行模式:
在命令行模式下,能够执行node进入Node交互式环境,也能够执行node hello.js
运行一个.js
文件。看到>
是在Node交互式环境下:
在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规范。在这个规范下,每一个.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
,把这个module
的exports
变量返回,这样,另外一个模块就顺利拿到了模块的输出:
var greet = require('./hello');
以上是Node实现JavaScript模块的一个简单的原理介绍。
不少时候,你会看到,在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运行环境中实现的。
在前面的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
也是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代码既能在浏览器中执行,也能在Node环境执行,但有些时候,程序自己须要判断本身究竟是在什么环境下执行的,经常使用的方式就是根据浏览器和Node环境提供的全局变量名称来判断:
if (typeof(window) === 'undefined') { console.log('node.js'); } else { console.log('browser'); }
后面,咱们将介绍Node.js的经常使用内置模块。
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
参数为null
,data
参数为读取到的String
。当读取发生错误时,err
参数表明一个错误对象,data
为undefined
。
这也是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);
若是咱们要获取文件大小,建立时间等信息,可使用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
是Node.js提供的又一个仅在服务区端可用的模块,目的是支持“流”这种数据结构。
什么是流?流是一种抽象的数据结构。想象水流,当在水管中流动时,就能够从某个地方(例如自来水厂)源源不断地到达另外一个地方(好比你家的洗手池)。
咱们也能够把数据当作是数据流,好比你敲键盘的时候,就能够把每一个字符依次连起来,当作字符流。这个流是从键盘输入到应用程序,实际上它还对应着一个名字:标准输入流(stdin)。
若是应用程序把字符一个一个输出到显示器上,这也能够当作是一个流,这个流也有名字:标准输出流(stdout)。流的特色是数据是有序的,并且必须依次读取,或者依次写入,不能像Array那样随机定位。
有些流用来读取数据,好比从文件读取数据时,能够打开一个文件流,而后从文件流中不断地读取数据。有些流用来写入数据,好比向文件写入数据时,只须要把数据不断地往文件流中写进去就能够了。
在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
。
就像能够把两个水管串成一个更长的水管同样,两个流也能够串起来。一个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 });
Node.js开发的目的就是为了用JavaScript编写Web服务器程序。由于JavaScript实际上已经统治了浏览器端的脚本,其优点就是有世界上数量最多的前端开发人员。若是已经掌握了JavaScript前端开发,再学习一下如何将JavaScript应用在后端开发,就是名副其实的全栈了。
要理解Web服务器程序的工做原理,首先,咱们要对HTTP协议有基本的了解。若是你对HTTP协议不太熟悉,先看一看HTTP协议简介。
要开发HTTP服务器程序,从头处理TCP
链接,解析HTTP是不现实的。这些工做实际上已经由Node.js自带的http
模块完成了。应用程序并不直接和HTTP协议打交道,而是操做http
模块提供的request
和response
对象。
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
,便可看到服务器响应的内容:
同时,在命令提示符窗口,能够看到程序打印的请求信息:
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
只要当前目录下存在文件index.html,服务器就能够把文件内容发送给浏览器。观察控制台输出:
200 /index.html 200 /css/uikit.min.css 200 /js/jquery.min.js 200 /fonts/fontawesome-webfont.woff2
第一个请求是浏览器请求index.html
页面,后续请求是浏览器解析HTML后发送的其它资源请求。
crypto
模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能,但速度会很是慢。Nodejs用C/C++实现这些算法后,经过cypto
这个模块暴露为JavaScript接口,这样用起来方便,运行速度也快。
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算法也是一种哈希算法,它能够利用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是一种经常使用的对称加密算法,加解密都用同一个密钥。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有不少不一样的算法,如aes192
,aes-128-ecb
,aes-256-cbc
等,AES除了密钥外还能够指定IV
(Initial Vector),不一样的系统只要IV不一样,用相同的密钥加密相同的数据获得的加密结果也是不一样的。
加密结果一般有两种表示方法:hex
和base64
,这些功能Nodejs所有都支持,可是在应用中要注意,若是加解密双方一方用Nodejs,另外一方用Java、PHP等其它语言,须要仔细测试。若是没法正确解密,要确认双方是否遵循一样的AES算法,字符串密钥和IV是否相同,加密后的数据是否统一为hex或base64格式。
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=23
,g=5
,A=8
,B=19
,因为不知道双方选的秘密整数a=6
和b=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服务器去处理证书。