Nodejs

204 阅读31分钟

Node.js 全局对象

在 JavaScript 中全局对象通常是 window,而在 Node.js 中全局对象是 global。

全局变量

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

  • 在最外层定义的变量。
  • 全局对象的属性。
  • 隐式定义的变量(未定义直接赋值的变量)。
    下面介绍一些常用的全局变量和全局函数:
  1. __filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径,且和命令行参数所指定的文件名不一定相同。如果在模块中,返回的值是模块文件的路径。比如创建一个叫 fnTest.js 的文件,输入以下代码:
console.log(__filename);

来看看运行效果:

1

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

1

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

来看看运行效果:

1

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

运行效果:

1

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

运行效果:

1

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

Node.js 创建第一个应用

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

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

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

var http = require("http"); // 加载 http 模块,并将实例化的 HTTP 赋值给变量 http

http.createServer(function (request, response) {
    // 发送 HTTP 头部
    // HTTP 状态值: 200 : OK
    // 内容类型: text/plain
    response.writeHead(200, { "Content-Type": "text/plain" });

    response.end("Hello World\n"); // 发送响应数据 "Hello World"
  })
  .listen(8080);

// 终端打印如下信息
console.log("Server running at http://127.0.0.1:8080/");

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

node server.js

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

浏览器运行效果:

1

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 示例:

{
  "name": "shiyanlou",
  "description": "Shiyanlou test package.",
  "version": "0.1.0",
  "keywords": ["shiyanlou", "nodejs"],
  "maintainers": [
    {
      "name": "test",
      "email": "test@shiyanlou.com"
    }
  ],
  "contributors": [
    {
      "name": "test",
      "web": "https://www.lanqiao.cn/"
    }
  ],
  "bugs": {
    "mail": "test@shiyanlou.com",
    "web": "https://www.lanqiao.cn/"
  },
  "licenses": [
    {
      "type": "Apache License v2",
      "url": "http://www.apache.org/licenses/apache2.html"
    }
  ],
  "repositories": [
    {
      "type": "git",
      "url": "http://github.com/test/test.git"
    }
  ],
  "dependencies": {
    "webkit": "1.2",
    "ssl": {
      "gnutls": ["1.0", "2.0"],
      "openssl": "0.9.8"
    }
  }
}

想要了解更多可以在:package.json 的介绍文档 查看。

其中版本说明

1、软件版本号

一般来讲大部分的软件版本号分3段,比如 A.B.C

  • A 表示大版本号,一般当软件整体重写,或出现不向后兼容的改变时,增加A,A为零时表示软件还在开发阶段
  • B 表示功能更新,出现新功能时增加B
  • C 表示小修改,如修复bug,只要有修改就增加C

2、(node.js中的)^和~区别

当我们查看项目配置文件package.json中已安装的库的时候,会发现他们的版本号之前都会加一个符号,有的是插入符号(^),有的是波浪符号(~)

  • 当使用npm install 安装包时,默认会在包的版本号前面添加^符号
  • 当在包的版本号前面插入波浪符号~时,表示当更新包时,锁定次版本,将补丁版本更至最新;例如 ~1.15.2 ,表示 >=1.15.2 && <1.16.0;
  • 当在包的版本号前面插入符号^时,表示当更新包时,锁定主版本,将次版本更到最新;例如 \ ^3.3.4 ,表示 >=3.3.4 && <4.0.0

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

1

包操作

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

安装包:

npm install express

更新包:

npm update express

删除包:

npm uninstall express

想要学习更多 npm 的操作可以访问 npm 中文文档

在 npm 社区中去查找包,再通过命令 npm install 模块名字 就可以安装。每个模块的名字全球唯一。

Node.js 中的函数

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

function sayHi(value) {
  console.log(value);
}
function execute(someFunction, value) {
  someFunction(value);
}
execute(sayHi, "hi");

这里的sayHi函数就可以称为回调函数

运行结果为:

1

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

function execute(someFunction, value) {
  someFunction(value);
}
execute(function (value) {
  console.log(value);
}, "hi");

匿名函数

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

function(){

}

箭头函数

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

语法为:

(参数 1, 参数 2, …, 参数 N) => {函数声明}

// 相当于:(参数 1, 参数 2, …, 参数 N) => {return 表达式;}
(参数 1, 参数 2, …, 参数 N) => 表达式(单一)

// 当只有一个参数时,圆括号是可选的
(单一参数) => {函数声明}
单一参数 => {函数声明}

// 没有参数的函数应该写成一对圆括号
() => {函数声明}

例子:

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

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

Node.js 异步编程

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

阻塞代码实例

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

创建 a.js 代码为:

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

运行结果为:

1

非阻塞代码实例

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

var fs = require("fs");
fs.readFile("syl.txt", function (err, data) {
  if (err) return console.error(err);
  console.log(data.toString());
});

console.log("程序执行完毕!");

运行结果为:

1

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

Node.js 事件

概述

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

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

EventEmitter 类获取

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

添加监听器

emitter.on(eventName, listener)

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

参数说明:

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

例子:

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

运行效果如下所示:

1

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

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

运行效果如下所示:

1

emitter.addListener(eventName, listener)

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

调用监听器

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

参数说明:

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

例子:

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

运行结果如下:

1

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

只执行一次的监听器

当使用 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");

运行结果为:

1

使用 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");

运行结果为:

1

默认情况下,事件监听器会按照添加的顺序依次调用。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");

运行结果为:

1

注: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");

运行结果为:

1

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");

运行结果为:

1

error 事件

当 EventEmitter 实例出错时,应该触发 'error' 事件。

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

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

运行结果为:

1

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

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

运行结果为:

1

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 服务。

最终的运行效果如下所示:

1

创建服务器

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

http.createServer([requestListener]);

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

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

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

response 对象常用的方法有:

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

    参数说明:

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

    比如:

    response.writeHead(200, { "Content-Type": "text/plain;charset=UTF-8" });
    

    注:此方法只能在消息上调用一次,并且必须在调用 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/");

最终的运行效果为:

1

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("服务器启动成功,可以访问了。。。");
});

运行效果为:

1

也就是说我们现在已经能够对不同的 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...");
});

来看看运行效果:

1

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

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

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

Node.js fs

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

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("文件打开成功!");
});

运行结果为:

1

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

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("文件关闭成功");
  });
});

运行结果为:

1

使用 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("文件关闭成功");
    });
  });
});

运行结果为:

1

使用 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("文件关闭成功");
    });
  });
});

运行结果为:

1

还有一种语法是:

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("文件关闭成功");
    });
  });
});

运行结果为:

1

fs.read 和 fs.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);
});

运行结果为:

1

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

使用 toString() 方法:

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

运行结果为:

1

设置输出编码:

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

运行结果为:

1

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);
  });
});

运行结果为:

1

我们可以通过设置 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);
  });
});

运行结果为:

1

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

Express框架

Express 简介

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

Express 安装

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

npm install express

安装好的界面如下所示:

1

第一个 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

运行效果如下所示:

1

路由

路由用于确定应用程序如何响应客户端请求,包含一个 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

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

1

对比一下我们前面用原生的 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

1

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

1

大家可以自行尝试把 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

服务器中运行效果为:

1

浏览器中运行效果为:

1

注:上面的例子中也就简单实现了我们的表单用 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

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

1

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

我们来用 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("服务器启动了");
});

服务器中运行效果为:

1

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

1