面向初学者的Node.js与Express.js使用指南——服务器、路由机制以及视图处理方法的详细讲解
Node.js是一种用于在浏览器之外执行JavaScript的运行时环境。无论是小型脚本还是大规模的后端应用程序,Node.js都提供了构建适用于各种环境的JavaScript应用所需的API和工具。 然而,学习Node.js可能会让人感到不知所措。由于有太多需要理解的API、包、工具和概念,人们很容易迷失方向。 正因为如此,这本书重点讲解了构建实际应用所需的基础Node.js知识,避免了不必要的干扰。你将了解到Node.js的工作原理,如何使用其内置的API,以及如何创建能够处理文件、URL、事件、HTTP请求等功能的应用程序。 你还会学习到Express.js是如何简化许多常见的服务器端任
Node.js是一种用于在浏览器之外执行JavaScript的运行时环境。无论是小型脚本还是大规模的后端应用程序,Node.js都提供了构建适用于各种环境的JavaScript应用所需的API和工具。
然而,学习Node.js可能会让人感到不知所措。由于有太多需要理解的API、包、工具和概念,人们很容易迷失方向。
正因为如此,这本书重点讲解了构建实际应用所需的基础Node.js知识,避免了不必要的干扰。你将了解到Node.js的工作原理,如何使用其内置的API,以及如何创建能够处理文件、URL、事件、HTTP请求等功能的应用程序。
你还会学习到Express.js是如何简化许多常见的服务器端任务的。我们会探讨路由、控制器、视图等其他概念,这些内容能帮助你更高效地组织和构建Web应用。
无论你是第一次接触后端开发,还是希望进一步提升自己的JavaScript技能,这本指南都能为你提供扎实且实用的基础知识,帮助你使用Node.js和Express.js进行开发。
目录
你应该已经了解的知识
本指南适用于那些已经掌握了基本JavaScript知识,并希望开始使用Node.js和Express.js构建Web应用程序的读者。如果你熟悉以下内容,那么阅读本指南会带来最大的帮助:
JavaScript的基础知识,包括变量、函数、数组和对象。
基本的异步JavaScript编程,包括回调函数、Promise以及`async`和`await`关键字。
基础的HTML知识,包括链接、表单和输入元素。
如何使用终端来导航文件夹并执行命令。
你不需要具备使用Node.js或Express.js的先验经验。熟悉如何安装npm包会很有帮助,但示例中的步骤会指导你完成所需的操作。
你需要的工具
在开始之前,请确保你已经安装了以下软件:
Node.js 24.15.0或更高版本。
npm 11.14.0或更高版本。
你可以通过运行以下命令来检查自己安装的版本:
node --version & npm --version
Node.js本身包含了npm,但随节点版一起提供的npm版本可能比这里列出的版本要旧。请核对两个版本的编号,如有需要,请分别更新npm。CodeSweetly的包管理器使用指南会告诉你如何安装、更新和检查这些工具。
本指南中提到的文件创建命令假设你的环境中已经安装了`mkdir`和`touch`等命令行工具。在macOS或Linux系统中,这些工具通常存在于Bash或zsh中;在Windows系统中,则可以通过Git Bash使用;而在使用Windows Subsystem for Linux的情况下,也可以通过Linux shell来执行这些命令。当然,你也可以直接在代码编辑器中创建文件和文件夹。
让我们开始学习Node.js吧。
Node.js究竟是什么?
Node.js既不是一种框架,也不是一种编程语言。实际上,它是一个异步、事件驱动的开发环境,其中包含了以下组件:
JavaScript引擎(谷歌的V8引擎)
模块系统(CommonJS和ES modules)
操作系统APIs(如`os`模块)
文件系统操作接口(`fs`模块)
网络服务器功能(`http`、`net`、`https`模块)
内存管理工具
事件循环机制
这些组件共同构成了Node.js的核心架构,使得JavaScript能够在浏览器之外运行。
上图展示了Node.js作为一个功能完备的运行时环境,它为在浏览器之外构建和运行各种规模的JavaScript应用程序提供了必要的基础设施——无论是服务器程序、命令行工具、脚本,还是全栈应用,都可以通过Node.js来实现。
让我们通过使用Node.js和Express.js来构建应用程序,从而探讨图片中强调的那些要点。首先,创建项目目录。
为你的项目创建一个新的目录
使用mkdir命令来创建新的项目目录,操作方法如下:
mkdir codesweetly-nodejs-app-001
注意:你可以使用任何你喜欢的名称。在本指南中,我们为了演示 purposes会使用codesweetly-nodejs-app-001这个名称。
之后,通过命令行进入你的项目目录。
cd codesweetly-nodejs-app-001
创建package.json文件
进入项目目录后,使用npm来初始化一个package.json文件。
npm init -y
接下来,使用以下命令删除package.json文件中可选的main字段:
npm pkg delete main
提示:package.json文件中的main字段用于指定当其他程序加载该包时应该执行的入口点。对于这个项目来说,我们并不需要这个字段,因为我们会直接运行我们的脚本;保留它也不会产生任何不良影响。
为什么选择Node.js?
JavaScript最初是为网页浏览器开发的,但其他环境也同样提供了执行它的基础设施。
Node.js在2009年发布时,为在网页浏览器之外运行JavaScript提供了可能。例如,我们可以创建一个可以在系统终端中运行的脚本。
1. 创建一个JavaScript文件
创建你希望让Node.js来执行的JavaScript文件。
在支持touch命令的shell环境中(如Bash或zsh),可以使用以下命令来创建文件;否则,也可以在代码编辑器中创建该文件:
touch console.js
2. 编写你的JavaScript程序
打开新创建的JavaScript文件,然后编写你的程序:
console.log("=== 来自CodeSweetly团队的问候! ===");
console.log("我们希望你在使用Node.js进行编程时能够享受到乐趣。");
console.log("感谢你成为CodeSweetly社区的一员。");
console.log("=== 继续编码,继续创造,继续发布你的作品吧。 ===");
3. 运行你的JavaScript程序
安装了Node.js之后,你就可以通过命令行使用node命令来运行Node.js应用程序了。
node console.js
node:用于运行Node.js脚本或REPL的命令。console.js:你希望让Node.js执行的JavaScript文件。
你也可以将这个命令添加到项目《package.json》文件中的`"scripts"`字段中:
{
"scripts": {
"start": "node console.js",
"test": "echo \"Error: no test specified\" && exit 1"
}
}
配置好这个脚本后,你就可以在终端中这样运行你的JavaScript程序:
npm run start
一旦你执行了这个脚本,Node就会将程序的输出显示在终端中。输出内容如下所示:
$ npm run start
> codesweetly-nodejs-app-001@1.0.0 start
> node console.js
=== 来自CodeSweetly团队的问候! ===
我们希望你能愉快地使用Node.js进行编程。
感谢你成为CodeSweetly社区的一员。
=== 继续编码,继续创造,继续发布你的作品吧。 ===
如你所见,我们成功地在浏览器之外运行了JavaScript程序。而这正是Node.js所能帮助我们做到的。Node.js是一个异步、事件驱动的运行环境,它允许我们在浏览器之外执行JavaScript代码。你还可以自动重新运行你的程序,接下来我们就来讨论如何实现这一点。
4. 自动重新运行你的JavaScript程序
默认情况下,每次你对JavaScript文件进行修改后,都需要手动重新运行它。
在不断修改代码的同时还要重复执行这些手动操作,确实会带来一些麻烦。幸运的是,Node提供了`--watch`选项来自动化这个过程:
node --watch filename.extension
上面的命令使用了`--watch`选项,使Node处于监控模式。这样一来,每当你在其依赖关系图中的任何文件上做出修改时,Node就会自动重新执行指定的脚本。
node:用于运行Node.js脚本或REPL的命令。--watch:用于激活Node的监控模式的选项。filename.extension:你需要让Node执行的脚本文件。
提示:在Windows、macOS或Linux系统中,可以使用`Ctrl + C`来停止正在运行的程序。
默认情况下,`--watch`选项会监控入口文件及其所依赖的所有模块。换句话说,这个命令会让Node监视以下内容:
入口文件(`filename.extension`)
入口文件导入的所有文件
这些被导入文件又所依赖的所有文件
以此类推,会监控整个依赖关系图中的所有文件
Node的默认监控模式不会监视与程序无关的文件。如果你想让Node监控其他文件,可以使用`--watch-path`选项:
node --watch-path=./src --watch-path=./tests filename.extension
在上面的命令中,`--watch-path`选项告诉Node要监控`src`和`tests`目录中的所有文件,即使这些文件并不在入口文件的依赖关系图中。
node:用于运行 Node.js 脚本或 REPL 的命令。--watch-path:此参数用于指定 Node 应该监控哪些路径以检测文件变化。filename.extension:你需要让 Node 执行的脚本文件。
提示:
使用
--watch-path时,Node 会忽略依赖关系图中的其他模块,除非这些模块位于你指定的路径范围内。因此,Node 只会监控你明确指出的路径,而不会检查其他地方。单独使用
也会使 Node 进入监控模式,但仍然只会监控你指定的路径。--watch-path在 macOS 和 Windows 上是可用的语法,但在 Linux 上不可用。在 Linux 上,应使用--watch来监控主入口文件及其依赖项。监控命令并不会自动刷新浏览器。它们只会检测文件是否发生变化,然后让 Node 重新运行你指定的程序。因此,如果你的程序需要向浏览器发送数据,你需要手动刷新浏览器或使用第三方工具来实现自动刷新。
Node.js 支持两种模块系统,让我们来了解一下它们。
Node 的模块支持
Node.js 同时支持 CommonJS 模块(扩展名为 .cjs)和 ECMAScript 模块(扩展名为 .mjs)。这意味着你可以根据自己的喜好选择使用哪种模块类型来构建 Node.js 应用程序。
例如,下面的文件是一个 script.mjs JavaScript 文件。由于它的扩展名是 .mjs,Node 会将其视为 ECMAScript 模块。
script.mjs
import http from "node:http";
而另一个文件 script.cjs,由于扩展名是 .cjs,Node 会将其视为 CommonJS 模块。
script.cjs
const http = require("node:http");
如果你想为项目中的所有 .js 文件指定具体的模块类型,可以在 package.json 文件中设置 type 字段,如下所示:
package.json
{
"scripts": {
"start": "node console.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"license": "ISC"
}
在上述代码中,"type": "module" 这一设置会使 Node 将受此 package.json 文件控制的 .js 文件视为 ES 模块。而嵌套的 package.json 文件则会单独定义其模块类型。.mjs 和 .cjs 文件仍然保持它们原有的模块类型。
如果想将某个范围内的 .js 文件视为 CommonJS 模块,只需将 "type" 字段设置为 "commonjs" 即可。
一些注意事项:
ES模块系统是JavaScript的官方标准模块系统。
在JavaScript尚未拥有官方标准的模块系统时,Kevin Dangoor于2009年1月启动了这个项目,后来这个项目发展成了CommonJS。
ES模块在2015年被纳入ECMAScript标准中。而CommonJS仍然在Node.js中得到支持。
在本指南中,我们主要会使用带有ES模块的Node.js,因为ES模块确实是JavaScript的标准模块系统。为了能够按照示例进行操作,请确保你的`package.json`文件中的`"type"`字段被设置为`"module"`。你可以通过在项目目录中运行以下命令来设置这个值:
npm pkg set type=module
在适当的情况下,我们也会对比CommonJS的语法。
现在,让我们来讨论如何使用Node.js创建能够接收并响应浏览器请求的Web服务器吧。
使用Node.js创建HTTP服务器
HTTP服务器允许你的JavaScript应用程序处理来自客户端(如浏览器)的请求,并向它们发送响应。
配置应用程序的HTTP服务器主要有三个步骤:
创建一个新的HTTP `
Server` 对象实例。指定服务器应运行的系统端口。
定义服务器应该如何响应客户端的请求。
让我们详细了解一下这三个步骤。首先,为你的项目创建一个ES模块。
touch server.js
接下来,打开新创建的模块文件,并初始化一个新的HTTP `Server` 对象实例。
初始化新的HTTP `Server` 对象实例
Node.js提供了`createServer`方法来创建HTTP `Server` 对象的实例。
你可以通过从Node.js的`http`模块中导入这个方法来在项目中使用它,具体操作如下:
server.js
import { createServer } from "node:http";
const server = createServer();
这里的具体原理是:
`
import`语句:将Node.js的`http`库中的`createServer`方法导入到`server.js`这个ES模块中。`
server`:这是一个变量,用于存储`createServer()函数返回的HTTP `Server`对象。
以下是使用CommonJS的方式:
server.cjs
const http = require("node:http");
const server = http.createServer();
一旦创建了本地HTTP `Server`对象,接下来就需要指定服务器应运行的端口。
配置服务器应运行的系统端口
HTTP `服务器` 对象提供了一个 `listen()` 方法,用于配置服务器应运行的端口以及接收客户端请求的端口。
语法
import { createServer } from "node:http";
const server = createServer();
server.listen(port, hostname, backlog, callback);
`listen()` 方法接受以下参数:
port:(数字)服务器应运行的端口。如果省略,操作系统会分配一个未使用的端口。你可以通过 `server.address().port` 来获取服务器在开始监听客户端连接后所使用的端口。hostname:(字符串)服务器应接收客户端连接的主机名或 IP 地址。如果省略,服务器会默认使用未指定的 IPv6 地址 或 IPv4 地址。这些未指定的地址会绑定到适用于该地址族的所有网络接口上。你可以通过 `server.address().address` 来获取服务器在开始监听客户端连接后所使用的 IP 地址。backlog:(数字)待处理连接队列的最大长度。默认值为 511。callback:(函数)当服务器开始监听客户端请求时,会执行的函数。
示例
server.js
import { createServer } from "node:http";
const server = createServer();
server.listen(3000, "127.0.0.1", 511, () => {
const info = server.address();
console.log(`服务器运行在 http://${info.address}:
${info.port}/`);
});
具体操作过程如下:
import语句:将 Node.js 的 `http` 库中的 `createServer` API 导入到 `server.js` 文件中。server:用于存储由 `createServer()` 函数返回的 HTTP `服务器` 对象的变量。server.listen():启动服务器以开始监听客户端连接。提示:当服务器成功开始运行时,该方法会触发一个listening事件。
以下是使用 CommonJS 的写法:
server.cjs
const http = require("node:http");
const server = http.createServer();
server.listen(3000, "127.0.0.1", 511, () => {
const info = server.address();
console.log(`服务器运行在 http://${info.address}:
${info.port}/`);
});
什么是127.0.0.1这个地址?
127.0.0.1这个地址是一个IPv4回环地址,它指的是你的本地计算机。主机名localhost也指的是你的本地计算机,但它可能会解析为127.0.0.1,或者IPv6回环地址::1。你可以这样使用它:
server.js
import { createServer } from "node:http";
const server = createServer();
server.listen(3000, "localhost", 511, () => {
console.log("服务器正在运行,地址为http://localhost:3000/");
});
如果你运行这个服务器并访问它的网址,浏览器会等待响应,因为我们还没有设置请求处理程序。现在我们就来配置它吧。
配置服务器以响应客户端的请求
createServer()方法接受一个requestListener回调函数,每当服务器收到HTTP请求时,这个回调函数会自动被调用。Node.js允许你使用这个回调函数来响应请求。
语法
createServer()方法接受两个可选参数。其语法如下:
import { createServer } from "node:http";
const server = createServer(options, callback);
options:一个用于自定义服务器行为的对象。callback:用于处理和响应客户端请求的requestListener函数。它接受两个参数:
import { createServer } from "node:http";
const server = createServer(options, function (request, response) {
// requestListener函数的主体代码
});
request参数:一个http.IncomingMessage对象,其中包含了关于客户端请求的详细信息。response参数:一个http.ServerResponse对象,用于响应客户端的请求。
将requestListener回调函数作为createServer的第二个参数传递给它,会使Node.js自动将其注册为对"request"事件的监听器。因此,上面的语法等价于:
import { createServer } from "node:http";
const server = createServer(options);
server.on("request", function (request, response) {
// requestListener函数的主体代码
});
server.on("request", callback)这条命令告诉服务器要监听请求事件,并在发生此类事件时执行指定的回调函数。
示例
server.js
// 添加 Node.js HTTP 模块
import { createServer } from "node:http";
// 指定服务器运行的主机名和端口号
const hostname = "localhost";
const port = 3000;
// 使用请求监听回调函数创建一个新的 Server 实例
const server = createServer((req, res) => {
res.statusCode = 200; // 设置成功的响应状态码为 200
res.setHeader("Content-Type", "text/plain"); // 定义响应数据的媒体类型
res.end("Hello World!"); // 指定响应数据并关闭响应流
});
// 在指定的端口和主机名上运行服务器
server.listen(port, hostname, () => {
console.log(`服务器正在以下地址运行:http://${hostname}:${port}/`);
});
上述代码片段使用了requestListener回调函数中的响应参数来配置服务器,使其能够响应客户端的请求。以下是另一种使用server.on("request", callback)的方法:
server.js
// 添加Node.js HTTP模块
import { createServer } from "node:http";
// 指定服务器运行的主机名和端口
const hostname = "localhost";
const port = 3000;
// 创建一个新的Server实例
const server = createServer();
// 监听请求事件并执行requestListener回调函数
server.on("request", (req, res) => {
res.statusCode = 200; // 设置成功的响应状态码为200
res.setHeader("Content-Type", "text/plain"); // 定义响应数据的媒体类型
res.end("Hello World!"); // 指定响应数据并关闭响应流
});
// 在指定的端口和主机名上运行服务器
server.listen(port, hostname, () => {
console.log(`服务器正在http://${hostname}:
${port}/运行`);
});
现在你的服务器已经设置完成,端口也配置好了,你可以运行这个应用程序了。
node server.js
node:用于运行Node.js脚本或REPL的命令。server.js:你需要让Node运行的JavaScript文件。
当服务器正在运行时,如果用户通过服务器的网址请求应用程序的资源,他们将会看到“Hello World!”这样的响应结果,如下图所示。
提示:在Windows、macOS或Linux系统中,可以使用Ctrl + C来停止正在运行的进程。
虽然node:http模块是Node.js用于处理HTTP请求的原生API,但开发人员通常会使用框架来简化这一过程。一些流行的Node.js Web框架包括Express.js、Fastify和Koa.js。我们就以Express.js为例,来看看框架是如何简化Node中HTTP请求处理的。
Express.js究竟是什么?
Express.js是一个Web框架,它能够简化Node.js中的服务器端HTTP请求处理流程。该框架扩展了Node的HTTP模块,提供了路由和中间件等内置功能,帮助开发人员高效地构建Web应用程序和API,而无需编写重复的代码。下面将介绍如何在你的Node.js项目中使用它。
1. 安装Express
npm install express@5.2.1
2. 使用Express.js配置项目的Node.js Web服务器
<打开JavaScript服务器文件,使用Express.js来搭建一个Web服务器:server.js
// 添加 Express 模块
import express from "express";
// 创建一个新的 Express 应用程序实例
const app = express();
// 设置服务器运行的端口号
const port = 3000;
// 为 “/” 路由创建一个处理 GET 请求的函数
app.get("/", (req, res) => {
res.send("Hello, world!");
});
// 在指定的端口上运行服务器
app.listen(port, () => {
console.log(`服务器正在 http://localhost:${port} 上运行`);
});
上述代码的作用如下:
创建一个 Express 应用程序实例,以便使用 Express 提供的各种 API(如路由处理器和中间件),从而简化 Node.js 中的 HTTP 请求处理流程。
使用 Express 的 `app.get()` 方法来定义服务器如何处理发往 “/” 路由的 HTTP GET 请求。
使用 Express 的 `res.send()` 方法发送 HTTP 响应,并根据数据类型自动设置相应的响应头信息。
使用 Express 的 `app.listen()` 方法在指定的端口上启动 Node.js HTTP 服务器;当服务器开始接收客户端请求时,会执行相应的回调函数。
提示:
在语句 `app.get("/", (req, res) => res.send("Hello, world!"))` 中:
`app.get()` 是 Express 的路由处理方法。
`"/"` 表示要处理的路由路径。
...` 是用于处理请求的函数。
整个语句就是一条路由定义。
传递给 Express 路由处理函数的参数 `req` 和 `res` 实际上是 Node.js 中 `http.IncomingMessage` 和 `http.ServerResponse` 对象的增强版本。Express 为这些对象添加了一些属性和辅助方法,从而简化了 HTTP 请求与响应的处理过程。
路由定义的顺序很重要。Express 会按照定义顺序依次处理匹配的请求。某个处理函数可以通过调用 `next()` 将控制权传递给下一个处理函数。
3. 运行 Express.js Web 服务器
要运行一个 Node.js 脚本(包括使用 Express.js 处理 HTTP 请求的脚本),只需在命令行中输入 `node` 后跟脚本文件名即可。
node server.js
在浏览器中访问 `http://localhost:3000`,就可以看到 “Hello, world!” 这一输出结果。
提示:在 Windows 上使用 `Ctrl + C`,在 macOS 上使用 `Control + C` 可以停止服务器运行。
原始的 Node.js 代码与 Express.js 代码
虽然可以使用 Node.js 原生的 HTTP API 来处理 Web 请求,但这样做需要手动进行很多配置工作,比如检查每个请求的方法和 URL、设置响应头信息,以及将数据对象转换为 JSON 格式等。
Express将重复出现的请求处理逻辑抽象为声明式方法,从而简化了Node.js Web服务器的开发过程。
下面是两个示例,分别展示了使用原生HTTP模块和Express方法编写Node.js Web服务器代码的差异。
示例1:使用原始Node.js处理Web请求
下面的Web服务器使用了Node内置的HTTP模块来处理对“首页”(/)和“书籍”(/books)这两个端点的GET请求;对于不匹配的请求,它会返回404错误响应。
server.js
import { createServer } from "node:http";
const port = 3000;
const server = createServer((req, res) => {
if (req.method === "GET" && req.url === "/") {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("欢迎来到首页!");
} else if (reqmethod === "GET" && req.url === "/books") {
const books = [
{ id: 1, name: "Code React Sweetly" },
{ id: 2, name: "Creating NPM Package" },
];
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(books));
} else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("页面未找到");
}
});
server.listen(port, () => {
console.log(`原始Node服务器运行在http://localhost:${port}`);
});
上述代码示例使用了Node的原生http模块来构建一个Web服务器,该服务器能够接收并响应浏览器的连接请求。在这个例子中,你需要手动完成以下操作:
路由匹配:
if (req.method === "..." && req.url === "...")响应头设置:
res.writeHead(...)JSON数据格式化:
JSON.stringify(...)
现在,让我们看看如何使用Express来构建类似的Web服务器。请逐一运行这些示例,替换server.js中的代码内容,然后重新启动服务器。
示例2:使用Express.js处理Web请求
下面的Web服务器使用了Express的路由功能来处理对“首页”(/)和“书籍”(/books)这两个端点的GET请求;对于不匹配的请求,它会返回404错误响应。
server.js
import express from "express";
const app = express();
const port = 3000;
app.get("/", (req, res) => {
res.send("欢迎来到首页!");
});
app.get("/books", (req, res) => {
const books = [
{ id: 1, name: "Code React Sweetly" },
{ id: 2, name: "Creating NPM Package" },
];
res.json(books);
});
app.use((req, res) => {
res.status(404).send("页面未找到");
});
app.listen(port, () => {
console.log(`Express服务器运行在http://localhost:${port}`);
});
上述代码片段使用了Express.js来构建一个Web服务器,该服务器能够接收并响应浏览器的连接请求。Express简化了以下各项任务:
路由逻辑: Express.js提供了诸如
app.get()、app.post()和app.delete()这样的简单路由声明方式,这样就无需为每条路由都编写复杂的条件逻辑来检查req.url和req.method了。响应头配置: Express的
res.send()方法会根据响应内容自动设置相应的响应头信息。当需要设置状态码时,可以使用res.status()方法,就像上面的404错误处理示例那样。JSON格式化: Express的
res.json()方法会自动将JavaScript对象转换为有效的JSON格式响应数据,同时负责序列化和响应头的设置工作。
在Node.js中创建Express.js Web服务器时需要了解的重要事项
在您的Node.js项目中创建Express.js服务器时,请记住以下关键点。
通常会最后调用app.listen()
在Express.js应用中,人们通常会将app.listen()方法放在最后调用。这样,在服务器开始监听请求之前,所有的路由、中间件以及错误处理机制都可以被正确配置完毕。
您可以使用环境变量来设置端口号
如果想要从环境变量中获取端口号,可以将现有的port声明替换为以下代码:
const port = process.env.PORT || 3000;
上述代码会让Node使用PORT环境变量的值作为端口号;如果该变量未设置或为空,系统会自动使用默认值3000。在Bash这样的shell环境中,可以使用以下命令为当前服务器运行设置端口号为8000:
PORT=8000 node server.js
常见的Express.js响应方法
以下是一些在Express.js中常用的响应方法:
res.send():向客户端发送HTTP响应,并根据该方法传入的数据类型自动设置相应的响应头信息。res.json():向客户端发送JSON格式的响应数据,同时会将响应头的Content-Type设置为application/json。res.redirect():将客户端的请求重定向到另一个URL地址。res.render():使用模板引擎生成视图内容,然后将生成的HTML字符串发送给客户端。res.status():设置响应的HTTP状态码,但不会结束请求-响应流程。您可以在其后链式调用其他响应方法,例如res.status(404).send("404错误:页面未找到")。如果想要使用默认的状态码200,则无需调用此方法。res.end():结束响应流程。如果没有传入数据参数,该方法不会发送任何额外的响应内容;但如果需要,在结束响应之前也可以传递数据信息。
Express的响应方法并不会终止HTTP请求处理函数的执行
虽然像res.send()这样的响应方法会关闭HTTP请求-响应循环,但它们并不会结束路由处理函数的执行。
下面来看一个例子:
app.get("/", (req, res) => {
// 这行代码会向客户端发送文本响应,并关闭请求-响应循环,但不会终止路由处理函数的执行
res.send("Hello, client!”
// 这行代码会将字符串输出到控制台,因为路由处理函数仍在运行中
console.log("Hello, devs!)
// 这行代码会导致错误,因为请求-响应循环已经关闭,在回调函数执行期间无法再向客户端发送响应
res.send("Hello, client again!");
});
在上面的例子中,console.log()语句能够正常执行,是因为路由处理函数仍在运行。而res.send("Hello, client!")这一行代码只是结束了对该请求的响应处理,并没有停止回调函数的执行。
需要注意的是,上面示例中的app.get(...)代码实际上就是Express中的一个路由。那么,在Express.js中,路由到底是什么呢?让我们来了解一下。
Express.js中的路由是什么?
在Express.js中,一个路由由一个HTTP方法、一个URL路径,以及一组用于处理客户端请求的中间件函数组成。
提示:
一个端点由一个HTTP方法和一个URL路径构成。(端点 = HTTP方法 + URL路径)
一个路由包括一个HTTP方法、一个URL路径,以及用于处理客户端请求的服务器端逻辑。(路由 = 服务器端的HTTP方法 + 服务器端的URL路径 + 处理函数)
Express中路由的语法结构
app.METHODPATH, HANDLER);
app:表示一个Express应用程序实例,它提供了对Express相关API的访问权限,包括各种方法和中间件,从而简化了在Node.js环境中处理HTTP请求的过程。METHOD:表示一个 lowercase形式的HTTP请求方法,例如get、post或delete。PATH:表示该路由将要处理的URL路径。它可以是一个字符串,也可以是一个正则表达式。Express 4还支持使用字符串模式进行匹配,不过在Express 5中这种语法有所不同。HANDLER:当请求的HTTP方法与路由定义中的方法相匹配时,Express会执行这个回调函数或中间件函数。处理函数可以是一个单独的函数,也可以是多个函数的组合,甚至是可以是一个函数数组与一些参数的组合。
提示:
你可以使用
app.all()方法来为特定路径上所有支持的HTTP请求方法设置处理函数。app.use()方法允许你为所有HTTP请求方法和路径设置中间件函数。如果你指定了路径参数,app.use()会匹配该路径及其子路径。例如,/book会匹配/book、/book/dashboard以及/book/dashboard/author/202605。
中间件与路由处理函数
开发者通常用“路由处理函数”来指代负责解析请求并发送响应的最终函数,而那些可重用的前置函数则被称为“中间件”。
app.METHOD PATH, MIDDLEWARE1, MIDDLEWARE2, HANDLER);
注意:Express并不要求用于路由的函数必须具有不同的名称或分类。在Express看来,这些函数都属于处理函数,而next()则用于将控制权传递给下一个函数。不过,开发者通常会区分中间件和路由处理函数,以便更好地组织代码、提高代码的可读性并便于沟通。
中间件函数通常存储在
/middlewares目录中,它们充当执行跨领域任务或准备工作的实用组件,例如身份验证、数据验证、日志记录以及数据加载等。这些函数通常是可重用的,并且会通过调用next()将控制权传递给下一个函数。路由处理函数有时会被组织在
/controllers目录中的控制器中,它们包含生成端点响应的业务逻辑,例如getUserProfile、createBlogPost或deleteAccount等。这些函数通常是针对特定路径设计的,并且会通过调用res.send()或res.json()等方法来完成请求-响应周期。
Express.js中的路由示例
以下是一些Express.js中的路由示例。
使用单个处理函数的Express路由
app.get("/single/handler", (req, res) => {
res.send("请求由单个处理函数进行处理!");
});
上面的示例使用了一个单一的回调函数来处理对/single/handler端点的GET请求。在这种情况下,这个处理函数就是一个内嵌的控制器。一种常见的做法是将控制器放在controllers目录中,这样既可以方便代码的测试和维护,又能保持路由定义的清晰性。
下面是一个具体的例子:
以下示例展示了一个使用模块化控制器处理的GET /single/handler路由。该控制器被单独放在一个模块中(controllers/single-handler.js)。这种设计将路由定义与生成响应的处理逻辑分离开来。
import express from "express";
import * as controller from "./controllers/single-handler.js";
const app = express();
const port = 3000;
app.get("/single/handler", controller.singleHandler);
app.listen(port, () => {
console.log(`Express服务器运行在http://localhost:${port}`);
});
下面是一个仅包含singleHandler处理函数的控制器文件示例。
controllers/single-handler.js
function singleHandler(req, res) {
res.send("请求通过单个处理函数得到了处理!");
}
export { singleHandler };
提示:控制器是一组用于处理特定端点请求的处理函数。为了提高代码的组织性并明确划分职责,控制器通常会被放在单独的模块中。
使用多个中间件函数的Express路由
app.get(
"/multiple/middleware",
(req, res, next) => {
console.log("第一个中间件:将控制权传递给下一个中间件>");
next();
},
(req, res, next) => {
console.log("第二个中间件:将控制权传递给下一个中间件>"
next();
},
(req, res) => {
res.send("请求通过多个中间件函数得到了处理!");
},
);
上述示例使用了多个中间件函数来处理对/multiple/middleware路径的GET请求。
使用中间件函数数组的Express路由
const arrayOfCallbacks = [
function callback1(req, res, next) {
console.log("第一个中间件:将控制权传递给下一个中间件>"
next();
},
function callback2(req, res, next) {
console.log("第二个中间件:将控制权传递给下一个中间件>"
next();
},
function callback3(req, res) {
res.send("请求通过中间件函数数组得到了处理!");
},
];
app.get("/array/middleware", arrayOfCallbacks);
上述示例使用了一个中间件函数数组来处理对/array/middleware端点的GET请求。
结合中间件函数数组与单独回调函数的Express路由
const arrayOfCallbacks = [
function callback1(req, res, next) {
console.log("第一个回调函数:将控制权传递给下一个中间件>");
next();
},
function callback2(req, res, next) {
console.log("第二个回调函数:将控制权传递给下一个中间件>"
next();
},
];
app.get(
"/mix/middleware",
arrayOfCallbacks,
(req, res, next) => {
console.log("第三个回调函数:将控制权传递给下一个中间件>"
next();
},
(req, res, next) => {
console.log("第四个回调函数:将控制权传递给下一个中间件>"
next();
},
(req, res) => {
res.send(
"请求是通过一组中间件函数以及独立的回调函数来处理的!"
);
},
);
上述示例同时使用了回调函数数组和单独的函数参数来处理对/mix/middleware路径的GET请求。
关于next参数需要了解的重要事项
next()会将控制权传递给栈中下一个中间件函数。next("route")会跳过当前路由的剩余处理程序,继续查找匹配的路由。匹配时会同时考虑请求方法和路径。只有在使用app.METHOD()或router.METHOD()定义的中间件函数中,这种用法才有效。next(new Error(value))会将控制权传递给下一个错误处理中间件。如果当前正在运行的中间件没有结束请求-响应周期,那么就需要在回调函数中使用
next参数来将控制权传递给下一个中间件。否则,客户端的请求将会处于待处理状态。
Express.js中的中间件分类
Express中间件可以分为五大主要类别:
应用层中间件
路由器层中间件
错误处理中间件
内置中间件
第三方中间件
这些分类说明了中间件的安装位置、其行为方式或其来源。
应用层中间件
这种类型的中间件是直接附加到Express应用程序实例(app)上的。
示例:
import express from "express";
const app = express();
app.get("/profile", (req, res, next) => {
console.log("Hi, there!");
next();
});
路由器层中间件
这种中间件是附加到Express Router实例(router)上的。它的运作方式与应用层中间件类似,但它是安装在路由器上而不是应用程序上。
示例:
import express from "express";
const router = express.Router();
router.get("/profile", (req, res, next) => {
console.log("Hi, there!");
next();
});
错误处理中间件
这种中间件用于处理请求处理过程中出现的错误,其函数签名包含四个参数。
import express from "express";
const app = express();
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send("Error 500: 遇到了意外错误");
});
注意:
即使某些参数没有被使用,这四个参数也必须全部存在。否则,Express无法识别该函数为错误处理中间件。Express是根据声明的参数数量来识别这类中间件的,而不是参数的名字。
应该将错误处理中间件定义在那些需要它来处理的路由和中间件之后。这样的安排可以确保它能处理来自前面中间件函数的错误请求。
内置中间件
这些中间件函数是Express自带的功能,默认即可使用,无需额外安装任何包。
示例:
import express from "express";
express.static("public");
express.json();
express.urlencoded();
第三方中间件
这些中间件函数是由Express社区其他开发者开发并分享的第三方包提供的。例如cors、morgan和helmet等。
import express from "express";
import cors from "cors";
const app = express();
app.use(cors());
注意:Express中的中间件分类并非互斥的
示例1:
import express from "express";
const app = express();
app.use(express.json());
上面代码中的express.json()中间件同时属于:
应用层中间件,因为它被添加到了
app实例上内置中间件,因为Express本身提供了这个功能
示例2:
import express from "express";
import cors from "cors";
const router = express.Router();
router.use(cors());
上面代码中的cors()中间件同时属于:
路由器层中间件,因为它被添加到了
router实例上第三方中间件,因为它是通过外部包提供的
你可能会问上面示例中的express.Router()方法是什么?让我们来了解一下。
什么是express.router()方法?
express.router()方法用于创建一个路由器对象,该对象可用于组合相关的路由和中间件。你可以使用app.use()或router.use()将这个路由器对象添加到Express应用中或其他路由器上。
如上图所示,路由器常被称为“迷你应用”,因为它们能够提供完整的中间件和路由系统。通过这种方式,我们可以将相关的路由集中在一起,并将其挂载到主Express应用上。例如,假设你的Express应用需要处理以下类别的路由和中间件:
首页: 用于处理与首页相关的路由和中间件。
书籍: 用于处理与书籍相关的路由和中间件。
视频: 用于处理与视频相关的路由和中间件。
在这种情况下,你可以使用express.Router()方法来将相关的路由和中间件组合在一起。这样可以使代码结构更加清晰,便于管理和重复使用。
虽然你可以在一个文件中创建所有的路由器和应用实例,但开发者通常会将其分开存储在不同的文件中,以提升代码的组织性和可维护性。因此,我们也应该采用这种做法,为每一类路由和中间件分别创建单独的文件。
为路由器创建目录
在项目的根目录下创建一个routes文件夹,用来存放所有的应用路由。
mkdir routes
注意:你可以随意给这个文件夹起名,但routes是一个常见的选择,因为这个名字正好表示了该文件夹中存放的是定义应用路由的文件。express.Router()方法只是Express提供的一个用于管理这些路由的工具而已。
创建路由文件
路由文件是一种模块,在其中你可以定义相关的路由,并导出一个Express路由器对象。每个文件通常对应着某个特定的资源或功能,比如用户、书籍或认证相关的内容。
在routes文件夹中创建相应的路由文件。
touch routes/index.js routes/books.js routes/videos.js
为与书籍相关的路由和中间件创建一个迷你应用
打开books.js文件,使用express.Router()实例来组合所有与书籍相关的路由和中间件。
routes/books.js
// 从Express中导入Router函数
import { Router } from "express";
// 创建一个新的Express路由器实例
const bookRouter = Router();
// 定义专门针对这个路由器实例的中间件
bookRouter.use((req, res, next) => {
console.log("访问了书籍路由:", Date.now());
next();
});
// 定义针对这个路由器实例的路由:
bookRouter.get("/", (req, res) => {
res.send("所有书籍列表
");
});
bookRouter.get(":id", (req, res) => {
res.type("text/plain").send(`书籍${req.params.id}的详细信息`);
});
bookRouter.get(":id/draft", (req, res) => {
// 在将ID插入HTML文本之前对其进行转义处理
const bookId = req.params.id
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">"
res.send("书籍${bookId}的草稿版本
");
});
export { bookRouter };
上述代码的作用如下:
创建了一个
bookRouter实例,以便使用Express提供的各种API方法,如.get()、.post()、.use()和.route()。使用Express的
router.use()方法将中间件添加到bookRouter实例上。使用Express的
router.get()方法来指定服务器如何处理HTTP GET请求。使用Express的
res.send()方法发送HTTP响应,并根据数据类型自动设置相应的头部信息。其中,/:id路由会将ID转义后才能显示出来。使用
export语句使得这个路由器对象可以在应用程序的任何需要使用它的地方被导入。
是时候练习如何使用路由器了
现在就是你检验自己所学关于Express路由器知识的好机会。
对于这个练习,你需要为项目的首页以及与视频相关的路由和中间件创建迷你应用。将它们分别从`routes/index.js`文件中导出为`indexRouter`,从`routes/videos.js`文件中导出为`videoRouter`,以便在后续步骤中使用。
在继续之前,请先自己尝试操作一下。如果需要帮助,可以参考`books.js`文件。通过练习,你会更好地理解这些内容。
完成这个练习后,就可以开始设置主Express应用了。
更新主Express应用
打开你项目的`server.js`文件,将刚刚创建的路由器添加到主Express应用中。
server.js
// 导入Express模块
import express from "express";
// 从相应的文件中导入路由器
import { indexRouter } from "./routes/index.js";
import { bookRouter } from "./routes/books.js";
import { videoRouter } from "./routes/videos.js";
// 创建一个新的Express应用实例
const app = express();
// 指定主机和端口来接收请求
const host = "localhost";
const port = 3000;
// 将路由器挂载到特定的路径上:
// 将indexRouter挂载到/
app.use("/", indexRouter);
// 将bookRouter挂载到/books及其子路径上
app.use("/books", bookRouter);
// 将videoRouter挂载到/videos及其子路径上
app.use("/videos", videoRouter);
// 在指定的端口和主机上运行服务器
app.listen(port, host, () => {
console.log(`服务器正在http://${host}:
${port}上运行!`);
});
上述代码的作用如下:
创建一个Express应用实例,以便使用诸如路由器和中间件这样的Express功能,从而简化Node.js中的HTTP请求处理流程。
使用Express的`app.use()`方法,将这些迷你应用添加到主Express应用中。
使用Express的`app.listen()`方法,在指定的端口上启动Node.js HTTP服务器,并在服务器开始接收客户端请求时执行相应的回调函数。
注意:`app.use()`方法中指定的路径被视为路由器的基础路径或挂载路径。在路由器中定义的路由会相对于这个路径进行匹配。例如:
`bookRouter`中的`/`路径会匹配对 `/books` 的请求。
`bookRouter`中的`/:id`路径会匹配对 `/books/:id` 的请求。
`bookRouter`中的`/:id/draft`路径会匹配对 `/books/:id/draft` 的请求。
运行你的Express.js应用
一旦完成了路由器的配置,就可以通过使用Node.js启动服务器文件来运行你的应用了。
node --watch server.js
然后,检查你的应用程序是否在指定的端点正常运行。例如,访问 http://localhost:3000/books。
提示:在 Windows、macOS 或 Linux 系统上,可以使用 Ctrl + C 来停止正在运行的进程。
关于 Express 路由器需要了解的重要事项
在使用 Express 路由器时,有一些重要的事项需要记住。
默认情况下,路由器不会继承父路由的参数
默认情况下,Express 不会将某个路由的路径参数传递给该路由内部的子路由。
示例:
// 添加 Express 模块
import express from "express";
// 创建一个新的 Express 应用程序实例
const app = express();
// 创建一个新的 Express 路由器实例
const bookRouter = express.Router();
// 定义一个子路由
bookRouter.get("/books/:bookId", (req, res) => {
const topic = req.params.topic; // 默认值为 undefined
const bookId = req.params.bookId;
res.json({ topic, bookId });
});
// 定义一个父路由
app.use("/codesweetly/:topic", bookRouter);
// 在指定的端口和主机上运行服务器
const server = app.listen(3000, "127.0.0.1", () => {
const { address, port } = server.address();
console.log(`服务器正在 http://${address}:
${port} 上运行!`);
});
如果你运行上述示例中的 Web 服务器,并在浏览器中输入以下 URL:
http://127.0.0.1:3000/codesweetly/CSS/books/91827
那么 req.params.topic 的值将会是 undefined,因为默认情况下子路由无法访问父路由的路径参数。因此,服务器只会向客户端返回一个包含 "bookId":"91827" 的响应。
如果想让子路由能够访问其父路由的路径参数,在创建路由器时需要将 mergeParams 设置为 true。
示例:
// 添加 Express 模块
import express from "express";
// 创建一个新的 Express 应用程序实例
const app = express();
// 创建一个新的 Express 路由器实例,并将 mergeParams 设置为 true
const bookRouter = express.Router({ mergeParams: true });
// 定义一个子路由
bookRouter.get("/books/:bookId", (req, res) => {
const topic = req.params.topic; // 在下面的 URL 中,这个参数的值为 "CSS"
const bookId = req.params.bookId;
res.json({ topic, bookId });
});
// 定义一个父路由
app.use("/codesweetly/:topic", bookRouter);
// 在指定的端口和主机上运行服务器
const server = app.listen(3000, "127.0.0.1", () => {
const { address, port } = server.address();
console.log(`服务器正在 http://${address}:
${port} 上运行!`);
});
如果你运行上述示例中的 Web 服务器,并在浏览器中输入以下 URL:
http://127.0.0.1:3000/codesweetly/CSS/books/91827
req.params.topic 这个参数的值将会是 CSS,因为 mergeParams: true 允许子路由访问父路由的路径参数。因此,服务器会向客户端发送一个 {"topic":"CSS","bookId":"91827"} 的响应。
提示:
代码行
const { address, port } = server.address()使用了 对象解构 的方式,从server.address()对象中获取address和port这两个值。app.listen()方法会返回一个原生 Node.js 的http.Server实例,这样就可以使用诸如.address()、.on()和.maxHeadersCount这样的服务器接口了。127.0.0.1是一个 IPv4 回环地址。localhost可能会解析为127.0.0.1,或者 IPv6 的回环地址::1。在这些示例中我们使用http://127.0.0.1:3000,是因为服务器明确设置在了这个 IPv4 地址上监听请求。
路由器可以被安装在其他路由器上
你可以将一个 Express 路由器连接到另一个 Express 路由器上。
示例:
// 导入 Express 模块
import express from "express";
// 创建一个新的 Express 应用实例
const app = express();
// 创建两个 Express 路由器实例
const htmlRouter = express.Router();
const bookRouter = express.router();
// 定义一个子路由
bookRouter.get(":bookId", (req, res) => {
res
.type("text/plain")
.send(`ID 为 ${req.params.bookId} 的 HTML 图书的详细信息`);
});
// 将 bookRouter 映射到 htmlRouter 的 /books 路由上
htmlRouter.use("/books", bookRouter);
// 将 htmlRouter 映射到 /html 及其所有子路径上
app.use("/html", htmlRouter);
// 在指定的端口和主机上运行服务器
const server = app.listen(3000, "127.0.0.1", () => {
const { address, port } = server.address();
console.log(`服务器正在地址 http://${address}:
${port} 上运行!`);
});
bookRouter 中的 GET /:bookId 路由会匹配到 GET /html/books/:bookId 这个请求,因为在这个示例中,bookRouter 是被连接到 htmlRouter 上的。
因此,如果你运行上面的示例中的 Web 服务器,并在浏览器中输入以下 URL:
http://127.0.0.1:3000/html/books/91827
服务器会向客户端发送一个 “ID 为 91827 的 HTML 图书的详细信息” 的响应。
现在你已经了解了什么是路由器,接下来我们就来讨论如何使用视图来定义你的 Express.js 应用程序返回给用户的网页的内容和结构吧。
在 Express.js 中,视图是什么?
视图是一种模板或文档,它用于定义Express.js应用程序向用户返回的网页的内容与结构。这种视图既可以是一个静态的HTML文件,也可以是由模板引擎在运行时渲染成HTML格式的动态模板。更多相关信息请参见运行时。在本指南中,我们将介绍两种类型的视图:
静态视图
动态视图
Express.js中的静态视图是什么?
静态视图包含服务器直接发送给浏览器的最终HTML内容。例如,让我们配置你的Express.js应用程序来提供四种静态视图。
创建静态视图
在项目的根目录下创建`index.html`、`about.html`、`contact.html`和`404.html`这些文件。
touch index.html about.html contact.html 404.html
打开每个视图文件,然后添加以下HTML内容:
index.html(首页)
<!doctype html>
<html lang="en">
Express.js中的静态视图 | CodeSweetly教程
欢迎使用静态视图指南
网站页面: