https://www.shiyanlou.com/courses/44/learning/?id=undefined

1.Node.js简介及简单实用

简介

现在我们将开始进行 Node.js 第一课的学习。本节中,我们将对 Node.js 进行简单的介绍,对 Node.js 的特点及适用场景进行讲述。通过本节的讲解,我们将会完成对 NPM、Node.js 全局对象、Node.js REPL、Node.js 调试的学习。另外我们还将使用 Node.js 创建一个简单的应用。

知识点

  • Node.js 概述、特点及适用场景
  • NPM
  • Node.js REPL
  • Node.js 全局对象
  • Node.js 调试

Node.js 概述、特点及适用场景

Node.js 概述

Node.js 是一个能够在服务器端运行 JavaScript 的开放源代码、跨平台 JavaScript 运行环境。Node.js 由 Node.js 基金会持有和维护,并与 Linux 基金会有合作关系。Node.js 采用 Google 开发的 V8 运行代码,使用事件驱动、非阻塞和异步输入输出模型等技术来提高性能,可优化应用程序的传输量和规模。这些技术通常用于数据密集的即时应用程序。

Node.js 大部分基本模块都用 JavaScript 语言编写。在 Node.js 出现之前,JavaScript 通常作为客户端程序设计语言使用,以 JavaScript 写出的程序常在用户的浏览器上运行。Node.js 的出现使 JavaScript 也能用于服务端编程。Node.js 含有一系列内置模块,使得程序可以脱离 Apache HTTP Server 或 IIS,作为独立服务器运行。

注:定义来自维基百科。

Node.js 特点

  1. 它是一个 JavaScript 运行环境。
  2. 依赖于 Chrome V8 引擎进行代码解释。
  3. 事件驱动:在 Node.js 中,客户端请求建立连接,提交数据等行为,会触发相应的事件。Node.js 在一个时刻,只能执行一个事件回调函数,但是在执行一个事件回调函数的中途,可以转而处理其他事件,然后返回继续执行原事件的回调函数。
  4. 非阻塞 I/O:Node.js 中采用了非阻塞型 I/O 机制,在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率。
  5. 轻量可伸缩,适用于实时数据交互应用。
  6. 单线程:好处是减少内存开销,不用像多线程编程那样处处在意状态同步的问题。缺点是错误会引起整个应用的退出。

Node.js 适用场景

我们从 Node.js 的特点中可以知道 Node.js 擅长处理 I/O,不善于计算(单线程的缺点),因此 Node.js 适用于:当应用程序需要处理大量并发的 I/O,而在向客户端发出响应之前,应用程序内部并不需要进行非常复杂的处理的时候,Node.js 也非常适合与 Web socket 配合,开发长连接的实时交互应用程序。比如:聊天室,博客系统,考试系统等。

NPM

NPM 介绍

NPM 是随同 Node.js 一起安装的包管理工具。因此当我们安装好 Node.js 的时候,也安装好了 NPM。由于实验楼的环境下已经安装好了 Node.js,大家可以直接使用,因此这里就不再讲解如何安装 Node.js。NPM 是一个命令行工具,用于从 NPM Registry 中下载、安装 Node.js 程序,同时解决依赖问题。

实验楼环境介绍

在终端中查看系统 Node.js 版本:

  1. node - v;

查看系统中 NPM 版本:

  1. npm - v;

示意图:
Node.js基础入门 - 图2

Node.js REPL

Node.js REPL(Read Eval Print Loop:交互式解释器)表示一个电脑的环境,类似 Window 系统的终端或 Unix/Linux shell,我们可以在终端中输入命令,并接收系统的响应。我们可以用来执行 JavaScript 代码。

启动 Node 终端

输入以下命令来启动 Node 终端:

  1. node;

如下所示:
Node.js基础入门 - 图3

然后我们就可以在 > 后输入简单的表达式,并按下回车键来显示代码运行结果。

简单的表达式运算

Node.js基础入门 - 图4

使用变量

Node.js基础入门 - 图5

多行表达式

Node.js基础入门 - 图6

三个点的符号是系统自动生成的,回车换行后即可。Node.js 会自动检测是否为连续的表达式。

下划线变量

可以使用下划线(_)获取上一个表达式的运算结果:

Node.js基础入门 - 图7

REPL 常用命令

  • Ctrl + C- 退出当前终端。
  • Ctrl + C- 连续按两次退出 Node REPL。
  • Ctrl + D- 退出 Node REPL。
  • 向上/向下键 - 查看输入的历史命令。

运行 JavaScript 文件

新建两个 JavaScript 文件,分别名为为 test.js 和 test2.js。依次写下如下代码:

test.js 中的代码:

  1. console.log("hello syl");

test2.js 中的代码:

  1. console.log("hello world");

然后通过node + xxx.js来运行,如下所示:
Node.js基础入门 - 图8

注:实验楼下的环境默认是在创建 js 文件的目录下运行的,如果是在自己电脑上操作,需要先进入到 js 文件所在的目录下。

Node.js 全局对象

在 JavaScript 中全局对象通常是 window,而在 Node.js 中全局对象是 global。所有全局变量(除了 global 本身以外)都是 global 对象的属性,我们可以直接访问到 global 的属性。

全局变量

按照 ECMAScript 的定义,满足以下条件的变量是全局变量:

  • 在最外层定义的变量。
  • 全局对象的属性。
  • 隐式定义的变量(未定义直接赋值的变量)。

注:当你定义一个全局变量的时候,这个变量同时也会成为全局对象的属性,反之亦然。在 Node.js 中你不可能在最外层定义变量,因为所有用户代码都是属于当前模块的,而模块本身不是最外层上下文。定义变量一定要使用var关键字,因为全局变量会污染命名空间。

下面介绍一些常用的全局变量和全局函数:

  1. __filename表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。如果在模块中,返回的值是模块文件的路径。比如创建一个叫 fnTest.js 的文件,输入以下代码:
  1. console.log(__filename);

来看看运行效果:
Node.js基础入门 - 图9

  1. __dirname表示当前执行脚本所在的目录。比如创建一个 dnTest.js 的文件,输入以下代码:
  1. console.log(__dirname);

Node.js基础入门 - 图10

  1. setTimeout(cb, ms)全局函数在指定的毫秒(ms)数后执行指定函数(cb),只执行一次函数。比如创建一个 st.js 的文件,输入以下代码:
  1. function foo() {
  2. console.log("Hello, syl!");
  3. }
  4. // 三秒后执行以上函数
  5. setTimeout(foo, 3000);

来看看运行效果:
Node.js基础入门 - 图11

  1. clearTimeout(t)用于停止一个之前通过setTimeout()创建的定时器。参数 t 是通过setTimeout()函数创建的定时器。比如清除上面案例的定时器:
  1. function foo() {
  2. console.log("Hello, syl!");
  3. }
  4. // 三秒后执行以上函数
  5. var t = setTimeout(foo, 3000);
  6. // 清除定时器
  7. clearTimeout(t);

运行效果:
Node.js基础入门 - 图12

  1. setInterval(cb, ms)setTimeout(cb, ms)类似,不同的是这个方法会不停的执行函数。直到clearInterval()被调用或窗口被关闭,也可以按Ctrl + C停止。比如创建一个 sI.js 的文件,输入以下代码:
  1. function foo() {
  2. console.log("Hello, syl!");
  3. }
  4. // 三秒后执行以上函数
  5. var t = setInterval(foo, 3000);
  6. // 清除定时器
  7. clearInterval(t);

运行效果:
Node.js基础入门 - 图13

  1. console.log()是个全局函数用于进行标准输出流的输出,即在控制台中显示一行字符串,和 JavaScript 中的使用一样。

Node.js 创建第一个应用

所有语言的第一课都由 “hello world” 来开始,在使用我们的 Node.js 创建第一个应用之前,我们先来看看 Node.js 应用由哪几部分组成:

  1. 引入 required 模块:使用 required 指令来载入 Node.js 模块。
  2. 创建服务器:服务器可以监听客户端的请求,类似于 Apache、Nginx 等 HTTP 服务器。
  3. 接受请求与响应请求。

新建一个名为 server.js 的文件。

  1. var http = require("http"); // 加载 http 模块,并将实例化的 HTTP 赋值给变量 http
  2. http
  3. .createServer(function (request, response) {
  4. // 发送 HTTP 头部
  5. // HTTP 状态值: 200 : OK
  6. // 内容类型: text/plain
  7. response.writeHead(200, { "Content-Type": "text/plain" });
  8. response.end("Hello World\n"); // 发送响应数据 "Hello World"
  9. })
  10. .listen(8080);
  11. // 终端打印如下信息
  12. console.log("Server running at http://127.0.0.1:8080/");

以上代码我们完成了一个可以工作的 HTTP 服务器。如果在本地计算机使用node server.js命令,你可以直接在浏览器中访问http://127.0.0.1:8080/,你会看到一个写着 “Hello World”的网页。在实验楼的环境下,请先输入:

  1. node server.js

然后点击右侧的 Web 服务按钮启动 Web 服务(注意:只能使用 8080 端口,即默认端口,使用其它端口无法访问),可以在新打开的页面中查看运行效果。
浏览器运行效果:

Node.js基础入门 - 图14

Node.js 调试

代码需要调试,Node.js 也不例外。我们把response.writeHead故意写错,写成response.writeHad,然后在浏览器中运行,来看看效果:

Node.js基础入门 - 图15

注:一定要在浏览器中运行了才会看到上述的错误提示。

我们可以从它的错误提示中可以看出是第八行的代码出错了。response.writeHad is not a function意思是这行代码用到的response.writeHad并不是一个函数。另外可以使用console.log()语句将其添加到代码中,然后在终端窗口打印结果,帮助我们调试。感兴趣的还可以自行搜索一些其他调试方法,比如:node-inspector,可以尽情的去尝试,看看它们是否对于你的开发有帮助。

总结

本节主要介绍了 Node.js 相关的一些基础概念、特点,使用 Node.js 完成了一个简单的应用编写。主要内容如下:

  • Node.js 特点及适用场景
  • NPM
  • Node.js REPL
  • Node.js 全局对象
  • Node.js 调试

Node.js 的功能十分强大。Node.js 的出现使 JavaScript 不再只局限于客户端,服务端的编写也同样适用。对于本节所创建的简单应用,一定要动手编写调试,加深记忆。

2.Node.js模块

简介

本节中,我们将对 Node.js 的包和模块的概念进行讲述。

知识点

  • Node.js 包
  • Node.js 模块

Node.js包

概述

包用于管理多个模块及其依赖关系,可以对多个模块进行封装,包的根目录必须包含 package.json 文件。package.json 文件是 CommonJS 规范用于描述包的文件,符合 CommonJS 规范的 package.json 文件一般包含以下字段:

  1. name:包名。包名是唯一的,只能包含小写字母、数字和下划线。
  2. version:包版本号。
  3. description:包说明。
  4. keywords:关键字数组,用于搜索。
  5. homepage:项目主页。
  6. bugs:提交 bug 的地址。
  7. license:许可证。
  8. maintainers:维护者数组。
  9. contributors:贡献者数组。
  10. repositories:项目仓库托管地址数组。
  11. dependencies:包依赖。

下面是一个 package.json 示例:

  1. {
  2. "name": "shiyanlou",
  3. "description": "Shiyanlou test package.",
  4. "version": "0.1.0",
  5. "keywords": ["shiyanlou", "nodejs"],
  6. "maintainers": [
  7. {
  8. "name": "test",
  9. "email": "test@shiyanlou.com"
  10. }
  11. ],
  12. "contributors": [
  13. {
  14. "name": "test",
  15. "web": "http://www.shiyanlou.com/"
  16. }
  17. ],
  18. "bugs": {
  19. "mail": "test@shiyanlou.com",
  20. "web": "http://www.shiyanlou.com/"
  21. },
  22. "licenses": [
  23. {
  24. "type": "Apache License v2",
  25. "url": "http://www.apache.org/licenses/apache2.html"
  26. }
  27. ],
  28. "repositories": [
  29. {
  30. "type": "git",
  31. "url": "http://github.com/test/test.git"
  32. }
  33. ],
  34. "dependencies": {
  35. "webkit": "1.2",
  36. "ssl": {
  37. "gnutls": ["1.0", "2.0"],
  38. "openssl": "0.9.8"
  39. }
  40. }
  41. }

想要了解更多可以在:package.json 的介绍文档查看。
注:package.json 文件可以自己手动编辑,还可以通过npm init命令进行生成。你可以自己尝试在终端中输入npm init命令来生成一个包含 package.json 文件的包。直接输入npm init --yes跳过回答问题步骤,直接生成默认值的 package.json 文件。此外,我们在 github 上传自己项目的时候,通常是不会把 node_modules 这个文件夹传上去的(太大了),只需要有 package.json 就能通过npm install命令安装所有依赖。

Node.js基础入门 - 图16

包操作

通过命令npm install xxx来安装包。比如:

安装包:

  1. npm install express

更新包:

  1. npm update express

删除包:

  1. npm uninstall express

想要学习更多 npm 的操作可以访问npm 中文文档
npm社区中去查找包,再通过命令npm install 模块名字就可以安装。每个模块的名字全球唯一。

Node.js 模块简介

模块概述

在 JavaScript 中,我们通常把 JavaScript 代码分为几个 js 文件,然后在浏览器中将这些 js 文件合并运行,但是在 Node.js 中,是通过以模块为单位来划分所有功能的。每一个模块为一个 js 文件,每一个模块中定义的全局变量和函数的作用范围也被限定在这个模块之内,只有使用 exports 对象才能传递到外部使用。Node.js 官方提供了很多模块,这些模块分别实现了一种功能,如操作文件及文件系统的模块 fs,构建 http 服务的模块 http,处理文件路径的模块 path 等。当然我们也可以自己编写模块。

模块的使用

在 Node.js 创建模块很简单,比如我们创建一个 myModule.js 的文件,写下如下代码:

  1. function foo() {
  2. console.log("hello syl");
  3. }

这样就创建好了一个模块,但是别的模块如何来访问它呢?我们使用module.exports来导出它。也就是说把 myModule.js 的代码改写成下面这样:

  1. function foo() {
  2. console.log("hello syl");
  3. }
  4. module.exports.foo = foo;

最后我们再建一个 index.js 的文件,使用require()函数来访问上面的模块,输入以下代码:

  1. var hello = require("./myModule.js");
  2. hello.foo();

运行效果为:
Node.js基础入门 - 图17

注:require()加载模块,以 ‘/‘ 为前缀的模块是文件的绝对路径。’./‘ 为前缀的模块是相对于调用require()的文件的,上面的例子中 index.js 和 myModule.js 是在同一个目录下(project 目录)。当没有以 ‘/‘、’./‘ 或 ‘../‘ 开头来表示文件时,这个模块必须是一个核心模块或加载自 node_modules 目录。如果给定的路径不存在,则require()会抛出一个 code 属性为'MODULE_NOT_FOUND'的 Error。

核心模块

核心模块定义在 Node.js 源代码的 lib/ 目录下。require()总是会优先加载核心模块。例如:require('http')始终返回内置的 HTTP 模块,即使有同名文件。

循环

当循环调用require()时,一个模块可能在未完成执行时被返回。比如:

a.js 的代码为:

  1. console.log("a 开始");
  2. exports.done = false;
  3. var b = require("./b.js");
  4. console.log("在 a 中,b.done = %j", b.done);
  5. exports.done = true;
  6. console.log("a 结束");

b.js 的代码为:

  1. console.log("b 开始");
  2. exports.done = false;
  3. var a = require("./a.js");
  4. console.log("在 b 中,a.done = %j", a.done);
  5. exports.done = true;
  6. console.log("b 结束");

main.js 的代码为:

  1. console.log("main 开始");
  2. var a = require("./a.js");
  3. var b = require("./b.js");
  4. console.log("在 main 中,a.done=%j,b.done=%j", a.done, b.done);

运行效果为:
Node.js基础入门 - 图18

也就是说当 main.js 加载 a.js 时,a.js 又加载 b.js。此时,b.js 会尝试去加载 a.js。为了防止无限的循环,会返回一个 a.js 的 exports 对象的未完成的副本给 b.js 模块。然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

module.exports 和 exports 的区别

我们发现每次导出接口成员的时候都通过module.exports.xxx = xxx的方式很麻烦,点儿的太多了。所以,Node.js 为了简化你的操作,专门提供了一个变量:exports 等于module.exports。也就是说在模块中还有这么一句代码:

  1. var exports = module.exports;

我们前面案例中的代码也就可以简写了:

  1. module.exports.foo = foo;
  2. exports.foo = foo; // 这两行代码效果是一样的

但是需要注意的是:就像任何变量,如果一个新的值被赋值给 exports,它就不再绑定到module.exports。我们具体来看个例子:
a.js 的代码:

  1. console.log(module.exports === exports);

Node.js基础入门 - 图19

两者一致,说明我们可以用任意一个来导出内部成员。

b.js 的代码:

  1. exports = {
  2. a: 3,
  3. };
  4. console.log(exports);
  5. console.log(module.exports);
  6. console.log(exports === module.exports);

Node.js基础入门 - 图20

也就是说给 exports 赋值会断开和module.exports之间的引用,同样的给module.exports重新赋值也会断开它们之间的引用。但是最终导出的是module.exports,在上面的例子中我们另外一个文件来用require()加载 b.js 只会得到 {} 而不是 {a:3}。

总结:require()得到的是module.exports导出的值,导出多个成员可以用module.exportsexports,导出单个成员只能用module.exports。如果你实在不好区分,那就全部都使用module.exports也是没问题的。

总结

本节中,我们学习了以下知识点:

  • Node.js 包
  • Node.js 模块

包和模块的概念是 Node.js 的基础,也是学习使用复杂 Node.js 的前提。合理的使用包和模块,会使你的程序变得低冗余,高可读,功能全面。在接下来的章节中,我们会介绍两个常用的 Node.js 模块,以加深理解。

3.Node.js函数

简介

Node.js 大部分模块都由 js 编写,所以函数的语法与 js 基本相同,我们将不再对基础的函数内容进行讲述,而是对一些复杂的函数进行学习。通过本节的学习,你将会对 Node.js 异步编程中大量使用的回调函数产生更深的理解。另外,你还将学习到 ES6 标准新增的一种函数:匿名函数。

知识点

  • Node.js 中的函数
  • 匿名函数
  • Node.js 异步编程

Node.js 中的函数

在 JavaScript 中,一个函数可以作为另一个函数的参数。我们可以先定义一个函数,然后把这个函数作为变量在另一个函数中传递,也可以在传递参数的地方直接定义函数。Node.js 中函数的使用与 Javascript 类似。

  1. function sayHi(value) {
  2. console.log(value);
  3. }
  4. function execute(someFunction, value) {
  5. someFunction(value);
  6. }
  7. execute(sayHi, "hi");

运行结果为:
Node.js基础入门 - 图21

上面的例子我们也可以写成:

  1. function execute(someFunction, value) {
  2. someFunction(value);
  3. }
  4. execute(function (value) {
  5. console.log(value);
  6. }, "hi");

匿名函数

匿名函数就是没有命名的函数。语法为:

  1. function(){
  2. }

箭头函数

ES6 标准新增了一种新的函数,箭头函数表达式的语法比函数表达式更短,并且没有自己的thisargumentssupernew.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。

语法为:

  1. (参数 1, 参数 2, …, 参数 N) => {函数声明}
  2. // 相当于:(参数 1, 参数 2, …, 参数 N) => {return 表达式;}
  3. (参数 1, 参数 2, …, 参数 N) => 表达式(单一)
  4. // 当只有一个参数时,圆括号是可选的
  5. (单一参数) => {函数声明}
  6. 单一参数 => {函数声明}
  7. // 没有参数的函数应该写成一对圆括号
  8. () => {函数声明}

例子:

  1. function(){
  2. console.log('hello syl');
  3. }
  4. // 上面的匿名函数可以用箭头函数改写为下面的
  5. () => console.log('hello syl');

因为我们在 Node.js 中会经常使用到匿名函数,为了缩减代码,我们可以使用箭头函数。一般来说上面语法中的简单使用就可以了,这里还是主要学习 Node.js 的操作,不过多的去深究箭头函数的其他用法,感兴趣的可以自行查阅MDN|箭头函数
在 Node.js 中,我们经常会使用到匿名函数,在后续的课程中,我们也经常会见到用到匿名函数。

Node.js 异步编程

Node.js 异步编程的直接体现就是回调。回调函数在完成任务后就会被调用,Node.js 使用了大量的回调函数,Node.js 所有 API 都支持回调函数。回调函数一般作为函数的最后一个参数出现。

阻塞代码实例

先创建一个 syl.txt 的文件,里面随便输入一段文本内容,比如:hello syl!。

创建 a.js 代码为:

  1. var fs = require("fs");
  2. var data = fs.readFileSync("syl.txt");
  3. console.log(data.toString());
  4. console.log("程序执行完毕!");

运行结果为:
Node.js基础入门 - 图22

非阻塞代码实例

把前面 a.js 的代码改为:

  1. var fs = require("fs");
  2. fs.readFile("syl.txt", function (err, data) {
  3. if (err) return console.error(err);
  4. console.log(data.toString());
  5. });
  6. console.log("程序执行完毕!");

运行结果为:
Node.js基础入门 - 图23

从这两个实例中我们可以初步地体验到阻塞和非阻塞的不同之处。第一个实例在文件读取完后才执行完程序。第二个实例我们不需要等待文件读取完,这样就可以在读取文件时同时执行接下来的代码,大大提高了程序的性能。我们将在后续 fs-文件系统章节中更加深入的去了解使用回调函数。

函数传递是如何让 HTTP 服务器工作的

我们再来看看我们创建第一个应用的案例:

  1. //加载 http 模块
  2. var http = require("http");
  3. http
  4. .createServer(function (request, response) {
  5. // 发送 HTTP 头部
  6. // HTTP 状态值: 200 : OK
  7. // 内容类型: text/plain
  8. response.writeHead(200, { "Content-Type": "text/plain" });
  9. // 发送响应数据 "Hello World"
  10. response.end("Hello World\n");
  11. })
  12. .listen(8080);
  13. // 终端打印如下信息
  14. console.log("Server running at http://127.0.0.1:8080/");

上面的代码其实就是在 http 模块的creatServer()函数中插入了一个匿名函数,我们还可以写成如下所示的代码:

  1. //加载 http 模块
  2. var http = require("http");
  3. function foo(request, response) {
  4. // 发送 HTTP 头部
  5. // HTTP 状态值: 200 : OK
  6. // 内容类型: text/plain
  7. response.writeHead(200, { "Content-Type": "text/plain" });
  8. // 发送响应数据 "Hello World"
  9. response.end("Hello World\n");
  10. }
  11. http.createServer(foo).listen(8080);
  12. // 终端打印如下信息
  13. console.log("Server running at http://127.0.0.1:8080/");

总结

本节主要介绍了 Node.js 大量使用的函数类型,主要内容如下:

  • Node.js 中的函数
  • 匿名函数
  • Node.js 异步编程

回调函数是函数中较难理解的部分,本节中所涉及到的实验代码,需要动手编写调试,加深记忆。

4.Node.js时间

简介

Node.js 异步编程是基于一些惯用的异步事件完成的。本节中,将会从 Node.js 的事件、监听器的整个生命周期这两个角度,对 Node.js 作进一步讲解。

知识点

  • 事件概述
  • EventEmitter
  • 监听器相关操作
  • error 事件

事件

概述

大多数 Node.js 核心 API 构建于惯用的异步事件驱动架构,其中某些类型的对象(又称触发器,Emitter)会触发命名事件来调用函数(又称监听器,Listener)。比如:fs.readStream打开文件时会发出一个事件。可以通过require("events");获得event模块。通常,事件名采用“小驼峰式”(即第一个单词全小写,后面的单词首字母大写,其它字母小写)命名方式。

EventEmitter

所有能触发事件的对象都是EventEmitter类的实例。这些对象有一个eventEmitter.on()函数,用于将一个或多个函数绑定到命名事件上。当EventEmitter对象触发一个事件时,所有绑定在该事件上的函数都会被同步地调用。

EventEmitter 类获取

  1. // 引入 events 模块
  2. var events = require("events");
  3. // 创建 eventEmitter 对象
  4. var eventEmitter = new events.EventEmitter();

添加监听器

emitter.on(eventName, listener)

使用emitter.on(eventName, listener)方法为指定事件注册一个监听器。添加listener函数到名为eventName的事件的监听器数组的末尾。不会检查listener是否已被添加。多次调用并传入相同的eventNamelistener会导致listener会被添加多次。

参数说明:

  • eventName:事件名称,string 类型。
  • listener:回调函数。

例子:

  1. // 引入 events 模块
  2. var events = require("events");
  3. // 创建 emitter 对象
  4. var emitter = new events.EventEmitter();
  5. // 为 connection 事件注册一个监听器
  6. emitter.on("connection", function () {
  7. console.log("已连接");
  8. });
  9. // 一秒后调用监听器
  10. setTimeout(function () {
  11. emitter.emit("connection");
  12. }, 1000);

运行效果如下所示:
Node.js基础入门 - 图24

默认情况下,事件监听器会按照添加的顺序依次调用。emitter.prependListener()方法可用于将事件监听器添加到监听器数组的开头。比如:

  1. // 引入 events 模块
  2. var events = require("events");
  3. // 创建 server 对象
  4. var emitter = new events.EventEmitter();
  5. // 为 connection 事件注册一个监听器
  6. emitter.on("connection", function () {
  7. console.log("我是a");
  8. });
  9. // 箭头函数,有兴趣的可以自行了解一下
  10. emitter.prependListener("connection", () => console.log("我是b"));
  11. // 一秒后调用监听器
  12. setTimeout(function () {
  13. emitter.emit("connection");
  14. }, 1000);

运行效果如下所示:
Node.js基础入门 - 图25

emitter.addListener(eventName, listener)

emitter.addListener(eventName, listener)emitter.on(eventName, listener)的别名。

调用监听器

使用emitter.emit(eventName[, ...args])按照监听器注册的顺序,同步地调用每个注册到名为 eventName 的事件的监听器,并传入提供的参数。如果事件有注册监听返回 true,否则返回 false。

参数说明:

  • eventName :事件名称
  • args:传递的参数,多个,类型为任意。

例子:

  1. // 引入 events 模块
  2. var events = require("events");
  3. // 创建 emitter 对象
  4. var emitter = new events.EventEmitter();
  5. // 定义一个回调函数
  6. var callback1 = function (arg1, arg2) {
  7. console.log("hello world", arg1, arg2);
  8. };
  9. var callback2 = function (arg3, arg4) {
  10. console.log("hello stranger", arg3, arg4);
  11. };
  12. // 为 connection 事件注册监听器
  13. emitter.on("connection", callback1);
  14. emitter.on("connection", callback2);
  15. // 调用监听器
  16. emitter.emit("connection", "愿善良的人", "都能被温柔以待");

运行结果如下:
Node.js基础入门 - 图26

监听器在绑定后,每当命名事件被触发时,就会调用绑定的回调函数,可重复触发,如果我们想实现一个只执行一次的监听器该怎么做呢?

只执行一次的监听器

当使用eventEmitter.on(eventName, listener)注册监听器时,监听器会在每次触发命名事件时被调用。比如:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 为 connection 事件注册一个监听器
var n = 0;
emitter.on("connection", function () {
  ++n;
  console.log("调用第" + n + "次");
});
// 调用监听器
emitter.emit("connection");
emitter.emit("connection");
emitter.emit("connection");
emitter.emit("connection");

运行结果为:
Node.js基础入门 - 图27

使用eventEmitter.once(eventName, listener)可以注册最多可调用一次的监听器。当事件被触发时,监听器会被注销,然后再调用。比如:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 为 connection 事件注册一个监听器
var n = 0;
emitter.once("connection", function () {
  ++n;
  console.log("调用第" + n + "次");
});
// 调用监听器
emitter.emit("connection");
emitter.emit("connection");
emitter.emit("connection");
emitter.emit("connection");

运行结果为:
Node.js基础入门 - 图28

默认情况下,事件监听器会按照添加的顺序依次调用。emitter.prependOnceListener()方法可用于将事件监听器添加到监听器数组的开头。用法与我们前面所学的emitter.prependListener()方法一致,区别在于这个方法注册的监听器最多只能调用一次,大家可以自行尝试写一下。

移除监听器

到目前为止,我们已经走完了过半的监听器的生命周期,接下来我们将学习如何移除监听器。

emitter.removeListener(eventName, listener)

使用emitter.removeListener(eventName, listener)移除监听器。

参数说明:

  • eventName 事件名称。
  • listener 监听器也就是回调函数名称。

例子:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 定义一个回调函数
var callback = function () {
  console.log("syl");
};
// 为 connection 事件注册一个监听器
emitter.on("connection", callback);
// 为 connection 事件移除监听器
emitter.removeListener("connection", callback);
// 调用监听器
emitter.emit("connection");

运行结果为:
Node.js基础入门 - 图29

注:removeListener()最多只会从监听器数组中移除一个监听器。我们可以多次调用removeListener()的方式来一个个的移除我们需要移除掉的监听器。

一旦事件被触发,所有绑定到该事件的监听器都会按顺序依次调用。也就是说在事件触发之后、且最后一个监听器执行完成之前,removeListener()removeAllListeners()不会从emit()中移除它们。

例子:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 定义回调函数
var callback1 = function () {
  console.log("我是1");
  emitter.removeListener("connection", callback2);
};
var callback2 = function () {
  console.log("我是2");
};
// 为 connection 事件注册监听器
emitter.on("connection", callback1);
emitter.on("connection", callback2);
// 第一次调用监听器,callback1 移除了监听器 callback2,但它依然会被调用。触发时内部的监听器数组为 [callback1, callback2]
emitter.emit("connection");
// 第二次调用监听器,此时 callback2 已经被移除了。内部的监听器数组为 [callback1]
emitter.emit("connection");

运行结果为:
Node.js基础入门 - 图30

emitter.off(eventName, listener)

emitter.off(eventName, listener)emitter.removeListener()的别名。

emitter.removeAllListeners([eventName])

使用emitter.removeAllListeners([eventName])移除全部监听器或指定的eventName事件的监听器。

例子:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 定义回调函数
var callback1 = function () {
  console.log("我是1");
};
var callback2 = function () {
  console.log("我是2");
};
// 为 connection 事件注册监听器
emitter.on("connection", callback1);
emitter.on("connection", callback2);
// 移除 connection 事件的所有监听器
emitter.removeAllListeners("connection");
// 调用监听器
emitter.emit("connection");

运行结果为:
Node.js基础入门 - 图31

设置监听器最大绑定数

emitter.setMaxListeners(n)

使用emitter.setMaxListeners(n)设置同一事件的监听器最大绑定数。默认情况下,如果为特定事件添加了超过 10 个监听器,则EventEmitter会打印一个警告,这有助于我们发现内存泄露。显然实际编码中并不是所有的事件都要限制 10 个监听器。emitter.setMaxListeners()方法可以为指定的EventEmitter实例修改限制。当值设为 Infinity(或 0)表示不限制监听器的数量。

查看事件绑定的监听器个数

emitter.listenerCount(eventName)

使用emitter.listenerCount(eventName)查看事件绑定的监听器个数。

例子:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 定义回调函数
var callback1 = function () {
  console.log("我是1");
};
var callback2 = function () {
  console.log("我是2");
};
// 为 connection 事件注册监听器
emitter.on("connection", callback1);
emitter.on("connection", callback2);
// 查看 connection 事件绑定的监听器个数
var num = emitter.listenerCount("connection");
console.log(num);

运行结果为:
Node.js基础入门 - 图32

error 事件

EventEmitter实例出错时,应该触发 ‘error’ 事件。

如果没有为 ‘error’ 事件注册监听器,则当 ‘error’ 事件触发时,会抛出错误、打印堆栈跟踪、并退出 Node.js 进程。比如:

var events = require("events");
var emitter = new events.EventEmitter();
emitter.emit("error");

运行结果为:
Node.js基础入门 - 图33

通常我们要为会触发 error 事件的对象设置监听器,避免遇到错误后整个程序崩溃。比如:

// 引入 events 模块
var events = require("events");
// 创建 emitter 对象
var emitter = new events.EventEmitter();
// 设置监听器
emitter.on("error", (err) => {
  console.error("错误信息");
});
emitter.emit("error");

运行结果为:
Node.js基础入门 - 图34

总结

在本节中,我们学习了以下内容

  • 事件概述
  • EventEmitter
  • 监听器相关操作
  • error 事件

Node.js 的异步编程中事件监听器的编程模式占据了很大一部分。事件和监听器的概念以及相关操作是本节的核心内容,需要编码练习,加深理解。

5.Node.js http 模块 和 fs 模块

简介

在前面的学习中,我们掌握了 Node.js 的基础概念以及包和模块的概念,在本节中,我们将会学习两个常用的 Node.js 的模块:http 和 fs。

知识点

  • Node.js http
  • Node.js fs

Node.js http

概述

Node.js 提供了 http 模块,http 模块主要用于搭建 HTTP 服务端和客户端,要使用 HTTP 服务器和客户端功能必须调用 http 模块,代码如下:

// 引入 http 模块
var http = require("http");

创建 http server

我们首先来回顾一下我们 Node.js 简介章节中创建的第一个应用的代码:

// 加载 http 模块
var http = require("http");
// 创建服务器
http
  .createServer(function (request, response) {
    // 发送 HTTP 头部
    // HTTP 状态值: 200 : OK
    // 内容类型: text/plain
    response.writeHead(200, { "Content-Type": "text/plain" });
    // 发送响应数据 "Hello World"
    response.end("Hello World\n");
  })
  .listen(8080);
// 终端打印如下信息
console.log("Server running at http://127.0.0.1:8080/");

输入:

node server.js

启动 Web 服务(注意:只能使用 8080 端口,即默认端口,使用其它端口无法访问)。然后点击右侧工具中的 Web 服务按钮,即可访问 Web 服务。
最终的运行效果如下所示:

Node.js基础入门 - 图35

创建服务器

创建服务器使用如下代码:

http.createServer([requestListener]);

当然该方法属于 http 模块,所以我们要先引入 http 模块,requestListener是一个请求函数,也就是我们上面所写的:

function(request, response){
    // 函数内容
}

requestListener请求函数是一个自动添加到request事件的函数(request事件每次有请求时都会触发,初期学习我们清楚有这个东西就行,不过多的去追究)。函数传递有两个参数:request请求对象 和response响应对象。我们调用request请求对象的属性和方法就可以拿到所有 HTTP 请求的信息,我们操作response响应对象的方法,就可以把 HTTP 响应返回给浏览器。
response对象常用的方法有:

response.writeHead(statusCode[, statusMessage][, headers])。表示向请求发送响应头。

参数说明:

1. statusCode:状态码,是一个 3 位 HTTP 状态码,如 404 表示网页未找到,200 表示正常。
2. statusMessage:可选的,可以将用户可读的`statusMessage`作为第二个参数。
3. headers:响应头。也就是设置 Content-Type 的值,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件。常用值有:(1)text/html:HTML 格式(2)text/plain:纯文本格式(3)application/x-www-form-urlencoded:数据被编码为名称/值对,这是标准的编码格式。其余的可以自行百度了解,比如[Content-Type 对照表](http://tool.oschina.net/commons)。

比如:

response.writeHead(200, { "Content-Type": "text/plain;charset=UTF-8" });
  1. 注:此方法只能在消息上调用一次,并且必须在调用response.end()之前调用它。
  2. response.write()发送一块响应主体,也就是说用来给客户端发送响应数据。可以直接写文本信息,也可以写我们的 html 代码,注意要设置 Content-Type 的值。write 可以使用多次,但是最后一定要使用 end 来结束响应,否则客户端会一直等待。
  3. response.end()此方法向服务器发出信号,表示已发送所有响应头和主体,该服务器应该视为此消息完成。必须在每个响应上调用方法response.end()

例子:

// 加载 http 模块
var http = require("http");
// 创建服务器
http
  .createServer(function (request, response) {
    // 发送 HTTP 头部
    // HTTP 状态值: 200 : OK
    // 内容类型: text/html
    response.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
    // 发送响应数据 'hello syl'
    response.write("hello syl");
    // 发送数据 hello world 并且字体为 h1 格式
    response.write("<h1>hello world</h1>");
    // 结束
    response.end();
    // 上面的三行代码也可以直接写成 response.end('hello syl <h1>hello world</h1>');
  })
  .listen(8080);
// 终端打印如下信息
console.log("Server running at http://127.0.0.1:8080/");

最终的运行效果为:
Node.js基础入门 - 图36

request 对象:

  1. request.url获取请求路径,获取到的是端口号之后的那一部分路径,也就是说所有的 url 都是以 / 开头的,判断路径处理响应。
  2. request.socket.localAddress获取 ip 地址。
  3. request.socket.remotePort获取源端口。

比如:

// 加载 http 模块
var http = require("http");
// 1. 创建 Server
var server = http.createServer();
// 2. 监听 request 请求事件,设置请求处理函数
server.on("request", function (req, res) {
  console.log("收到请求了,请求路径是:" + req.url);
  console.log(
    "请求我的客户端的地址是:",
    req.socket.remoteAddress,
    req.socket.remotePort
  );
  var url = req.url;
  res.writeHead(200, { "Content-Type": "text/html;charset=UTF-8" });
  if (url === "/") {
    res.end("<h1>Index page</h1>");
  } else if (url === "/login") {
    res.end("<h1>Login page</h1>");
  } else {
    res.end("404 Not Found.");
  }
});
// 3. 绑定端口号,启动服务
server.listen(8080, function () {
  console.log("服务器启动成功,可以访问了。。。");
});

运行效果为:
Node.js基础入门 - 图37

也就是说我们现在已经能够对不同的 url 做出相应的反应。

创建一个简单的网站

首先新建两个 html 文件,分别叫 index.html 和 register.html 案例代码很简单,大家可以自行尝试美化一下自己的 html 页面。

index.html 的代码为:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>42摄氏度:成都创出100年气象史上高温新纪录</h1>
    2017年07月07日 <br />
    10:43:96 来源: <strong>新华网</strong>
    <hr />
    <p>
      <em
        >新华网成都7月7日电(记者李小花)7日7时36分至47分这一时间段,成都气象观测站测得当日最高温达42摄氏度。这是成都有气象记录以来100年的高温新纪录,打破了此前1934年创下的40.2摄氏度的历史极值。</em
      >
    </p>
    <p>
      <ins>成都已经连续2天发出了最高等级的红色高温警报。</ins
      >成都中心气象台首席服务官李刚说,今年副热带高压强度特别强,对成都地区的控制“实在太稳定了”,整个7月份基本上都处在它的势力范围之内。6日成都已出现了气象史上7月份“第四高”的高温值,这使得7日的“基础”气温就很高,超过了30摄氏度,然后不断地升温。此外,6日白天风小,又吹的是西南风,特别是在中午之后这一个最易出现高温的时段,光照又比较强,所以气温“直线飙升”,一举冲破历史极值,出现了“创纪录”的极端酷暑天。
    </p>
    <p>
      在成都历史上,出现40摄氏度以上极端高温的几率并不大。根据相关资料,中心城区观测站
      <font color="red" size="5">100年来仅出现了5次记录</font
      >,除了这一次的新纪录,还有就是1934年7月12日的40.2摄氏度;1934年8月25日、2009年7月20日、2010年8月13日的40摄氏度。
    </p>
    <p>
      <del
        >由于气温实在太高,成都6日下午不少地区出现了热对流天气。气象台说,首先是双流地区,下起了热雷雨。到15时05分,全市大部分地区出现了分散性的雷电活动和热雷雨,中心城区徐家汇等地都响起了隆隆的雷声。</del
      >
    </p>
  </body>
</html>

register.html 的代码为:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <form action="" method="post">
      <fieldset>
        <legend>注册</legend>
        <!-- 文本输入框 -->
        用户名:<input
          type="text"
          maxlength="4"
          name="username"
          value="实验楼"
        />
        <br />
        <br />
        <!--邮箱-->
        邮箱:<input type="email" id="email" name="email" multiple />
        <br />
        <br />
        <!--电话号码-->
        电话号码:<input type="tel" id="tel" name="tel" />
        <br />
        <br />
        <!-- 密码输入框 -->
        密码:<input type="password" name="pwd" />
        <br />
        <br />
        <!-- 单选框 -->
        <input type="radio" name="gender" checked="checked" />男
        <input type="radio" name="gender" />女 <br /><br />
        <!-- 下拉列表 -->
        省(市):
        <select>
          <option>北京</option>
          <option>上海</option>
          <option>广东</option>
          <option selected="selected">深圳</option>
        </select>
        <!-- 下拉列表多选 -->
        <select multiple="multiple">
          <option>北京</option>
          <option>上海</option>
          <option>广东</option>
          <option selected="selected">深圳</option>
        </select>
        市(区):
        <select>
          <!-- 下拉列表信息分组 -->
          <optgroup label="北京市">
            <option>东城区</option>
            <option>西城区</option>
            <option>朝阳区</option>
            <option>丰台区</option>
          </optgroup>
          <optgroup label="上海市">
            <option>黄浦区</option>
            <option>徐汇区</option>
            <option>长宁区</option>
            <option>静安区</option>
          </optgroup>
        </select>
        <br /><br />
        <!-- 多选框 -->
        <input type="checkbox" checked="checked" />吃饭
        <input type="checkbox" checked="checked" />睡觉
        <input type="checkbox" checked="checked" />打豆豆
        <!-- 多行文本框 -->
        <textarea cols="130" rows="10"></textarea><br /><br />
        <label for="myColor">你最爱的颜色是什么?</label>
        <input type="text" name="myColor" id="myColor" list="mySuggestion" />
        <datalist id="mySuggestion">
          <option>black</option>
          <option>blue</option>
          <option>green</option>
          <option>red</option>
          <option>black</option>
          <option>yellow</option>
        </datalist>
        <br /><br />
        <!-- 文件上传控件 -->
        <input type="file" />
        <br /><br />
        <!-- 文件提交按钮 -->
        <input type="submit" />
        <!-- 普通按钮 -->
        <!-- <input type="button" value="普通按钮"> -->
        <!-- 图片按钮 -->
        <!--<input type="image" src="按钮.jpg">-->
        <!-- 重置按钮 -->
        <input type="reset" />
      </fieldset>
    </form>
  </body>
</html>

然后我们新建一个 server.js 写我们的服务器代码,代码如下所示:

//  结合 fs 发送文件中的数据
//  引入 http 和 fs 模块
var http = require("http");
var fs = require("fs");
// 创建服务器
var server = http.createServer();
// 监听 request 请求事件,设置请求处理函数
server.on("request", function (req, res) {
  // 处理 url
  var url = req.url;
  if (url === "/") {
    // 下面注释代码的写法显然是不合理的
    // res.end('<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><title>Document</title></head><body><h1>首页</h1></body>/html>')
    res.setHeader("Content-Type", "text/plain");
    // 我们要发送的还是在文件中的内容
    fs.readFile("./index.html", function (err, data) {
      if (err) {
        res.end("文件读取失败,请稍后重试!");
      } else {
        // data 默认是二进制数据,可以通过 .toString 转为咱们能识别的字符串
        // res.end() 支持两种数据类型,一种是二进制,一种是字符串
        res.writeHead(200, {
          "Content-Type": "text/html",
        });
        res.end(data);
      }
    });
  } else if (url === "/register") {
    // url:统一资源定位符
    // 一个 url 最终其实是要对应到一个资源的
    fs.readFile("./register.html", function (err, data) {
      if (err) {
        res.end("文件读取失败,请稍后重试!");
      } else {
        //setHeader也是设置响应头,它们将与传递给 response.writeHead() 的任何响应头合并,其中 response.writeHead() 的响应头优先。
        res.setHeader("Content-Type", "text/html");
        res.end(data);
      }
    });
  } else {
    res.end("<h1>404 Not Found.</h1>");
  }
});
server.listen(8080, function () {
  console.log("Server is running...");
});

来看看运行效果:
Node.js基础入门 - 图38

也就是说我们可以通过更改请求路径来响应出不同的 html 页面,当然这是我们自己写的,用户的话可能是点击一个链接,比如:

<a href="/user/logout">注销</a>

然后就和我们上面设置的一样,写个判断语句,当请求路径是某个路径的时候,响应一个相应的页面出来。大家可以自行尝试一下,写一个导航栏页面,能够跳转到其他页面,并且能够跳转回来。

Node.js fs

Node.js 中的 fs 模块供了一个 API,用于以接近标准 POSIX 函数的方式与文件系统进行交互。导入文件系统模块的语法如下:

var fs = require("fs");

所有文件系统操作都具有同步和异步的形式。异步方法中回调函数的第一个参数总是留给异常参数(exception),如果方法成功完成,那么这个参数为 null 或者 undefined。
在 Node.js 中绝大部分需要在服务器运行期反复执行业务逻辑的代码,必须使用异步代码。否则,同步代码在执行时期,服务器将停止响应,因为 Node.js 是单线程。

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

fs 模块的操作有很多,我们这里讲解一些比较常用的。有兴趣的可以自行查阅Node.js 中文官方文档

打开文件

异步打开文件的语法格式为:

fs.open(path, flags[, mode], callback);

参数说明:

  • path:文件的路径
  • flags:文件打开的行为。
  • mode:设置文件模式(权限),文件创建默认权限为 0o666(可读写)。mode 设置文件模式(权限和粘滞位),但仅限于创建文件的情况。在 Windows 上,只能操作写权限。
  • callback:回调函数,带有两个参数如:callback(err, fd)。

flags 参数可以是以下值:

  • ‘a’ - 打开文件用于追加。如果文件不存在,则创建该文件。
  • ‘ax’ - 与 ‘a’ 相似,但如果路径存在则失败。
  • ‘a+’ - 打开文件用于读取和追加。如果文件不存在,则创建该文件。
  • ‘ax+’ - 与 ‘a+’ 相似,但如果路径存在则失败。
  • ‘as’ - 以同步模式打开文件用于追加。如果文件不存在,则创建该文件。
  • ‘as+’ - 以同步模式打开文件用于读取和追加。如果文件不存在,则创建该文件。
  • ‘r’ - 打开文件用于读取。如果文件不存在,则会发生异常。
  • ‘r+’ - 打开文件用于读取和写入。如果文件不存在,则会发生异常。
  • ‘rs+’ - 以同步模式打开文件用于读取和写入。指示操作系统绕开本地文件系统缓存。这对于在 NFS 挂载上打开文件非常有用,因为它允许跳过可能过时的本地缓存。它对 I/O 性能有非常实际的影响,因此除非需要,否则不建议使用此标志。这不会将fs.open()fsPromises.open()转换为同步的阻塞调用。如果需要同步操作,则应使用fs.openSync()之类的操作。
  • ‘w’ - 打开文件用于写入。创建文件(如果它不存在)或截断文件(如果存在)。
  • ‘wx’ - 与 ‘w’ 相似,但如果路径存在则失败。
  • ‘w+’ - 打开文件用于读取和写入。创建文件(如果它不存在)或截断文件(如果存在)。
  • ‘wx+’ - 与 ‘w+’ 相似,但如果路径存在则失败。

例子:

新建一个 input.txt 的文件,不写任何内容,然后创建 file.js 文件打开 input.txt 文件进行读写,代码如下:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("input.txt", "r+", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
});

运行结果为:
Node.js基础入门 - 图39

同步打开文件的语法格式为:

fs.openSync(path, flags[, mode])

注:参数和用法参照异步fs.open()。在 Node.js 中我们大多是用异步的方式,因此对于同步的用法不做过多的讲解。

关闭文件

异步关闭文件的语法格式为:

fs.close(fd, callback);

参数说明:

  • fd:通过fs.open()方法返回的文件描述符。
  • callback:回调函数,除了可能的异常,完成回调没有其他参数。

新建一个 test.txt 的文件,内容随意输入也可以不输入,再新建一个 closeFile.js 的文件写入以下代码:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("test.txt", "r+", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
  // 异步关闭文件
  fs.close(fd, function (err) {
    if (err) {
      console.log(err);
    }
    console.log("文件关闭成功");
  });
});

运行结果为:
Node.js基础入门 - 图40

使用 fs.read 和 fs.write 读写文件

使用fs.read()fs.write()读写文件需要使用fs.open()打开文件和fs.close()关闭文件。

使用 fs.read 读取文件

异步读取文件的语法格式为:

fs.read(fd, buffer, offset, length, position, callback);

参数说明:

  • fd:通过fs.open()方法返回的文件描述符。
  • buffer:是数据写入的缓冲区。
  • offset:是缓冲区中开始写入的偏移量。一般它的值我们写为 0。
  • length:是一个整数,指定要读取的字节数。
  • position:指定从文件中开始读取的位置。如果 position 为 null,则从当前文件位置读取数据,并更新文件位置。
  • callback:回调函数,有三个参数(err, bytesRead, buffer)。err 为错误信息,bytesRead 表示读取的字节数,buffer 为缓冲区对象。

例子:

新建一个 test.txt 的文件写入:hello syl。再新建一个 read.js 的文件,写上如下的代码:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("test.txt", "r+", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
  console.log("准备读取文件:");
  // 创建一个大小为 1024 字节的缓存区
  var buf = Buffer.alloc(1024);
  // 异步读取文件
  fs.read(fd, buf, 0, buf.length, 0, function (err, bytes, buf) {
    if (err) {
      console.log(err);
    }
    console.log(bytes + "字节被读取");
    // 仅输出读取的字节
    if (bytes > 0) {
      console.log(buf.slice(0, bytes).toString());
    }
    // 异步关闭文件
    fs.close(fd, function (err) {
      if (err) {
        console.log(err);
      }
      console.log("文件关闭成功");
    });
  });
});

运行结果为:
Node.js基础入门 - 图41

使用 fs.write 写入文件

异步写入文件的语法格式为:

fs.write(fd, buffer, offset, length, position, callback);

参数说明:

  • fd:从指定的文件写入数据。
  • buffer:是数据写入的缓冲区。
  • offset:指定要写入的 buffer 部分。
  • length:是一个整数,指定要写入的字节数。
  • position 指定应该写入此数据的文件开头的偏移量。如果typeof position !== 'number',则从当前位置写入数据。
  • callback:回调有三个参数(err, bytesWritten, buffer),其中bytesWritten指定从buffer写入的字节数。

例子:

在前面例子的基础上,我们新建一个 write.js 的文件,写入以下代码:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("./test.txt", "a", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
  console.log("准备写入文件:");
  // 新写入内容为 hello world
  var buffer = Buffer.from(new String(" hello world"));
  // 异步写入文件
  fs.write(fd, buffer, 0, 12, 0, function (err, bytes, buffer) {
    if (err) {
      throw err;
    }
    console.log("写入成功");
    // 打印出 buffer 中存入的数据
    console.log(bytes + "字节被写入");
    console.log(buffer.slice(0, bytes).toString());
    // 异步关闭文件
    fs.close(fd, function (err) {
      if (err) {
        console.log(err);
      }
      console.log("文件关闭成功");
    });
  });
});

运行结果为:
Node.js基础入门 - 图42

还有一种语法是:

fs.write(fd, string[, position[, encoding]], callback);

参数说明:

  • fd:从指定的文件写入数据。
  • string:写入的数据,如果不是字符串,则该值将被强制转换为字符串。
  • position 指定应该写入此数据的文件开头的偏移量。如果typeof position !== 'number',则从当前位置写入数据。
  • encoding:指定字符串的编码,默认为 ‘utf8’。
  • callback:回调有三个参数(err, written, string),其中 written 指定字符串中已写入文件的字节数。写入的字节数与字符串的字符数是不同的。

例子:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("test.txt", "a", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
  console.log("准备写入文件:");
  // 新写入数据为 ohaiyo
  var data = " ohaiyo";
  // 异步写入文件
  fs.write(fd, data, 0, "utf-8", function (err, bytes, buffer) {
    if (err) {
      return console.error(err);
    }
    console.log(bytes + "字节被写入");
    console.log(buffer);
    // 异步关闭文件
    fs.close(fd, function (err) {
      if (err) {
        console.log(err);
      }
      console.log("文件关闭成功");
    });
  });
});

运行结果为:
Node.js基础入门 - 图43

fs.readfs.write需要结合fs.open得到文件句柄来使用,我们再介绍另外一种读写方式。

readFile 读取文件

异步读取文件的语法格式为:

fs.readFile(path, [options], callback);

参数说明:

  • path:文件名或文件描述符。
  • options:该参数是一个对象,包含{encoding, flag}。encoding 默认值为 null,flag 默认值为 ‘r’。
  • callback:回调函数。

例子:

新建一个 test.txt 的文件,文件的内容为:

hello syl hello world

新建一个 readFile.js 的文件,写入如下代码:

// 引入 fs 模块
var fs = require("fs");
// 读取文件
fs.readFile("./test.txt", function (err, data) {
  // 读取文件失败/错误
  if (err) {
    throw err;
  }
  // 读取文件成功
  console.log(data);
});

运行结果为:
Node.js基础入门 - 图44

运行结果显示的是原始二进制数据在缓冲区中的内容。要显示文件内容可以使用toString()或者设置输出编码,readFile.js 可以改成这样:

使用toString()方法:

// 引入 fs 模块
var fs = require("fs");
// 读取文件
fs.readFile("./test.txt", function (err, data) {
  // 读取文件失败/错误
  if (err) {
    throw err;
  }
  // 读取文件成功
  console.log(data.toString());
});

运行结果为:
Node.js基础入门 - 图45

设置输出编码:

// 引入 fs 模块
var fs = require("fs");
// 读取文件
fs.readFile("./test.txt", "utf-8", function (err, data) {
  // 读取文件失败/错误
  if (err) {
    throw err;
  }
  // 读取文件成功
  console.log(data);
});

运行结果为:
Node.js基础入门 - 图46

fs.readFileSync(filename, [options])readFile的同步方法。

writeFile 写入文件

异步写入文件的语法格式为:

fs.writeFile(file, data, [options], callback);

参数说明:

  • file:文件名或文件描述符。
  • data:要写入文件的数据,可以是 String(字符串)或 Buffer(缓冲)对象。
  • options:该参数是一个对象,包含{encoding, mode, flag}。encoding 默认值为:’utf8’,mode 默认值为 0o666,flag 默认为 ‘w’。
  • callback:回调函数。

例子:

新建一个 writeFile.js 的文件,写入如下代码:

// 引入 f s模块
var fs = require("fs");
// 写入文件内容(如果文件不存在会创建一个文件)
// 写入时会先清空文件
fs.writeFile("./test.txt", "我是新写入的内容", function (err) {
  if (err) {
    throw err;
  }
  console.log("Saved.");
  // 写入成功后读取测试
  fs.readFile("./test.txt", "utf-8", function (err, data) {
    if (err) {
      throw err;
    }
    console.log(data);
  });
});

运行结果为:
Node.js基础入门 - 图47

我们可以通过设置 flag 的值,来改变默认的写入方式,比如设置为 ‘a’,追加数据到文件中。新建一个 testFlag.js 代码如下:

// 引入 fs 模块
var fs = require("fs");
// 写入文件内容(如果文件不存在会创建一个文件)
// 传递了追加参数 { 'flag': 'a' }
fs.writeFile("./test.txt", "我是新加的内容", { flag: "a" }, function (err) {
  if (err) {
    throw err;
  }
  console.log("Saved.");
  // 写入成功后读取测试
  fs.readFile("./test.txt", "utf-8", function (err, data) {
    if (err) {
      throw err;
    }
    console.log(data);
  });
});

运行结果为:
Node.js基础入门 - 图48

注:异步追加内容还有一个专门的方法叫fs.appendFile。感兴趣的可以自行了解。

获取文件信息

异步获取文件信息的格式为:

fs.stat(path, callback);

参数说明:

  • path:文件路径。
  • callback:回调函数,带有两个参数如:(err, stats),stats 是fs.Stats对象。如果出现错误,则 err.code 将是常见系统错误之一。

不建议在调用fs.open()fs.readFile()fs.writeFile()之前使用fs.stat()检查文件是否存在。而是,应该直接打开、读取或写入文件,并在文件不可用时处理引发的错误。

fs.stat(path)执行后,会将 stats 类的实例返回给其回调函数。可以通过 stats 类中的提供方法判断文件的相关属性。例如判断是否为文件:

例子:

var fs = require("fs");
fs.stat("/home/project/fs.js", function (err, stats) {
  console.log(stats.isFile()); // true
});

运行效果为:
Node.js基础入门 - 图49

一般这个用的很少,所以这里简单的提一下。想要了解更多的请查看Node.js fs.Stats 类

截取文件

异步截取文件的格式为:

fs.ftruncate(fd[, len], callback);

参数说明:

  • fd:通过fs.open()方法返回的文件描述符。
  • len:文件内容截取的长度,默认为 0。
  • callback:除了可能的异常,完成回调没有其他参数。

例子:

新建 test.txt 的文件写入:hello syl hello world。新建 ftr.js 写入以下代码:

// 引入 fs 模块
var fs = require("fs");
// 异步打开文件
fs.open("test.txt", "r+", function (err, fd) {
  if (err) {
    return console.error(err);
  }
  console.log("文件打开成功!");
  console.log("截取6字节内的文件内容,超出部分将被去除。");
  // 截取文件
  var buf = Buffer.alloc(1024);
  fs.ftruncate(fd, 6, function (err) {
    if (err) {
      console.log(err);
    }
    console.log("文件截取成功。");
    console.log("读取相同的文件");
    fs.read(fd, buf, 0, buf.length, 0, function (err, bytes) {
      if (err) {
        console.log(err);
      }
      // 仅输出读取的字节
      if (bytes > 0) {
        console.log(buf.slice(0, bytes).toString());
      }
      // 关闭文件
      fs.close(fd, function (err) {
        if (err) {
          console.log(err);
        }
        console.log("文件关闭成功!");
      });
    });
  });
});

运行结果为:
Node.js基础入门 - 图50

删除文件

异步删除文件的语法格式为:

fs.unlink(path, callback);

参数说明:

  • path:文件路径。
  • callback: 除了可能的异常,完成回调没有其他参数。

例子:

新建一个 test.txt 的文件,再新建一个 fsU.js 文件,写入以下代码:

// 引入 fs 模块
var fs = require("fs");
// 删除 test.txt 文件
fs.unlink("test.txt", function (err) {
  if (err) {
    return console.error(err);
  }
  console.log("文件删除成功!");
});

运行结果为:
Node.js基础入门 - 图51

注:在这里不要理所应当的决定新建文件就是 fs.link,虽然有这个方法,但是它的作用不是新建文件,有兴趣的可以自行查阅一下官方文档。新建文件使用fs.writeFile,可以再回去看一下前面的内容注释。

修改文件名

异步的修改文件名的语法为:

fs.rename(oldPath, newPath, callback);

参数说明:

  • oldPath:原来的文件名字。
  • newPath:新的文件名字。
  • callback:回调函数,除了可能的异常,完成回调没有其他参数。

例子:

新建一个 old.txt 的文件,和 rename.js 的文件写入以下代码:

// 引入 fs 模块
var fs = require("fs");
// 异步的修改文件名字
fs.rename("old.txt", "new.txt", (err) => {
  if (err) throw err;
  console.log("重命名完成");
});

运行结果为:
Node.js基础入门 - 图52

目录操作

新建目录

异步创建目录的语法为:

fs.mkdir(path[, options], callback);

参数说明:

  • path:文件路径。
  • options:有两个参数。recursive 表示是否以递归的方式创建目录,默认为 false。mode 设置目录权限,Windows 上不支持。默认为 0o777。
  • callback:回调函数,除了可能的异常,完成回调没有其他参数。

例子:

// 引入 fs 模块
var fs = require("fs");
console.log("创建目录 ./test");
// 异步创建目录
fs.mkdir("./test/", function (err) {
  if (err) {
    return console.error(err);
  }
  console.log("目录创建成功。");
});

运行结果为:
Node.js基础入门 - 图53

读取目录

异步读取目录的语法为:

fs.readdir(path[, options], callback);

参数说明:

  • path:文件路径。
  • options:有两个参数(encoding,withFileTypes)。encoding 默认值为 ‘utf8’,withFileTypes 默认值为 false。
  • callback:回调函数,回调函数带有两个参数(err, files)。err 为错误信息,files 为目录下的文件数组列表。

例子:

新建 readdir.js 文件,输入如下代码并保存:

// 引入 fs 模块
var fs = require("fs");
// 读取刚才新建的目录
fs.readdir("./test", function (err, files) {
  if (err) {
    throw err;
  }
  // files 是一个数组
  // 每个元素是此目录下的文件或文件夹的名称
  console.log(files);
});
//读取project目录
fs.readdir("../project", function (err, files) {
  if (err) {
    throw err;
  }
  // files 是一个数组
  // 每个元素是此目录下的文件或文件夹的名称
  console.log(files);
});

运行结果为:
Node.js基础入门 - 图54

删除目录

异步删除目录的语法为:

fs.rmdir(path, callback);

参数说明:

  • path:文件路径。
  • callback:回调函数,除了可能的异常,完成回调没有其他参数。

例子:

// 引入 fs 模块
var fs = require("fs");
// 执行前创建一个空的 test 目录,我们前面已经创建好了
console.log("准备删除目录 ./test");
// 异步删除目录
fs.rmdir("./test", function (err) {
  if (err) {
    return console.error(err);
  }
});

运行结果为:
Node.js基础入门 - 图55

总结

在本节中,我们学习了两个常用的 Node.js 的模块:

  • Node.js http
  • Node.js fs

在 Node.js 的实际使用中,这两个模块会经常被调用。所以,本节中对两个模块的方法的介绍需要大家动手编码操作调试,尽量做到熟练掌握。

6.Node.js Express 框架

简介

本节中,我们将介绍一个 Node.js 的 Web 框架:Express。

知识点

  • Express 简介
  • 安装 Express
  • 路由
  • 静态文件
  • Express 处理 GET、POST 请求

Express

Express 简介

Express 是一个高度包容,快速而极简的 Node.js Web 框架,提供了一系列强大特性帮助我们创建各种 Web 应用,和丰富的 HTTP 工具。我们可以通过 Express 可以快速地搭建一个完整功能的网站。使用框架的目的就是让我们更加专注于业务,而不是底层细节。

Express 安装

在实验楼的环境中使用以下命令安装 Express:

npm install express

安装好的界面如下所示:
Node.js基础入门 - 图56

第一个 Express 框架实例

我们的第一个实例是输出 “hello world”,新建 hello.js 文件,代码如下所示:

var express = require("express");
var app = express();
app.get("/", function (req, res) {
  res.send("Hello World");
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

执行以上代码:

node hello.js

运行效果如下所示:
Node.js基础入门 - 图57

点击工具 Web 服务,会新开一个窗口显示Hello World。如果没反应,请先执行mvn jetty:run命令即可启动 Web 服务。特别需要注意的是实验楼环境下只能使用 8080 端口。

路由

路由用于确定应用程序如何响应客户端请求,包含一个 URI(路径)和一个特定的 HTTP 请求方法(GET、POST 等)。

每个路由可以具有一个或多个处理程序函数,这些函数在路由匹配时执行。

路由定义采用以下结构:

app.method(path, handler);

注:app是 Express 的实例,method是指 HTTP 请求方法(GET、POST 等),path是指服务器上的路径,handler是指路由匹配时执行的函数。
下面我们来看一下简单的例子来学习如何定义路由,新建 test.js 文件代码如下所示:

var express = require("express");
var app = express();
// GET 请求
app.get("/", function (req, res) {
  console.log("GET 请求");
  res.send("Hello,我是GET请求");
});
// POST 请求
app.post("/", function (req, res) {
  console.log("POST 请求");
  res.send("Hello,我是 POST 请求");
});
// /index 响应 index 页面 GET 请求
app.get("/index", function (req, res) {
  console.log("/响应index页面 GET 请求");
  res.send("Hello,我是 index 页面 GET 请求");
});
// 对页面 abcd, abxcd, ab123cd, 等响应 GET 请求
app.get("/ab*cd", function (req, res) {
  console.log("/ab*cd GET 请求");
  res.send("Hello,我是正则匹配");
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

运行上述代码:

node test.js

在浏览器中的运行效果为:
Node.js基础入门 - 图58

对比一下我们前面用原生的 http,少了很多的判断语句,代码量也大大减少了,而这也是我们 Express 的魅力所在。我们后面将继续学习如何使用 Express 框架,其实 Express 框架之与 Node.js 就相当于 jQuery 之与 JavaScript。

静态文件

Express 提供了内置的中间件express.static来设置静态文件如:图片,CSS,JavaScript 等。比如:

// 我们只有公开了 public 目录,才可以直接通过 /public/xx 的方式访问 public 目录中的资源
app.use("/public/", express.static("./public/"));

来看个完整的例子,首先新建 public 目录,在 public 目录下新建 test.txt 文,里面随便写一句话:我爱学习,身体棒棒!然后在 project 目录下新建一个 testStatic.js 的文件代码如下:

var express = require("express");
var app = express();
app.use(express.static("public"));
app.get("/", function (req, res) {
  res.send("Hello World");
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

执行上面的代码:

node testStatic

Node.js基础入门 - 图59

在浏览器中的运行效果为:

Node.js基础入门 - 图60

大家可以自行尝试把app.use(express.static('public'));这行代码注释掉运行后会显示Cannot GET /test.txt

Express 框架处理 GET 请求 和 POST 请求

GET 请求

我们写一个简单的表单提交数据的案例,来演示如何用 Express 框架处理 GET 请求。

首先新建一个 getTest.html 文件,代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>
  <body>
    <form action="/get_test" method="GET">
      学号: <input type="text" name="stuNum" /><br />
      姓名: <input type="text" name="stuNam" />
      <input type="submit" value="提交" />
    </form>
  </body>
</html>

再新建一个 getTest.js 的文件,代码如下所示:

var express = require("express");
var app = express();
app.get("/", function (req, res) {
  // 传送指定路径的文件-会自动根据文件 extension 设定 Content-Type
  // 也可以用前面的 art-template 模板引擎
  res.sendFile(__dirname + "/" + "getTest.html");
});
app.get("/get_test", function (req, res) {
  // 输出 JSON 格式
  var response = {
    studentNumber: req.query.stuNum,
    studentName: req.query.stuNam,
  };
  console.log(response);
  // JSON.stringify() 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串
  res.end(JSON.stringify(response));
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

执行上面的代码:

node getTest.js

服务器中运行效果为:
Node.js基础入门 - 图61

浏览器中运行效果为:

Node.js基础入门 - 图62

注:上面的例子中也就简单实现了我们的表单用 get 方式的提交功能,大家注意观察 url 栏上,是把学号和姓名都附加在 url 地址上的,也就是说我们是能够看到的。

POST 请求

同样的我们首先新建一个 postTest.html 的文件,代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title></title>
  </head>
  <body>
    <form action="/post_test" method="POST">
      学号: <input type="text" name="stuNum" /><br />
      姓名: <input type="text" name="stuNam" />
      <input type="submit" value="提交" />
    </form>
  </body>
</html>

注:上面的代码就是就是把 method 改成了 post,还改了下 action 路由。
再新建一个 postTest.js 的文件,代码如下所示:

var express = require("express");
var app = express();
// 加载 body-parser
var bodyParser = require("body-parser");
// 创建 application/x-www-form-urlencoded 编码解析
var urlencodedParser = bodyParser.urlencoded({ extended: false });
app.get("/", function (req, res) {
  // 传送指定路径的文件,会自动根据文件 extension 设定 Content-Type
  // 也可以用前面的 art-template 模板引擎
  res.sendFile(__dirname + "/" + "postTest.html");
});
app.post("/post_test", urlencodedParser, function (req, res) {
  // 输出 JSON 格式
  var response = {
    studentNumber: req.body.stuNum,
    studentName: req.body.stuNam,
  };
  console.log(response);
  // JSON.stringify() 方法是将一个 JavaScript 值(对象或者数组)转换为一个 JSON 字符串
  res.end(JSON.stringify(response));
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

执行上面的代码:

node postTest.js

在浏览器中的运行效果为:
Node.js基础入门 - 图63

改写创建一个简单的网站的例子

我们来用 Express 框架改写前面的创建一个简单的网站的例子,其中 index.html 和 register.html 的代码与前面的一致。

首先引入art-template模板引擎:

npm install express art-template express-art-template

在 project 目录下,新建一个 views 目录,然后在 views 目录下,创建 index.html 和 register.html 页面,最后新建一个 app.js 的文件。代码如下所示:

var express = require("express");
var app = express();
app.engine("html", require("express-art-template"));
app.get("/", function (req, res) {
  res.render("index.html");
});
app.get("/register", function (req, res) {
  res.render("register.html");
});
app.listen(8080, function () {
  console.log("服务器启动了");
});

服务器中运行效果为:
Node.js基础入门 - 图64

在浏览器中的运行效果为:

Node.js基础入门 - 图65

总结

在本节中,我们学习了一个 Node.js Web 框架:Express,内容如下

  • Express 简介
  • 安装 Express
  • 路由
  • 静态文件
  • Express 处理 GET、POST 请求

通过使用 Express 框架,我们可以快速便捷的搭建一个功能完整的网站。 到这里,Node.js 基础内容的学习就完成了,对于课程中出现的实验代码,一定要动手编码调试,这样才能对代码和概念有更深的理解,对于 Node.js 的使用才能更加熟练。