近期在学习IBM全栈应用开发微学士课程,故此记录学习笔记。

1. 服务端JavaScript入门

客户端-服务端的应用程序(比如基于云的应用程序)通常由“前端”和“后端”组成。前端是指用户在浏览器中看到的应用程序的部分,后端是指在服务器上运行的应用程序的部分。

1.1. 后端

后端开发人员负责开发确保网站正常运行的技术,包括服务器端的应用程序、数据库和服务器。

  • 服务器由硬件和软件组成,它们与客户端进行通信并提供功能。多种类型的服务器可用于不同的目的:Web服务器用于存储和提供网站的内容,数据库服务器用于存储和提供数据,应用程序服务器用于存储和提供应用程序的功能
  • 数据库是一种用于存储和访问数据的软件。数据库服务器是一种用于存储和访问数据的服务器。数据库服务器可以存储和访问结构化数据(例如,关系数据库)和非结构化数据(例如,文本文件、图像和视频)
  • 网页应用程序接口(API)允许两个软件之间相互通信。网络服务便是网页应用程序接口的一种、使用HTTP请求进行通信
  • 编程语言是一种用于编写软件的语言。用于后端开发的编程语言包括Java、Python、PHP、Ruby、JavaScript和C#
  • 框架是一种用于编写软件的工具。框架提供了一组通用的功能,可以帮助开发人员编写软件。用于后端开发的框架包括Spring、Django、Laravel、Ruby on Rails、Node.js和ASP.NET
  • runtimes是一种用于运行软件的工具,行为类似于微型操作系统,为应用程序的运行提供必要的资源。Node.js就是后端runtime环境的一个例子

Node.js作为一种后端技术之所以如此流行,原因之一是它运行在谷歌Chrome浏览器的开源V8引擎上。V8引擎也是在前端运行浏览器的引擎。大多数现代浏览器都使用V8引擎,因此,Node.js和浏览器之间的代码兼容性很好。

1.1.1. 可扩展性

可扩展性对企业软件的成功至关重要。它受应用程序负载的影响,是后端的一大责任。而负载指的是并发用户、交易、数据量和其他因素的总和。

可扩展性是指应用程序在不影响性能的情况下动态处理负载增减的能力。

1.2. Node.js

Node.js是一个运行在V8上的开源语言,它是JavaScript的服务器端实现。Node.js由事件驱动,使用非阻塞I/O模型,这使得它非常轻量级、高效和可扩展。

Node.js着重强调使用轻量级语言进行并发编程,它是一种单线程语言,但是,它可以使用事件循环和回调函数来处理并发。

Node.js适合希望使用回调函数和Node.js runtime事件循环等功能来构建并发应用程序的开发人员。JavaScript语言和Node.js runtime的这些功能使得开发人员只需使用一套最少的工具就可以实现快速开发。

通过服务器端JavaScript,Node.js的应用程序可以处理和路由来自客户端的请求:

  1. 用户在用HTML和CSS编写的用户界面中选择一个选项
  2. 用户的这一操作会触发在客户端实现业务逻辑的JavaScript代码
  3. JavaScript代码会向服务器发送一个请求(通过HTTP调用带有JSON数据的API)
  4. 作为在服务器上运行的Node.js应用程序的一部分,REST网络服务会接收请求并处理它
  5. REST网络服务处理请求后,通过HTTP将结果作为JSON数据返回给客户端

1.2.1. 模块

在Node.js中,模块是包含相关的、已封装的JavaScript代码的文件,用于实现特定的功能。模块可以是内置的,也可以是外部的;可以是单个文件,也可以是文件夹。

当外部应用程序需要使用模块中包含的代码时,应用程序需要调用该模块。而调用模块就需要使用语句import()或者require()

模块规范:
一个或多个模块组成一个包,包是一个目录,其中包含一个package.json文件,该文件描述了包的内容。包可以发布到npm(Node.js包管理器)上,以便其他开发人员可以使用它们。

常用的模块规范有CommonJS和ES。默认情况下,Node.js使用CommonJS规范,但是,Node.js也支持ES规范。库作者仅需要将包文件的扩展名从.js更改为.mjs,就可以使用ES模块规范。

CommonJS规范使用require(),而ES规范使用import()
当需要在自身文件之外使用模块时,必须先导出模块。在使用CommonJS规范时,可以使用module.exports导出模块;在使用ES规范时,可以使用export导出模块。

import()require()的区别:

import() require()
必须在文件开头调用 可以在文件的任何位置调用
不能在条件语句和函数中调用 可以在条件语句和函数中调用
静态绑定 动态绑定
在编译时解析 在运行时解析
异步 同步
对比require(),在涉及到加载数百个模块的应用程序中运行速度更快

require()

1
2
// export from a file named message.js
module.exports = 'Hello Programmers';
1
2
3
// import from the message.js file
let msg = require('./message.js');
console.log(msg);

import()

1
2
3
// export from file named module.mjs
const a = 1;
export { a as "myvalue" };
1
2
// import from module.mjs
import { myvalue } from module.mjs;

1.2.2. 创建简单的网络服务器

Node.js runtime打包了许多实用程序模块,你可以使用它们来创建和扩展应用程序。例如,HTTP Node.js模块提供了能够监听HTTP请求的功能。

1
2
3
4
5
6
7
8
9
10
let server = http.createServer(function(request, response) {  // create an instance of a web server
let body = "Hello World!";
response.writeHead(200, { // this callback function handles the incoming request message and provides an appropriate response message
"Content-Length": body.length,
"Content-Type": "text/plain"
});
response.end(body);
});

server.listen(8080); // set the server to listen to a specific port

1.2.3. Package.json

一个软件包由一个或多个模块组成。每个软件包都有一个package.json文件,用于描述Node.js模块的详细信息。

如果模块没有package.json文件,Node.js就会假定主模块是index.js文件。

1
2
3
4
5
6
7
// Package.json
{
"name": "mod_today",
"version": "1.0.0",
"main": "./lib/today",
"license": "Apache-2.0"
}

要为模块指定不同的主模块,可以在模块目录中Node.js脚本的相对路径中指定主模块。

1.2.4. 导入/导出Node.js模块

你可以使用require()函数导入Node.js模块。

1
let today = require("./today");

require()语句假定了脚本的文件扩展名为.js。它会创建一个对象来表示导入的模块,并将其分配给变量today

每个Node.js模块都有一个隐式exports对象。要向导入模块的Node.js应用程序提供函数或值,就需要在exports中添加属性。

1
2
3
4
5
6
7
let date = new Date();

let days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];

exports.dayOfWeek = function() { // the dayOfWeek property is added to the exports object
return days[date.getDay() - 1];
};

导入Node.js模块时,require()函数会返回一个JavaScript对象,该对象代表着模块的一个实例。

1
let today = require("./mod_today");  // the today variable is an instance of the today Node.js module that is called "today"

要访问模块的属性,就要从变量中检索属性。

1
console.log("Happy %s!", today.dayOfWeek());  // today.dayOfWeek() represents the current exported property from the today Node.js module

1.3. Express

Express.js是一个高度可配置的框架,用于在Node.js上构建应用程序。它通过使用HTTP实现程序方法和中间件来抽象出Node.js中的低级API。

以下功能可让你快速开发应用程序:

  1. Express.js应用程序

    1
    const app = express();
  2. 图像、CSS和JavaScript文件等静态文件

  3. 静态路由:定义接收和处理客户端请求的端点

  4. server.js:用于启动应用程序的文件

  5. package.json:用于定义应用程序的依赖项和脚本的文件

1.4. 软件包管理器

软件包管理器是一套用于处理包含依赖关系的模块和软件包的工具。依赖关系是指一个软件包依赖于另一个软件包。

代码库通常包含着许多依赖项,但代码库本身是独立的,不依赖于代码库之外的任何东西。这种独立性使得代码库可以在不同的环境中使用。

软件包管理器能够自动完成查找、安装、更新、配置、维护和删除软件包的工作。它们通常连接并维护一个数据库,其中包含着软件包的依赖关系和版本信息。

1.4.1. npm

npm是Node.js的软件包管理器。它是一个命令行工具,用于安装、更新、配置和删除Node.js软件包。

所有的npm软件包都需要一个package.json文件,该文件描述了软件包的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "myapp",
"version": "1.0.0",
"description": "My first Node.js app",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "John Doe",
"license": "ISC",
"dependencies": {
"express": "^4.17.1"
}
}

npm使用package.json文件中的dependencies属性来确定软件包的依赖关系。dependencies属性是一个对象,其中包含着软件包的名称和版本号。

npm有两种安装软件包的方式:本地安装或者全局安装。如果安装的软件包要在应用程序中使用,就应该使用本地安装。如果安装的软件包要在命令行中使用,就应该使用全局安装。

默认情况下npm会采用本地安装。

1
npm install <package_name>

该命令将在当前工作目录中创建一个node_modules文件夹,并将软件包安装到该文件夹中。

全局安装意味着安装软件包的计算机上的所有应用程序都可以使用该代码。全局安装应当谨慎使用,因为它会在计算机上创建一个全局软件包,这可能会导致版本冲突。

要安装node_modules文件夹中的所有软件包,要使用以下命令:

1
npm install -g <package_name>

2. 异步I/O与回调编程

2.1. 异步I/O

所有的网络操作都是异步的,因为它们需要等待网络响应。

网络服务调用的响应可能不会立即返回。当应用程序阻塞(或等待)网络操作完成时,就会浪费服务器上的处理时间。

Node.js以非阻塞方式进行所有网络操作。每个网络操作都会立即返回。要处理网络调用的结果,就需要编写一个回调函数。

应用程序、Node.js框架、调用远程服务器的网络服务和回调函数之间的交互如下:

  1. 应用程序会调用http.request(),该函数会调用远程网络服务器并请求网络服务
  2. 在Node.js框架从远程网络服务器接收HTTP响应消息之前,它会立即返回http.request()函数调用的结果。该结果只表明请求消息已成功发送,并不会说明任何有关响应消息的信息
  3. 当Node.js框架从远程服务器接收到HTTP响应消息时,它会调用在http.request()函数调用过程中定义的回调函数。该函数处理HTTP响应消息,并将结果返回给应用程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let options = {  // included the hostname of the remote server, and a URL resource path
host: "w1.weather.gov",
path: "/xml/current_obs/KSFO.xml"
};

http.request(options, function(response) { // when the Node.js module calls this anonymous function, events occur while it is receiving parts of the HTTP response object
let buffer = "";
let result = "";

response.on("data", function(chunk) {
buffer += chunk;
});

response.on("end", function() {
console.log(buffer);
});
}).end();
  • 在实际的应用程序时,你可能需要使用HTTPS而不是HTTP

2.1.1. http.request()

该函数接收一个URL和一组选项。如果URL和选项都被传入,则将两者合并,选项优先。

1
http.request(options, [callback function]);

该方法还可以接收一个可选的回调函数,在收到响应后立即调用。

1
http.request(options, function(response) { ... });

http.request()调用回调函数时,会在回调函数的第一个参数中传递一个响应对象。该回调函数的第一个参数就是响应对象。

Node.js框架会在请求函数运行时发出多个事件。你可以使用object.on()方法并将事件名称作为第一个参数传递,从而监听这些事件。如果请求成功,每次数据输入时都会在响应对象上触发一个数据事件,响应结束时触发一个结束事件。

2.1.2. 处理错误

如果请求失败,在close事件之后就会出现error事件。

1
2
3
4
5
6
let request = http.request(options, function(response) { ... });

request.on("error", function(e) {
resultCallback(e.message);
});
request.end();

2.2. 回调函数

作为一个异步框架,Node.js广泛地使用了回调函数。回调函数是一个函数,它作为参数传递给另一个函数,并在另一个函数完成后调用。

软件开发工具包(SDK)中的Node.js模块会将错误对象作为回调函数的第一个参数。

1
Call function((error))

根据这一约定,回调函数会检查第一个参数是否包含了错误对象。

1
function(error, parameter1, parameter2, ...) { ... }

如果定义了错误对象,回调函数就会处理错误并清理所有打开的网络或数据库连接。

1
2
3
4
5
6
7
8
9
10
11
weather.current(location, function(error, temp_f) {
if (error) {
console.log(error); // if the error is defined, print the error message
return;
}

// otherwise, the weather.current function call completed successfully
console.log("The current weather reading is %s degrees.", temp_f);
});

response.end("... ${temp_f}_")

2.2.1. 传递错误对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
exports.current = function(location, resultCallback) {
// ...
http.request(options, function(response) {
let buffer = "";
let result = "";

response.on("data", function(chunk) {
buffer += chunk;
});

response.on("end", function() {
parseString(buffer, function(error, result) {
if (error) {
resultCallback(error);
return;
}

resultCallback(null, result.current_observation.temp_f[0]);
});
});
});
}

2.2.2. 每级一个回调

当Node.js应用程序以非阻塞方式来调用一个模块时,该应用程序会提供一个回调函数来处理结果。 如果主应用程序调用了http.request(),它就必须提供一个回调处理程序来处理HTTP响应消息。

如果主应用程序调用了一个调用了http.request()的函数,那就会有两个回调函数:

  1. 自定义模块有一个回调函数,用于处理来自http.request()的HTTP响应消息
  2. 主营用程序有一个回调函数,用于处理第一个回调函数捕获的结果

带回调的主应用程序:

1
2
3
4
5
6
let weather = require("./weather");
let location = "KSFO";

weather.current(location, function(temp_f) {
console.log(temp_f);
});

主程序调用weather.current()时,会传递一个匿名的回调函数来处理调用结果。

1
2
3
4
5
6
7
8
9
exports.current = function(location, resultCallback) {
// ...
http.request(options, function(response) {
// ...
response.on("end", function() {
resultCallback(...);
});
}).end();
}

自定义的Node.js模块函数中的resultCallback()函数链接着主应用程序中weather.current()函数的匿名回调函数function(temp_f)

通过回调返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
exports.current = function(location, resultCallback) {
let option = {
host: "w1.weather.gov",
path: "/xml/current_obs/" + location + ".xml"
};

http.request(options, function(response) {
let buffer = "";

response.on("data", function(chunk) {
buffer += chunk;
});

response.on("end", function() {
parseString(buffer, function(error, result) {
// ...
resultCallback(null, result.current_observation.temp_f[0]);
});
});
}).end();
}

另一个回调函数的例子:

1
2
3
4
5
const message = function() {
console.log("This message is shown after 3 seconds");
}

setTimeout(message, 3000);

JavaScript中有一个内置方法叫setTimeout(),它会在执行操作前等待指定的时间(以毫秒为单位)。在示例中,信息被传入setTimeout()函数。因此,在等待3秒后,setTimeout()会将消息写入控制台。

通常这些异步回调(简称async)都用于访问数据库中的数值、下载图像、读取文件等。

2.2.3. 回调地狱

回调地狱是指在异步编程中,回调函数嵌套过多,导致代码难以阅读和维护。每个回调函数都依赖于前一个回调函数,并等待前一个回调函数完成后才能执行。

1
2
3
4
5
6
7
firstFunction(args, function() {
secondFunction(args, function() {
thirdFunction(args, function() {
// And so on ...
});
});
});

这种结构有时也被称为“The Pyramid of Doom”(末日金字塔)。

回调的另一个问题是IoC(控制反转)。当控制流(如指令的执行)处于代码的外部时,就会发生控制反转。很多时候,回调会将控制权转交给第三方,但是第三方代码的问题和错误可能很难被发现。这种情况下你不得不去信任第三方代码或者编写额外的代码来确保第三方代码不会在不应该的时候被调用、被调用的次数过多或过少、丢失上下文、传回错误的参数等。

要想缓解回调地狱和IoC的问题,你可以:

  • 写注释
  • 使用Promise
  • 将函数拆分成更小的函数
  • 使用async/await

2.3. Promise

对于API请求、I/O操作和其他异步操作,Promise是一种更好的解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let prompt = require("prompt-sync");
let fs = require("fs");

const methCall = new Promise((resolve, reject) => {
setTimeout(() => {
let filename = prompt("What is the name of the file?");
try {
const data = fs.readFileSync(filename, {
encoding: "UTF-8",
flag: "r"
});
resolve(data);
} catch(err) {
reject(err);
}
}, 3000);
});

2.3.1. Axios请求

HTTP请求在同步调用时可能会阻塞。Node.js生态系统中有许多包,它们将Promise封装在HTTP请求中,axios就是其中之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
const axios = require("axios").default;

const connectToURL = (url) => {
const req = axios.get(url); // the status of the promise until it hears back from the URL requested is pending
console.log(req);
req.then(resp => {
console.log("Fulfilling");
console.log(resp.data);
})
.catch(err => {
console.log("Rejected");
});
}

2.4. JSON

JSON是API数据交换的标准格式。

要将JSON字符串解析为JavaScript对象,可以使用方法JSON.parse()。而要将JavaScript对象转换为JSON字符串,可以使用方法JSON.stringify()

3. Express网页开发框架

默认的Node.js框架为构建网页应用程序提供了一套有限的功能。

例如,Node.js不提供XML消息的解析功能。在简单消息中,你可以使用JavaScript字符串函数来解析消息,也可以使用XML文档对象,但该对象解析XML数据流的效率并不高。

开发人员往往依赖第三方软件包来扩展Node.js功能。

你可以将网络服务信息解析为字符串:

1
2
3
4
5
6
7
8
9
10
11
12
response.on("data", function(chunk) {
buffer += chunk;
});

response.on("end", function() {
let matches = buffer.match(/\<temp_f\>.+\<\/temp_f\>/g);
if (null != matches || matches.length > 0) {
let result = matches[0].replace(/\<temp_f\>/, "").replace(/\<\temp_f\>/, "");
}

resultCallback(null, result);
});

这种手动解析的方法有着许多缺点:

  • 字符串匹配忽略了XML数据的结构
  • 信息体可能包含了畸形的XML数据
  • 根据XML数据的复杂程度,字符串匹配可能要比构建数据的XML树更有效率
  • 字符串匹配对XML数据结构变化的容忍度很低
  • 如果信息添加或删除了任何XML元素,那就必须更改字符串匹配函数的正则表达式

xml2js是一个流行的Node.js软件包,它可以将XML数据解析为JavaScript对象。与其他XML解析包不同,xml2js只使用JavaScript而不是其他语言。

第三方软件包的软件许可可能与Node.js框架不同。在安装软件包之前,请确认许可条款是否适用于你的公司和应用程序。

1
npm install xml2js

将软件包导入到Node.js应用程序中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let parseString = require("xml2js").parseString;
exports.current = function(location, resultCallback) {
// ...
let request = https.request(options, function(response) {
// ...
response.on("end", function() {
parseString(buffer, function(error, result) {
if (error) { ... }

// the result JavaScript variable represents the contents of the XML fragment in buffer
resultCallback(null, result.current_observation.temp_f[0]);
});
});
});
}

3.1. 网页框架

Node.js不是网页框架,而是在服务器上执行JavaScript的runtime环境。网页框架是支持网页应用程序的基本结构,因此要使用Node.js,就需要使用与之配合使用的网页框架。

与Node.js协同工作的框架被称为node网页框架。它们可采用两种方法构建后端:

  1. MVC(模型-视图-控制器):将应用程序分解为三个部分,每个部分都有自己的职责
  2. REST API

3.1.1. MVC

MVC是一种设计模式,用于将应用程序分解为三个部分:

  1. 模型:负责处理数据
  2. 视图:负责渲染模型传递的数据
  3. 控制器:负责管理数据流、处理用户提供的数据,并将数据发送给模型

MVC框架一般用于开发需要将数据、数据的展示和操作数据的模块分开的应用程序。

MVC模式的框架包括Koa、Django、Express和NestJS。

3.1.2. REST API

REST API允许多个网络服务相互通信。但这会受到一些限制:客户端的代码必须完全独立于服务器端的代码;客户端代码的更新不会干扰服务器端代码的运行,反之亦然。

REST API是无状态的。这代表着客户端不需要知道服务器的状态,服务器也不需要知道客户端的状态。这种无状态的特性使得REST API非常适合用于构建分布式应用程序。

REST API通过对资源的操作进行通信,不依赖于API的特定实现。当客户端使用GETPOSTPUTDELETE等HTTP方法与服务器通信时,服务器便会向客户端响应资源状态。

3.1.3. Express

Express.js是最流行的node网页框架之一。它用于路由和中间件、使用JavaScript进行直接编程,意味着学习曲线很低。

Express.js提供调试机制,有助于轻松找出应用程序中的错误。它采用异步编程方式,同时处理多个相互独立的操作请求,因此性能很好。

3.1.4. Koa

Koa是一个相对较新的网页框架,由设计Express的同一团队设计。它设计得更小巧、更具表现力,并为网页应用程序和API提供了更强大的基础。

Koa使用异步函数,因此不需要回调,这提高了处理错误的能力。该框架适合由经验丰富的大型团队开发高性能、高要求、复杂的应用程序。

3.1.5. Socket.IO

Socket.IO是开发在客户端和服务器之间实时交换双向数据的应用程序的绝佳选择。你可以开发利用Websocket而不是HTTP协议的应用程序。

它的服务器可以推送数据,而无需客户端调用数据,因此十分适用于聊天室、短信应用、视频会议和多人游戏等应用程序。

3.1.6. Hapi.js

Hapi.js是一个可靠的开源节点网页框架,内置了大量安全功能。它的插件系统使得开发人员可以轻松地扩展应用程序的功能。

它最著名的用途是开发代理和API服务器、HTTP代理用户程序、REST API以及其他桌面和应用程序。

3.1.7. NestJS

NestJS框架适合构建动态、可扩展的企业应用程序,其灵活性得益于大量的库。它采用了MVC架构。

NestJS构建在Express的基础之上,因此它们具有相似的功能。

NestJS与TypeScript兼容,还能与前端Angular框架结合使用。

TypeScript是一种JavaScript的超集,它添加了类型和其他功能,以帮助开发人员编写更好的代码。

NestJS结合了面向对象编程和函数式编程的优点,因此它的代码易于阅读和维护。

3.2. Express

Express主要用于两个目的:

  1. API
  2. 使用服务端渲染(SSR)来设置模板

Express API设置了一个与应用程序数据层交互的HTTP接口。在API的情况下,数据会使用响应对象(简称res)以JSON格式返回给客户端。

res.json()方法用于通知客户端发送数据的内容类型,如图像或文本。它还可用于对数据进行字符串化。

而在SSR中,Express用于设置模板。Express负责使用客户端通过HTTP请求的数据、结合模板动态编写HTML、CSS和/或JavaScript。

3.2.1. Node.js应用程序框架

Express实现了一个app类,你可以将其映射到网络资源路径。

1
2
3
4
5
6
7
const express = require("express");
const app = express();
const port = 3000;
// ...
let server = app.listen(port, function() {
console.log(`Listening on URL http://localhost:${port}`);
});

3.2.2. Express是如何工作的

  1. 在Node.js项目的包文件中,将Express作为依赖项添加到dependencies属性中
  2. 运行npm命令来下载缺少的模块
  3. 导入Express模块并创建一个Express应用程序实例
  4. 创建一个新的路由处理程序
  5. 在指定端口号上启动HTTP服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
// mynodeserver.js
const express = require("express");
const app = express();
const port = 3000;
app.get('/temperature/:location_code', function(request, response) {
const varlocation = request.params.location_code;
weather.current(location, function(error, temp_f) {
// ...
});
});
let server = app.listen(port, function() {
console.log(`Listening on URL http://localhost:${port}`);
});

要处理网页应用程序请求,可将HTTP方法和网络资源路径映射到JavaScript函数。

3.2.3. 路由

路由是服务器端脚本的一个重要组成部分。对同一服务器的不同路由的请求必须由服务器处理。服务器必须处理对每个路由的请求,否则就会返回相应的错误信息。路由可在应用程序级或路由器级处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const express = require("express");
const app = new express();

app.get("user/about/:id", (req, res) => {
res.send("Response about user " + req.params.id);
});

app.post("user/about/:id", (req, res) => {
res.send("Response about user " + req.params.id);
});

app.get("item/about/:id", (req, res) => {
res.send("Response about user " + req.params.id);
});

app.post("item/about/:id", (req, res) => {
res.send("Response about user " + req.params.id);
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

你需要在应用程序级别使用单独的方法来处理每个路由上的每个方法。当端点或路由较少时,这种方法很简单。但是,当端点或路由数量增加时,这种方法就会变得复杂。我们需要使用路由器来让我们的代码更加简洁、易于阅读和维护。

路由器本身用于处理分支查询,并以不同方法路由每个查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require("express");
const app = new express();

let userRouter = express.Router();
let itemRouter = express.Router();

app.use("/item", itemRouter);
app.use("/user", userRouter);

userRouter.get("/about/:id", (req, res) => {
res.send("Response about user " + req.params.id);
});

userRouter.get("/details/:id", (req, res) => {
res.send("Details about user " + req.params.id);
});

itemRouter.get("/about/:id", (req, res) => {
res.send("Information about item " + req.params.id);
});

itemRouter.get("/details/:id", (req, res) => {
res.send("Details about item " + req.params.id);
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

3.2.4. 中间件

中间件包括了可以访问请求和响应对象以及下一个函数的函数。下一个参数决定了函数执行后的操作。

一个Express应用程序可以有多个中间件,而且它们之间可以相互连接。

中间件根据目的、用途和功能分为不同的类型:

  1. 应用程序级
  2. 路由器级
  3. 错误处理
  4. 内置
  5. 第三方

应用程序级中间件可以使用app.use()方法绑定到应用程序上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");
const app = new express();

app.use(function(req, res, next) {
if (req.query.password !== "pwd123") {
return res.status(402).send("This user cannot login ");
}
console.log("Time:", Date.now());
});

app.get("/", (req, res) => {
return res.send("Hello World!");
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

客户端向服务器应用程序发出的所有请求都会通过该中间件进行路由。这种路由对验证和检查会话信息等操作很有用。

路由器级中间件不与应用程序绑定。相反,它与express.Router()实例绑定。你可以为特定路由使用特定的中间件,而不是让所有请求都通过同一个中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const express = require("express");
const app = new express();

let userRouter = express.Router();
let itemRouter = express.Router();

userRouter.use(function(req, res, next) {
console.log("User query Time: ", Date());
next();
});

userRouter.get("/:id", function(req, res, next) {
res.send("User " + req.params.id + " last successful login " + Date());
});

itemRouter.use(function(req, res, next) {
console.log("Item query Time: ", Date());
next();
});

itemRouter.get("/:id", function(req, res, next) {
res.send("Item " + req.params.id + " last enquiry " + Date());
});

app.use("/user", userRouter);
app.use("/item", itemRouter);

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});
  • 响应将根据客户端的请求路径而不同

错误处理中间件既可以绑定到整个应用程序,也可以绑定到特定路由器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const express = require("express");
const app = new express();

app.use("/user/:id", function(req, res, next) {
if (req.params.id == 1) {
throw new Error("Trying to access admin login");
} else {
next();
}
});

app.use(function(err, req, res, next) {
if (err != null) {
res.status(500).send(err.toString());
} else {
next();
}
});

app.get("/user/:id", (req, res) => {
return res.send("Hello! User Id ", req.params.id);
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

错误处理中间件总是需要四个参数:errorrequestresponsenext()。不过,你可以省略next()参数;即使省略了,也可以在方法中定义。

内置中间件可以绑定到整个应用程序或者特定路由器上。内置中间件对于从服务器渲染HTML、解析来自前端的JSON输入和解析cookie等操作很有用。

1
2
3
4
5
6
7
8
9
const express = require("express");
const app = new express();

// define the static files that can be rendered from the cad220_staticfiles directory
app.use(express.static("cad220_staticfiles"));

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

你也可以定义自己的中间件或使用第三方中间件,这些中间件可以通过npm install命令安装。

创建中间件很简单。你可以定义一个包含三个参数的函数,然后将其与app.use()或者router.use()绑定。中间件的顺序取决于.use()方法用于绑定中间件的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require("express");
const app = new express();

function myLogger(req, res, next) {
req.timeReceived = Date();
next();
}

app.use(myLogger);

app.get("/", (req, res) => {
res.send("Request received at " + req.timeReceived + " is a success!");
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

3.2.5. 模板渲染

模板渲染是服务器在HTML模板中填充动态内容的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require("express");
const app = new express();
const expressReactViews = require("express-react-views");

const jsxEngine = expressReactViews.createEngine();

app.set("view engine", 'jsx'); // views are JSX code

app.set("views", "myviews"); // the views are in a directory named myviews

app.engine("jsx", jsxEngine);

app.get("/:name", (req, res) => {
res.render("index", {name: req.params.name});
});

app.listen(3333, () => {
console.log(`Listening at http://localhost:3333`);
});

本代码示例使用了express-react-views软件包,它是一个用于渲染React视图的Express模板引擎。

3.3. 验证

身份验证是通过获取凭证并使用这些凭证验证用户身份的过程。身份验证的目的是识别用户身份,并根据其身份提供访问权限和内容。

身份验证可以通过以下方法实现:

  • 基于会话
  • 基于令牌
  • 无密码

3.3.1. 基于令牌的身份验证

基于令牌的身份验证是在Node.js中实施身份验证的最常见方法。由于令牌只需存储在客户端,所以基于令牌的身份验证更具有可扩展性;服务器只需要验证令牌和用户信息,因此更容易处理多个用户;其灵活性能够在多个服务器上实现身份验证。基于令牌的身份验证中使用的JWT可以签名和加密,这意味着它们不会被篡改、没有私人加密密钥便无法读取。

让我们建立一个Express.js API服务器、根据使用权限访问员工信息。应用程序将有两个API,每个API都有自己的端点:

  1. 使用POST API登录,通过在请求体中发送用户名和密码、返回网页令牌(对该应用程序接口端点的调用应当通过托管应用程序前端的网页服务器来进行)
  2. GET API将获取只有通过身份验证的用户才能访问的员工信息:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const express = require("express");
    const myapp = express();

    // creates a web server module by calling the express() function and assigning it to the constant myapp
    // the myapp.get() function creates a GET API endpoint for the Employees API, and any call to this endpoint currently returns an HTTP status code of 401
    // 401 means Not Authorized
    myapp.get("/employees". (req, res) => {
    return res.status(401).json({message: "Please login to access this resource"});
    });

    myapp.listen(5000, () => {
    console.log("API Server is localhost:5000");
    });
    在代码的下一部分,只要用户名和密码正确,我们就允许用户登录,并返回经过验证生成的令牌。一般来说,用户名和密码都存储在数据库中。但是,为了简单起见,我们将在代码中使用“user”和“password”作为用户名和密码。
    要生成经过验证的JWT(JSON Web Token),要使用jsonwebtoken包:
    1
    2
    3
    4
    const express = require("express");
    const jsonwebtoken = require("jsonwebtoken");

    const JWT_SECRET = "aVeryVerySecretString";
    通过myapp.use()方法,API方法可以返回JSON响应:
    1
    2
    3
    4
    5
    6
    7
    const myapp = express();
    myapp.use(express.json());

    myapp.post("/signin", (req, res) => {
    const {uname, pwd} = req.body;
    // however, that the JWT Secret should always be generated using a password generator and stored in the config file as an environment variable and not hard coded in the API, as shown here
    });
    然后将请求正文中的用户名和密码与从数据库中获取的值进行比较:
    1
    2
    3
    4
    5
    6
    7
    8
    if (uname === "user" && pwd === "password") {
    return res.json({
    // Once the username and password match, the JWT is generated using the jsonwebstoken.sign() function by including the username and the JWT secret as parameters and is returned as a JSON response from the signin API
    token: jsonwebtoken.sign({user: "user"}, JWT_SECRET)
    });
    // if the username and password match fails, then a HTTP status code of 401 is returned with the message "Invalid username and/or password"
    return res.status(401).json({message: "Invalid username and/or password"});
    });
    接着,我们用“employees”端点定义GET API方法:
    1
    2
    3
    4
    5
    6
    7
    8
    myapp.get("/employees", (req, res) => {
    let tkn = req.header("Authorization");
    if (!tkn) return res.status(401).send("No Token");
    if (tkn.startsWith("Bearer ")) {
    tokenValue = tkn.slice(7, tkn.length).trimLeft();
    }
    // ...
    });
    signin API调用中获取的令牌会在Authorization标头中传递。GET API(也就是“employees”)已更新,可以使用req.header()函数从传入的API请求中读取Authorization标头。值得注意的是,Authorization标头的值总是以Bearer开头,因此这个令牌也被称为Bearer令牌。
    获取的JWT可通过传递获取的令牌和JWT密钥、使用函数jsonwebtoken.verify()进行验证:
    1
    2
    3
    4
    5
    6
    7
    myapp.get("/employees", (req, res) => {
    // ...
    const verificationStatus = jsonwebtoken.verify(tokenValue, "aVeryVerySecretString");
    if (verificationStatus.user === "user") {
    return res.status(200).json({message: "Access Successful to Employee Endpoint"});
    }
    });
    如果验证失败,则会将401状态码返回给客户端。