Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,是一个可以让 JavaScript 运行在服务器端的平台
Node.js 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。Node.js 的包管理器 npm,是全球最大的开源库生态系统。
采用了单线程、异步式I/O、事件驱动式的程序设计模型
I/O(input/output),即输入/输出端口。每个设备都会有一个专用的I/O地址,用来处理自己的输入输出信息。
ctrl + c - 退出当前终端。
ctrl + c 按下两次 - 退出 Node REPL。
ctrl + d - 退出 Node REPL.
向上/向下 键 - 查看输入的历史命令
tab 键 - 列出当前命令
help - 列出使用命令
break - 退出多行表达式
.clear - 退出多行表达式
save filename - 保存当前的 Node REPL 会话到指定文件
load filename - 载入当前 Node REPL 会话的文件内容。
Node.js 事件循环
Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。
Node.js 的每一个 API 都是异步的,并作为一个独立线程运行,使用异步函数调用,并处理并发。
Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。
Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.
观察者模式:观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
事件驱动程序
Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。
当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。
这个模型非常高效可扩展性非常强,因为webserver一直接受请求而不等待任何读写操作。(这也被称之为非阻塞式IO或者事件驱动IO)
在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。
整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。
Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件
Node.js 中所谓的 JavaScript 只是 Core JavaScript,或者说是 ECMAScript 的一个实现,不包含 DOM(文档对象模型)、BOM(浏览器对象模型) 或者 Client JavaScript(客户端脚本)。这是因为 Node.js 不运行在浏览器中,所以不需要使用浏览器中的许多特性。
Node.js 能做什么
具有复杂逻辑的网站;
基于社交网络的大规模 Web 应用;
Web Socket 服务器;
TCP/UDP 套接字应用程序;
命令行工具;
交互式终端程序;
带有图形用户界面的本地应用程序;
单元测试工具;
客户端 JavaScript 编译器。
TCP/UDP协议
TCP (Transmission Control Protocol)和UDP(User Datagram Protocol)协议属于传输层协议。其中TCP提供IP环境下的数据可靠传输,它提供的服务包括数据流传送、可靠性、有效流控、全双工操作和多路复用。通过面向连接、端到端和可靠的数据包发送。通俗说,它是事先为所发送的数据开辟出连接好的通道,然后再进行数据发送;而UDP则不为IP提供可靠性、流控或差错恢复功能。一般来说,TCP对应的是可靠性要求高的应用,而UDP对应的则是可靠性要求低、传输经济的应用。TCP支持的应用协议主要有:Telnet、FTP、SMTP等;UDP支持的应用层协议主要有:NFS(网络文件系统)、SNMP(简单网络管理协议)、DNS(主域名称系统)、TFTP(通用文件传输协议)等。
Node.js 用异步式 I/O 和事件驱动代替多线程,带来了可观的性能提升。Node.js 除了使用 V8 作为JavaScript引擎以外,还使用了高效的 libev 和 libeio 库支持事件驱动和异步式 I/O。
CommonJS 为了统一javascript在浏览器外的实现
CommonJS 规范包括了模块(modules)、包(packages)、系统(system)、二进制(binary)、控制台(console)、编码(encodings)、文件系统(filesystems)、套接字(sockets)、单元测试(unit testing)等部分。目前大部分标准都在拟定和讨论之中,已经发布的标准有
Modules/1.0、Modules/1.1、Modules/1.1.1、Packages/1.0、System/1.0。
REPL (Read-eval-print loop),即输入—求值—输出循环
例子:
$ node
> console.log('Hello World');
Hello World
Undefined
Supervisor工具
在开发 Node.js 实现的 HTTP 应用时会发现,无论你修改了代码的哪一部份,都必须终止
Node.js 再重新运行才会奏效。这是因为 Node.js 只有在第一次引用到某部份时才会去解析脚本文件,以后都会直接访问内存,避免重复载入,
Node.js的这种设计虽然有利于提高性能,却不利于开发调试,因
为我们在开发过程中总是希望修改后立即看到效果,而不是每次都要终止进程并重启。
supervisor 可以帮助你实现这个功能,它会监视你对代码的改动,并自动重启 Node.js。
使用方法很简单,首先使用 npm 安装 supervisor:
$ npm install -g supervisor
如果你使用的是 Linux 或 Mac,直接键入上面的命令很可能会有权限错误。原因是 npm需要把 supervisor 安装到系统目录,需要管理员授权,可以使用 sudo npm install -gsupervisor 命令来安装。
接下来,使用 supervisor 命令启动 app.js:
$ supervisor app.js
异步式 I/O 与事件式编程
Node.js 最大的特点就是异步式 I/O(或者非阻塞 I/O)与事件紧密结合的编程模式。这种模式与传统的同步式 I/O 线性的编程思路有很大的不同,因为控制流很大程度上要靠事件和回调函数来组织,一个逻辑要拆分为若干个单元。
阻塞与线程
什么是阻塞(block)呢?线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作),通常要耗费较长的时间,这时操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为 阻塞。当 I/O 操作完毕时,操作系统将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种 I/O 模式就是通常的同步式 I/O(Synchronous I/O)或阻塞式 I/O (Blocking I/O)。
相应地,异步式 I/O (Asynchronous I/O)或非阻塞式 I/O (Non-blocking I/O)则针对所有 I/O 操作不采用阻塞的策略。当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作的完成或数据的返回,而只是将 I/O 请求发送给操作系统,继续执行下一条语句。当操作系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个事件。为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。
阻塞模式下,一个线程只能处理一项任务,要想提高吞吐量必须通过多线程。而非阻塞模式下,一个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率永远是 100%,I/O 以事件的方式通知。在阻塞模式下,多线程往往能提高系统吞吐量,因为一个线程阻塞时还有其他线程在工作,多线程可以让 CPU 资源不被阻塞中的线程浪费。而在非阻塞模式下,线程不会被 I/O 阻塞,永远在利用 CPU。多线程带来的好处仅仅是在多核 CPU 的情况下利用更多的核,而Node.js的单线程也能带来同样的好处。这就是为什么 Node.js 使用了单线程、非阻塞的事件编程模式
单线程事件驱动的异步式 I/O 比传统的多线程阻塞式 I/O 究竟好在哪里呢?简而言之,异步式 I/O 就是少了多线程的开销。对操作系统来说,创建一个线程的代价是十分昂贵的,需要给它分配内存、列入调度,同时在线程切换的时候还要执行内存换页,CPU 的缓存被清空,切换回来的时候还要重新从内存中读取信息,破坏了数据的局部性。
回调函数
Node.js 中如何用异步的方式读取一个文件
//readfile.js
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
console.log('end.');
运行的结果如下:
end.
Contents of the file.
注:function()是回调函数,在Node.js 中,异步式 I/O 是通过回调函数来实现的。 fs.readFile 接收了三个参数,第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。
JavaScript 支持匿名的函数定义方式,譬如我们例子中回调函数的定义就是嵌套在fs.readFile 的参数表中的。这种定义方式在 JavaScript 程序中极为普遍,与下面这种定义
方式实现的功能是一致的:
//readfilecallback.js
function readFileCallBack(err, data) {
if (err) {
console.error(err);
} else {
console.log(data);
}
}
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', readFileCallBack);
console.log('end.');
fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的事件时,事件循环会主动调用回调函数以完成后续工作。因此我们会先看到 end. ,再看到file.txt 文件的内容
事件
Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。在开发者看来,事件由 EventEmitter 对象提供。
Node.js 的事件循环机制
Node.js 在什么时候会进入事件循环呢?答案是 Node.js 程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数,所以 Node.js 始终在事件循环中,程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出 I/O 请求或直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。
模块和包
什么是模块
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。
在前面章节的例子中,我们曾经用到了 var http = require('http'), 其中 http是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过require 函数获取了这个模块,然后才能使用其中的对象。
创建及加载模块
1. 创建模块
一个文件就是一个模块
Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口, require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。
例子:
//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};
在同一目录下创建 getmodule.js,内容是:
//getmodule.js
var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();
运行node getmodule.js,结果是:
Hello BYVoid
在以上示例中,module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过 require('./module') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。
2. 单次加载
上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为
require 不会重复加载模块,也就是说无论调用多少次 require, 获得的模块都是同一个。
我们在 getmodule.js 的基础上稍作修改:
//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();
运行后发现输出结果是 Hello BYVoid 2 ,这是因为变量 hello1 和hello2 指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。
3. 覆盖 exports
把一个对象封装到模块中,例如:
//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;
此时我们在其他文件中需要通过 require('./singleobject').Hello 来获取Hello 对象,这略显冗余,可以用下面方法稍微简化:
//hello.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
module.exports = Hello;
//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello()
注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello=Hello 。在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的exports 。
事实上, exports 本身仅仅是一个普通的空对象,即 {} ,它专门用来声明接口,本质上是通过它为模块闭包的内部建立了一个有限的访问接口。因为它没有任何特殊的地方,所以可以用其他东西来代替,譬如我们上面例子中的 Hello 对象。
警告:
不可以通过对exports 直接赋值代替对module.exports 赋值。
exports 实际上只是一个和 module.exports 指向同一个对象的变量,它本身会在模块执行结束后释放,但 module不会,因此只能通过指定module.exports 来改变访问接口。
创建包
包是在模块基础上更深一步的抽象,Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合 CommonJS 规范的包应该具备以下特征:
package.json 必须在包的顶层目录下;
二进制文件应该在 bin 目录下;
JavaScript 代码应该在 lib 目录下;
文档应该在 doc 目录下;
单元测试应该在 test 目录下。
1. 作为文件夹的模块
模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一个文件夹。最简单的包,就是一个作为文件夹的模块。
例子:
建立一个叫做 somepackage 的文件夹,在其中创建 index.js,内容如下:
//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};
然后在 somepackage 之外建立 getpackage.js,内容如下:
//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();
运行 node getpackage.js,控制台将输出结果 Hello. 。
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制
package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
- 2. package.json
在前面例子中的 somepackage 文件夹下,我们创建一个叫做 package.json 的文件,内容如
下所示:
{
"main" : "./lib/interface.js"
}
然后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以同样的方式再次调用这个包,依然可以正常使用。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作为包的接口。
备注:package.json里面不能有main{}之类的注释,不然会报错
package.json是CommonJS 规定的用来描述包的文件,完全符合规范的 package.json 文件应该含有以下字段。
name :包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
description :包的简要说明。
version :符合语义化版本识别
keywords :关键字数组,通常用于搜索。
maintainers :维护者数组,每个元素要包含 name 、 email (可选)、 web (可选)字段。
contributors :贡献者数组,格式与 maintainers 相同。包的作者应该是贡献者数组的第一个元素。
bugs :提交bug的地址,可以是网址或者电子邮件地址。
licenses :许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到许可证文本的地址)字段。
repositories :仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
dependencies :包的依赖,一个关联数组,由包名称和版本号组成。
例子:
{
"name": "mypackage",
"description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
"version": "0.7.0",
"keywords": [
"package",
"example"
],
"maintainers": [
{
"name": "Bill Smith",
"email": "bills@example.com",
}
],
"contributors": [
{
"name": "BYVoid",
"web": "http://www.byvoid.com/"
}
],
"bugs": {
"mail": "dev@example.com",
"web": "http://www.example.com/bugs"
},
"licenses": [
{
"type": "GPLv2",
"url": "http://www.example.org/licenses/gpl.html"
}
],
"repositories": [
{
"type": "git",
"url": "http://github.com/BYVoid/mypackage.git"
}
],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}
Node.js 包管理器
Node.js包管理器,即npm是 Node.js 官方提供的包管理工具,它已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。
本地模式和全局模式
本地模式:npm [install/i] [package_name]
全局模式:npm [install/i] -g [package_name]
为什么要使用全局模式呢?多数时候并不是因为许多程序都有可能用到它,为了减少多重副本而使用全局模式,而是因为本地模式不会注册 PATH 环境变量。举例说明,我们安装supervisor 是为了在命令行中运行它,譬如直接运行 supervisor script.js,这时就需要在 PATH环境变量中注册 supervisor。npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中的 bin 目录没有包含在 PATH 环境变量中,不能直接在命令行中调用。而当我们使用全局模式安装时,npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文件中 bin 字段包含的文件会被链接到 /usr/local/bin/。/usr/local/bin/ 是在 PATH 环境变量中默认定义的,因此就可以直接在命令行中运行 supervisor script.js 命令了。
创建全局链接
npm 提供了一个有趣的命令 npm link,它的功能是在本地包和全局包之间创建符号链接。我们说过使用全局模式安装的包不能直接通过require 使用,但通过 npm link 命令可以打破这一限制。举个例子,我们已经通过 npm install -g express 安装了 express ,这时在工程的目录下运行命令:
$ npm link express
./node_modules/express -> /usr/local/lib/node_modules/express
我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这种方法,我们就可以把全局包当本地包来使用了。
包的发布
*编写模块
1)新建文件夹,比如:somepackage
2) 该文件夹下新建js文件,比如:index.js
js内容如下:
exports.sayHello=function(){
return "Hello,zhoudaozhang.";
};
*初始化包描述文件
使用cmd命令定位到somepackage文件夹
输入 npm init 并执行
npm的init命令可以帮助你生成package.json文件,这是我的文件内容:
{
"name": "somepackage_xiaotian",
"version": "1.0.0",
"description": "'hehe'",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" &&
exit 1"
},
"keywords": [
"Hello",
"world"
],
"author": "zhou daozhang",
"license": "ISC"
}
*注册包仓库账号
npm adduser
输入这个命令会有提示输入用户名,密码,邮箱等资料
这和去官方源仓库https://www.npmjs.com/注册是一样的
*上传包
npm publish
如果上传成功会提示
+somepackage_xiaotian@1.0.0 否则上传失败
这个时候去https://www.npmjs.com/登陆仓库账号就可以看到自己的包啦
*安装包
npm install somepackage_xiaotian
通过此命令可以在世界上任一一台机器上安装somepackage_xiaotian了
发布包过程可能会遇到很多问题,我印象比较深刻的是npm ERR publish
403
You do not have permission to publish 'somepackage'.Are you logged in as
the corrent user?:somepackage
意思是我没权限发布somepackage,并问我是否使用了正确的账号,
那也许是somepackage被别人发布过了吧,所以我修改了package.json文件
把name改成somepackage_xiaotian.
*分析包
这个命令可以为你分析出当前路径下能够通过模块路径找到的所有包,并生成依赖树。
npm ls
全局对象Global
JavaScript
中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。在浏览器 JavaScript 中,通常 window 是全局对象,而 Node.js 中的全局对象是 global ,所有全局变量(除了 global 本身以外)都是 global
对象的属性。
全局对象与全局变量
global
最根本的作用是作为全局变量的宿主
满足以下条件的变量是全局变量:
在最外层定义的变量;
全局对象的属性;
隐式定义的变量(未定义直接赋值的变量)。
注:在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的,而模块本身不是最外层上下文
提示:永远使用 var 定义变量以避免引入全局变量,因为全局变量会污染命名空间,提高代码的耦合风险。
Process
process 是一个全局变量,即 global 对象的属性。它用于描述当前 Node.js 进程状态的对象,提供了一个与操作系统的简单接口
process.argv
是命令行参数数组,第一个元素是 node,第二个元素是脚本文件名
process.stdout
是标准输出流,通常我们使用的
console.log() 向标准输出打印字符,而
process.stdout.write() 函数提供了更底层的接口。
process.stdin
是标准输入流,初始时它是被暂停的,要想从标准输入读取数据,你必须恢复流,并手动编写流的事件响应函数。
process.nextTick(callback)
的功能是为事件循环设置一项任务,Node.js 会在下次事件循环调响应时调用
callback 。
http://nodejs.org/api/process.html详细了解的地方
console
console 用于提供控制台标准输出
console.log()
:向标准输出流打印字符并以换行符结束
console.error()
:与 console.log() 用法相同,只是向标准错误流输出。
console.trace()
:向标准错误流输出当前的调用栈。
常用工具 util
util 是一个 Node.js 核心模块,提供常用函数的集合,用于弥补核心 JavaScript 的功能过于精简的不足。
util.inherits
util.inherits(constructor,
superConstructor) 是一个实现对象间原型继承的函数。
var
util = require('util');
function
Base() {
this.name
= 'base';
this.base
= 1991;
this.sayHello
= function() {
console.log('Hello
' + this.name);
};
}
Base.prototype.showName
= function() {
console.log(this.name);
};
function
Sub() {
this.name
= 'sub';
}
util.inherits(Sub,
Base);
var
objBase = new Base();
objBase.showName();
objBase.sayHello();
console.log(objBase);
var
objSub = new Sub();
objSub.showName();
//objSub.sayHello();
console.log(objSub);
结果:
base
Hello
base
{
name: 'base', base: 1991, sayHello: [Function] }
sub
{
name: 'sub' }
这个只能继承原型里面的属性和方法
util.inspect
util.inspect(object,[showHidden],[depth],[colors])
是一个将任意对象转换为字符串的方法,通常用于调试和错误输出。它至少接受一个参数
object ,即要转换的对象。
showHidden 是一个可选参数,如果值为 true ,将会输出更多隐藏信息。
depth 表示最大递归的层数,如果对象很复杂,你可以指定层数以控制输出信息的多
少。如果不指定 depth ,默认会递归2层,指定为 null 表示将不限递归层数完整遍历对象。
如果 color 值为
true ,输出格式将会以 ANSI 颜色编码,通常用于在终端显示更漂亮的效果。
注:util.inspect 并不会简单地直接把对象转换为字符串,即使该对
象定义了 toString 方法也不会调用。
module.exports 和exports的区别?
Module.exports才是真正的接口,exports只不过是它的一个辅助工具。 最终返回给调用的是Module.exports而不是exports。
所有的exports收集到的属性和方法,都赋值给了Module.exports。当然,这有个前提,就是Module.exports本身不具备任何属性和方法。如果,Module.exports已经具备一些属性和方法,那么exports收集来的信息将被忽略。
这就是
EventEmitter 最简单的用法。接下来我们介绍一下 EventEmitter 常用的API。
EventEmitter.on(event, listener) 为指定事件注册一个监听器,接受一个字符串
event 和一个回调函数 listener 。
EventEmitter.emit(event, [arg1], [arg2],
[...]) 发射 event 事件,传递若干可选参数到事件监听器的参数表。
EventEmitter.once(event, listener) 为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器。
EventEmitter.removeListener(event, listener) 移除指定事件的某个监听器,
listener 必须是该事件已经注册过的监听器。
文件系统 fs
fs 模块是文件操作的封装,它提供了文件的读取、写入、更名、删除、遍历目录、链接等 POSIX 文件系统操作。与其他模块不同的是, fs 模块中所有的操作都提供了异步的和同步的两个版本,例如读取文件内容的函数有异步的
fs.readFile() 和同步的fs.readFileSync() 。
fs.readFile
fs.readFile(filename,[encoding],[callback(err,data)])
是最简单的读取
文件的函数。它接受一个必选参数 filename ,表示要读取的文件名。第二个参数
encoding是可选的,表示文件的字符编码。 callback 是回调函数,用于接收文件的内容。如果不指定
encoding ,则
callback 就是第二个参数。回调函数提供两个参数 err
和
data , err 表示有没有错误发生, data 是文件内容。如果指定了 encoding , data 是一个解析后的字符串,否则 data 将会是以
Buffer 形式表示的二进制数据。
例;
以 Buffer 形式表示的二进制数据
var
fs = require('fs');
fs.readFile('content.txt',
function(err, data) {
if
(err) {
console.error(err);
}
else {
console.log(data);
}
});
encoding
指定编码
var
fs = require('fs');
fs.readFile('content.txt',
'utf-8', function(err, data) {
if (err)
{
console.error(err);
}
else {
console.log(data);
}
});
fs.readFileSync
fs.readFileSync(filename,
[encoding]) 是
fs.readFile 同步的版本。它接受的参数和 fs.readFile 相同,而读取到的文件内容会以函数返回值的形式返回。如果有错误发生, fs 将会抛出异常,你需要使用 try 和
catch 捕捉并处理异常。
Fs.read
fs.read(fd,
buffer, offset, length, position, [callback(err, bytesRead,
buffer)])
是 POSIX
read 函数的封装,相比fs.readFile 提供了更底层的接口。 fs.read的功能是从指定的文件描述符fd 中读取数据并写入 buffer 指向的缓冲区对象。offset 是buffer 的写入偏移量。length 是要从文件中读取的字节数。position 是文件读取的起始
位置,如果 position 的值为
null ,则会从当前文件指针的位置读取。回调函数传递bytesRead 和 buffer ,分别表示读取的字节数和缓冲区对象。
用的时候,和fs.open一起用
var
fs = require('fs');
fs.open('content.txt',
'r', function(err, fd) {
if
(err) {
console.error(err);
return;
}
var
buf = new Buffer(8);
fs.read(fd,
buf, 0, 8, null, function(err, bytesRead, buffer) {
if
(err) {
console.error(err);
return;
}
console.log('bytesRead:
' + bytesRead);
console.log(buffer);
})
});
运行结果则是:
bytesRead:
8
<Buffer
54 65 78 74 20 e6 96 87>
HTTP 服务器与客户端
工程的结构
Express
都生成了哪些文件,除了 package.json,它只产生了两个 JavaScript 文件 app.js 和 routes/index.js。模板引擎 ejs 也有两文件 index.ejs 和layout.ejs,此外还有样式表 style.css。下面来详细看看这几个文件。
- app.js
routes 是一个文件夹形式的本地模块,即 ./routes/index.js ,它的功能是为指定路径组织返回内容,相当于 MVC 架构中的控制器。
- routes/index.js
routes/index.js
是路由文件,相当于控制器,用于组织展示的内容:
exports.index
= function(req, res) {
res.render('index',
{ title: 'Express' });
};
app.js
中通过 app.get('/', routes.index); 将“ / ”路径映射到 exports.index函数下。其中只有一个语句
res.render('index', { title: 'Express' }) ,功能是调用模板解析引擎,翻译名为 index 的模板,并传入一个对象作为参数,这个对象只有一个
属性,即 title: 'Express' 。
3.
index.ejs
index.ejs
是模板文件,即 routes/index.js 中调用的模板,内容是:
<h1><%=
title %></h1>
<p>Welcome
to <%= title %></p>
它的基础是 HTML 语言,其中包含了形如<%= title %>的标签,功能是显示引用的变量,即
res.render 函数第二个参数传入的对象的属性。
4.
layout.ejs (新的里面,没有这个)
模板文件不是孤立展示的,默认情况下所有的模板都继承自 layout.ejs,即<%- body %>
部分才是独特的内容,其他部分是共有的,可以看作是页面框架。
<!DOCTYPE
html>
<html>
<head>
<title><%=
title %></title>
<link
rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<%-
body %>
</body>
</html>
REST 风格的路由规则
表征状态转移(Representational State Transfer),它是一种基于 HTTP 协议的网络应用的接口风格,充分利用 HTTP 的方法实现统一风格接口的服务。
安全是指没有副作用,即请求不会对资源产生变动,连续访问多次所获得的结果不受访问者的影响。而幂等指的是重复请求多次与一次请求的效果是一样的,比如获取和更新操作是幂等的,这与新增不同。删除也是幂等的,即重复删除一个资源,和删除一次是一样的。
在 MVC 架构中,模板引擎包含在服务器端。
基于 JavaScript 的模板引擎有许多种实现,我们推荐使用 ejs (Embedded JavaScript),因为它十分简单,而且与 Express 集成良好。由于它是标准 JavaScript 实现的,因此它不仅可以运行在服务器端,还可以运行在浏览器中。
ejs 的标签系统非常简单,它只有以下3种标签。
<% code %>:JavaScript 代码。
<%= code %>:显示替换过 HTML 特殊字符的内容。
<%- code %>:显示原始 HTML 内容。
视图助手
Express
提供了一种叫做视图助手的工具,它的功能是允许在视图中访问一个全局的函数或对象,不用每次调用视图解析的时候单独传入。前面提到的 partial 就是一个视图助手。
由于ejs的升级,《node.js开发指南》中使用的 partial 函数已经摒弃,使用foreach,include代替
<%-
partial('listitem',items) %>
修改为:
1 2 3 4 |
<ul><% <% <%}) </ul> |
next()
Express
提供了路由控制权转移的方法,即回调函数的第三个参数 next ,通过调用next() ,会将路由控制权转移给后面的规则,例如:
app.all('/user/:username',
function(req, res, next) {
console.log('all
methods captured');
next();
});
app.get('/user/:username',
function(req, res) {
res.send('user:
' + req.params.username);
});
当访问被匹配到的路径时,如 http://localhost:3000/user/carbo,会发现终端中打印了
allmethods captured ,而且浏览器中显示了 user: carbo 。这说明请求先被第一条路由规则捕获,完成 console.log 使用 next() 转移控制权,又被第二条规则捕获,向浏览器返回了信息。
这是一个非常有用的工具,可以让我们轻易地实现中间件,而且还能提高代码的复用程度。例如我们针对一个用户查询信息和修改信息的操作,分别对应了 GET 和 PUT 操作,而两者共有的一个步骤是检查用户名是否合法,因此可以通过 next()
方法实现:
var
users = {
'byvoid':
{
name:
'Carbo',
website:
'http://www.byvoid.com'
}
};
app.all('/user/:username',
function(req, res, next) {
// 检查用户是否存在
if (users[req.params.username])
{
next();
}
else {
next(new
Error(req.params.username + ' does not exist.'));
}
});
app.get('/user/:username',
function(req, res) {
// 用户一定存在,直接展示
res.send(JSON.stringify(users[req.params.username]));
});
app.put('/user/:username',
function(req, res) {
// 修改用户信息
res.send('Done');
});
上面例子中, app.all 定义的这个路由规则实际上起到了中间件的作用,把相似请求
的相同部分提取出来,有利于代码维护其他 next 方法如果接受了参数,即代表发生了错误。
使用这种方法可以把错误检查分段化,降低代码耦合度。
微博网站过程中发现的一些问题;
1、
css和javascritp的路径不用写绝对路径,例:
<script src="/javascripts/jquery-1.11.3.js"></script>
<script src="/javascripts/bootstrap.js"></script>
<link rel='stylesheet' href='/stylesheets/bootstrap.css'
/>
<link href="/stylesheets/bootstrap-responsive.css"
rel="stylesheet">
2、
app.js里面要加这个
3、 var partials = require('express-partials');
app.use(partials());
之前要安装express-partials模块,npm install express-partials –g
3上面这个要写在
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
后面
数据库的格式是由表(table)、行(row)、字段(field)组成的。表有固定的结构,规定了
每行有哪些字段,在创建时被定义,之后修改很困难。行的格式是相同的,由若干个固定的字段组成。每个表可能有若干个字段作为索引(index),这其中有的是主键(primary key),用于约束表中的数据,还有唯一键(unique key),确保字段中不存放重复数据。表和表之间
可能还有相互的约束,称为外键(foreign key)。对数据库的每次查询都要以行为单位,复杂的查询包括嵌套查询、连接查询和交叉表查询。
拥有这些功能的数据库被称为关系型数据库,关系型数据库通常使用一种叫做 SQL(Structured Query Language)的查询语言作为接口,因此又称为 SQL 数据库。典型的 SQL 数据库有 MySQL、Oracle、Microsoft SQL Server、PostgreSQL、SQLite,等等。
会话
会话是一种持久的网络协议,用于完成服务器和客户端之间的一些交互行为。会话是一个比连接粒度更大的概念,一次会话可能包含多次连接,每次连接都被认为是会话的一次操作。在网络应用开发中,有
必要实现会话以帮助用户交互。例如网上购物的场景,用户浏览了多个页面,购买了一些物品,这些请求在多次连接中完成。许多应用层网络协议都是由会话支持的,如 FTP、Telnet 等,而 HTTP 协议是无状态的,本身不支持会话,因此在没有额外手段的帮助下,前面场景中服务器不知道用户购买了什么。
为了在无状态的 HTTP 协议之上实现会话,Cookie 诞生了。Cookie 是一些存储在客户端的信息,每次连接的时候由浏览器向服务器递交,服务器也向浏览器发起存储 Cookie 的请求,依靠这样的手段服务器可以识别客户端。我们通常意义上的 HTTP 会话功能就是这样
实现的。具体来说,浏览器首次向服务器发起请求时,服务器生成一个唯一标识符并发送给客户端浏览器,浏览器将这个唯一标识符存储在 Cookie 中,以后每次再发起请求,客户端浏览器都会向服务器传送这个唯一标识符,服务器通过这个唯一标识符来识别用户。
Underscore模块