Web前端开发(7) Node.js
2023-08-09 14:53:19 # NJU # Web前端开发

9-Node.js

1. 服务端编程

静态网页与动态网页

静态页面

  • 客户/消费者的观点: 一个url指向同一个html文件
  • 服务器/生产者的观点: 存储在Web服务器根文件夹内或子文件夹内的文件
  • HTML文件: 无论何时当一个特定资源被请求的时候都返回相同的被硬编码的内容
  • 可以直接在浏览器上显示

动态页面

  • 客户/消费者的观点: url指的是动态html(可能每次请求都不同)
  • 服务器/生产者的观点: 程序/脚本生成html
  • 它不是一个html, 而是一个程序产生的html(s), 页面通常是通过将数据库的数据植入到HTML模板中的占位符中而产生的。
  • 不能直接在浏览器中显示

服务端编程

  • 服务器端页面是使用多种web编程语言/框架之一编写的程序
    • 例如: PHP, Java/JSP, Ruby on Rails, ASP.NET, Python, Perl
  • 每种语言/框架都有其优点和缺点

服务端编程的优势

  • 信息的高效存储和传输、定制用户体验、控制对内容的访问、存储会话和状态信息、通知和通讯、数据分析

2. Node.js

Node.js

  • Node(正式名称 Node.js)是一个开源的、跨平台的运行时环境
  • 服务端Javascript: 开发人员可以使用 JavaScript 创建各种服务器端工具和应用程序
  • 事件驱动, 异步I/O框架
  • 性能: 在v8之上的c++内核
  • 在单一进程中可以最小的开销(cpu/内存)处理成千上万的并发连接
  • 模块系统
  • 不是一个web框架, 也不是一种语言

Node.js is a platform built on Chrome’s JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices. — from nodejs.org

Node.js 的诞生

image-20230126145036774

Node.js 现状

  • 2018 年 5 月 31 日, Node.js 基⾦会发布的用户调查报告, 显示学习 Node.js 看起来更容易了, 少于 2 年 node 经验的用户中, 有 43% 的觉得”容易”。绝大多数(85%)Node.js 用户用于网页开发, 43% 参与一些企业级开发, 13% 用于大数据分析, 8% 用于嵌入式系统。

Node.js 应用场景

  • 网站(如express/koa等)
  • im即时聊天(socket.io)
  • api(移动端, pc, h5)
  • HTTP Proxy(淘宝、Qunar、腾讯、百度都有)
  • 前端构建工具(grunt/gulp/bower/webpack/fis3…)
  • 写操作系统(NodeOS)
  • 跨平台打包工具(PC端的electron、nw.js, 比如钉钉PC客户端、微信小程序IDE、微信客户端, 移动的cordova, 即老的 Phonegap, 还有更加有名的一站式开发框架ionicframework)
  • 命令行工具(比如cordova、shell.js)
  • 反向代理(比如anyproxy, node-http-proxy)

Node 优势

从 web 服务器开发的⻆度来看, Node 有很多好处:

  • 卓越的性能表现!Node 为优化 web 应用的吞吐量和扩展度而生, 对常见的 web 开发问题是一套绝佳方案(比如实时 web 应用)
  • 代码还是熟悉的老伙伴 JavaScript, 这意味着在客户端和服务器端”上下文切换”的时间成本更低
  • 与传统的 web 服务器语言(例如 Python、PHP 等)相比, JavaScript 理念更新, 语言设计的改进带来了诸多好处。许多其它新近流行的语言也可编译/转换成 JavaScript, 所以 TypeScript、CoffeeScript、ClojureScript、Scala、LiveScript 等等也可以使用
  • Node 包管理工具(node package manager, NPM)提供了数十万个可重用的工具包。它还提供了一流的依赖解决方案, 可实现自动化工具链构建
  • Node.js 是可移植的, 可运行于 Microsoft Windows、macOS、Linux、Solaris、FreeBSD、OpenBSD、WebOS 和 NonStop OS。此外, 许多 web 主机供应商对其提供了良好支持(包括专用的基础框架和构建 Node 站点的文档)
  • 它有一个非常活跃的第三方生态系统和开发者社区, 很多人愿意提供帮助

何时使用

  • Node所针对的应用程序有一个专门的简称: DIRT, 表示数据密集型实时(data-intensive real-time)程序
  • 性能和I/O负载: Nodejs非常好的解决了IO密集的问题, 通过异步IO来实现
  • 大前端的基石
  • 从脚⼿架、辅助前端开发(比如 SSR、PWA 等)的快速开发实践, 到 API 中间层、代理层, 到专业的后端开发都有非常成熟的经验
  • 全栈

Node.js 不适合的领域

  • 计算密集型应用
  • 内存控制
  • 大内存的应用, 由于V8引擎有内存设计的限制, 32位环境中最大堆是 1G, 64位环境中最大堆也不到2G, 如果要一次读入 10G 数据, 对于 Nodejs来说也无法实现
  • 静态服务器, 虽然Nodejs的优势在IO密集集应用, 但是和 Nginx 的处理静态资源还是有很大的差距
  • 不需要异步的应用: 比如系统管理, 自行化脚本等, Nodejs的异步调用可能会给编程带来一些麻烦。

3. 事件循环

Node.js 事件循环

image-20230126155638013

There are a couple of implications of this apparently very simple and basic model Avoid synchronous code at all costs because it blocks the event loop Which means: callbacks, callbacks, and more callbacks

事件驱动模型

  • 由一个事件收集器、一个事件发送器和一个事件处理器组成

image-20230126160013635

image-20230126160035582

image-20230126160042277

Node.js 支持 JS

这是 Node.js 能够发展壮大的一个非常重要的间接原因

  • 首先, Javascript 作为前端工程师的主⼒语言, 在技术社区中有相当的号召⼒。而且, 随着 Web 技术的不断发展, 特别是前端的重要性增加, 不少前端工程师开始试水”后台应用”, 在许多采用 Node.js 的企业中, 工程师都表示因为习惯了 Javascript, 所以选择 Node.js
  • 其次, Javascript 的匿名函数和闭包特性非常适合事件驱动、异步编程
  • 有 Google V8 引擎的加持, Node.js 的性能也是受益其中。

4. 阻塞

同步 vs 异步

同步 异步
等待每个操作完成, 然后执行下一个操作。 从不等待每个操作完成, 一次执行所有操作。
一步一步执行 回调, 用于处理结果

同步式 I/O 和异步式 I/O 的特点

  • 阻塞方法同步执行, 非阻塞方法异步执行
同步式 I/O(阻塞式) 异步式 I/O(非阻塞式)
利用多线程提供吞吐量 单线程即可实现高吞吐量
通过事件片分割和线程调度利用多核CPU 通过功能划分利用多核CPU
需要由操作系统调度多线程使用多核 CPU 可以将单进程绑定到单核 CPU
难以充分利用 CPU 资源 可以充分利用 CPU 资源
内存轨迹大, 数据局部性弱 内存轨迹小, 数据局部性强
符合线性的编程思维 不符合传统编程思维

阻塞 vs 非阻塞

  • 阻塞 是指在 Node.js 程序中, 其它 JavaScript 语句的执行, 必须等待一个非 JavaScript 操作完成。这是因为当阻塞发生时, 事件循环无法继续运行 JavaScript
  • 在 Node.js 中, JavaScript 由于执行 CPU 密集型操作, 而不是等待一个非 JavaScript 操作(例如 I/O)而表现不佳, 通常不被称为阻塞。在 Node.js 标准库中使用 libuv 的同步方法是最常用的阻塞操作。原生模块中也有阻塞方法
  • 在Node.js标准库中的所有I/O方法都提供异步版本, 非阻塞, 并且接受回调函数。某些方法也有对应的阻塞版本, 名字以 Sync 结尾

  • 示例: 从文件中读取数据并显示数据

    image-20230126162814121

阻塞

  • 同步
  • 文件读取
    • 从文件读取数据
    • 显示数据
    • 完成其他任务
1
2
3
var data = fs.readFileSync("test.txt");
console.log(data);
console.log("Do other tasks");

非阻塞

  • 异步
  • 从文件中读取数据
    • 当读取数据完成时, 显示数据
  • 完成其他任务
1
2
3
fs.readFile("test.txt", function(err, data) {
console.log(data); // callback
});

5. 实践

package.json

项目信息

  • name 项目名称
  • version 项目的版本号
  • description 项目的描述信息
  • entry point 项目的入口文件
  • test command 项目启动时脚本命令
  • git repository 如果你有 Git 地址, 可以将这个项目放到你的 Git 仓库⾥
  • keywords 关键词
  • author 作者
  • license 项目要发行的时候需要的证书, 平时可以忽略它

Node.js API

image-20230126210438297

Node.js流行模块

  • Express.js 由核⼼ Node 项目团队的成员之一 TJ Holowaychuk 构建。大型社区支持此框架, 因此具有不断更新和改⾰所有核⼼功能的优势。这是一个极简主义的框架, 用于构建 mobile 应用程序和 API
  • Koa 由创建 Express.js 的同一团队开发, 通常被称为下一代 NodeJS 框架。 Koa 的独特之处在于它使用了一些非常酷的 ECMAScript (ES6)方法, 使你无需回调即可工作, 同时极大地扩展了错误处理
  • Hapi 是一个强大且健壮的框架, 用于开发API。完善的插件系统和各种关键功能(例如输入验证、基于配置的功能、实现缓存、错误处理、日志记录等)使 Hapi 成为最受欢迎的框架之一。它用于构建有用的应用, 并通为 PayPal, Disney 等多个大型网站提供技术解决方案。Hapi 以最小的开销构建安全、强大、可扩展的开箱即用的功能
  • Socket.io用于构建实时 Web 应用。这是一个 Javascript 库, 可在 Web 客户端和服务器之间进行双向数据通信。异步数据 I/O、⼆进制流和即时消息传递是此框架最重要的功能
  • Meteor.JS 是最常用的 NodeJS 框架之一。 NodeJS 的全栈框架, 允许用户构建实时应用程序。它用于创建基于移动和基于 Web 的 JavaScript 应用。

6. 模块

模块

两种模块, 不兼容

  • ES6 模块, 简称 ESM
  • Node.js 专用的 CommonJS 模块, 简称 CJS

ES6 模块与 CommonJS 模块的差异

语法

  • CommonJS 模块使用 require() 加载和 module.exports 输出
  • ES6 模块使用 importexport
1
2
3
4
5
6
7
8
9
10
11
12
var http = require('http');
var fs = require('fs');
var express = require('express');

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
// main.js
import {firstName, lastName, year} from './profile';
console.log(firstName, lastName) // Michael Jackson

CommonJS 模块输出的是一个值的拷贝, ES6 模块输出的是值的引用

CommonJS 模块是运行时加载, ES6 模块是编译时输出接口

  • CommonJS 加载的是一个对象(即module.exports属性), 该对象只有在脚本运行完才会生成
  • ES6 模块不是对象, 它的对外接口只是一种静态定义, 在代码静态解析阶段就会生成

模块加载的实质

cjs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3

es6

1
2
3
4
5
6
7
8
9
10
// lib.mjs
export let counter = 3;
export function incCounter() {
counter++;
}
// main.mjs
import { counter, incCounter } from './lib.mjs';
console.log(counter); // 3
incCounter();
console.log(counter); // 4

Node.js 的区分

  • Node.js 要求 ES6 模块采用 .mjs 后缀文件名
    • 只要脚本文件⾥面使用 import 或者 export 命令, 那么就必须采用.mjs后缀名
    • Node.js 遇到.mjs文件, 就认为是 ES6 模块, 默认启用严格模式, 不必在每个模块文件顶部指定”use strict”
    • 如果不希望将后缀名改成.mjs, 可以在项目的package.json文件中, 指定 type 字段为 module
  • .cjs 文件总是以 CommonJS 模块加载
    • 如果没有 type 字段, 或者 type 字段为 commonjs, 则 .js 脚本会被解释成 CommonJS 模块
  • .js 文件的加载取决于 package.json ⾥面 type 字段的设置
  • 注意, ES6 模块与 CommonJS 模块尽量不要混用!!!
    • require 命令不能加载 .mjs 文件, 会报错, 只有 import 命令才可以加载 .mjs 文件
    • 反之, .mjs 文件⾥面也不能使用 require 命令, 必须使用 import

同时支持两种格式的模块(了解)

  • 如果原始模块是 ES6 格式, 那么需要给出一个整体输出接口, 比如 export default obj, 使得 CommonJS 可以用 import() 进行加载

  • 如果原始模块是 CommonJS 格式, 那么可以加一个包装层

    1
    2
    import cjsModule from '../index.js';
    export const foo = cjsModule.foo;
  • 可以把这个文件的后缀名改为 .mjs, 或者放在一个子目录, 再在这个子目录⾥面放一个单独的 package.json 文件, 指明{ type: “module” }

  • 另一种做法是在 package.json 文件的 exports 字段, 指明两种格式模块各自的加载入口

    1
    2
    3
    4
    "exports": {
    "require": "./index.js",
    "import": "./esm/wrapper.js"
    }

require 方法查找策略

image-20230127150018532

A simple file server

1
2
3
4
5
6
7
var http = require("http");
var server = http.createServer(function(req, res){
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n')
});
server.listen(3000);
console.log('Server running at http://localhost:3000/');
1
2
3
node server.js
sudo npm install -g supervisor
supervisor server.js

as

1
2
3
4
5
6
//main.js
import { lastName as surname } from './profile';
console.log(surname); // Jackson

//profile.js
export {firstName as name}

export default

1
2
3
4
5
6
7
8
9
10
11
//default.js
function add(a, b){
return a + b;
}
export default add;
// 实际上
export {add as default};
// main.js
import add from './default'
//实际上 add 名字可以随便起
import {default as add} from './default'

7. 循环加载

CommonJS 循环加载

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
//a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中, b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');

//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中, a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');

//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);

/*
在 b.js 之中, a.done = false
b.js 执行完毕
在 a.js 之中, b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
*/

CommonJS模块的重要特性是加载时执行, 即脚本代码在require的时候, 就会全部执行。CommonJS的做法是, 一旦出现某个模块被”循环加载”, 就只输出已经执行的部分, 还未执行的部分不会输出。

ES6 循环加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// a.mjs如下
import {bar} from './b.mjs';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';

// b.mjs
import {foo} from './a.mjs';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';

/*
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: Cannot access 'foo' before initialization
*/

借助函数提升解决, 改为函数表达式也会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// a.mjs
import {bar} from './b.mjs';
console.log('a.mjs');
console.log(bar());
function foo() { return 'foo' }
export {foo};
// b.mjs
import {foo} from './a.mjs';
console.log('b.mjs');
console.log(foo());
function bar() { return 'bar' }
export {bar};

/*
$ node --experimental-modules a.mjs
b.mjs
foo
a.mjs
bar
*/

8. 其他

Node.js 版本

  • LTS 和 Current 其实并不是版本, 而是同一个主版本号的不同阶段
    • LTS: Long Term Support。该版本进入了漫长的维护期。它又分为两个阶段: Active LTS 和 Maintenance LTS。从以往的发布历史看, LTS 至少会被跟进 2 年时间, 按照最新的官方网站的说法, Active LTS 持续 12 个月, Maintenance LTS 将会被持续维护 18 个月的时间。Node.js 12 之前, active 阶段持续18个月, maintenance 阶段持续 12 个月。
    • Current: 一个新主版本号 release 后, 先进入 Current 阶段, 该阶段持续 6 个月, 目的是给各个库(library)的作者时间来支持新版。偶数版本在 Current 阶段后进入 LTS 阶段, 而奇数版本则终结不再维护
  • 奇偶版本号
    • Nodejs主版本号(semver-major)奇数版本和偶数版本有不同的生命周期
    • 每隔6个月, 社区会从 Nodejs master 分支拉出一个分支作为主版本的release。偶数版本在4月发版, 奇数版本则在10月
    • 奇数版本发版时, 上一个偶数版本会进入LTS阶段, 而奇数版本则只持续6个月的时间, 终结不再维护

已无性能优势?

  • 实现成本、调优成本、学习成本

image-20230128113254724

MEAN

MEAN 是一个 Javascript 平台的现代 Web 开发框架总称

  • MongoDB 是一个使用 JSON 风格存储的数据库, 非常适合 JavaScript (JSON是JS数据格式)
  • ExpressJS 是一个 Web 应用框架, 提供有帮助的组件和模块帮助建立一个网站应用。
  • AngularJS 是一个前端 MVC 框架。
  • Node.js 是一个并发、异步、事件驱动的Javascript服务器后端开发平台。

MEAN 架构原理

image-20230128113438817

Web 框架

image-20230128113515334

Node.js 框架的优势

现在 NodeJS 框架正在成为最常用的构建 Web 应用前后端的开发框架。这是自定义 Web 开发的首选环境。让我们检查一些主要的 NodeJS 框架的优点

  • 实时工作环境、简单的编码经验、无缝数据流、在整个开发过程中使用相同的代码模式、方便易用

框架选型

  • 业务场景、特点
  • 自身团队能⼒、喜好, 有时候技术选型决定团队氛围的, 需要平衡激进与稳定
  • 熟悉程度
  • 个人学习求新, 企业架构求稳, 无非喜好与场景而已

预处理器

image-20230128113646359

跨平台

image-20230128140941232

其他

image-20230128141012898

10-Express

1. 简介

什么是 Express ?

  • Express是最流行的node web框架, 它是许多其他流行的节点 web 框架的底层库。它提供了机制:
    • 在不同的URL路径(路由)中使用不同HTTP动词的请求编写处理程序
    • 与”视图”呈现引擎集成, 以便通过将数据插入模板来生成响应
    • 设置常见的web应用程序设置, 比如用于连接的端口, 以及用于呈现响应的模板的位置
    • 在请求处理管道的任何位置添加额外的请求处理”中间件”
  • 虽然Express本身是非常简单的, 但是开发人员已经创建了兼容的中间件包来解决几乎所有的 web 开发问题
    • cookie、会话、用户登录、URL参数、POST数据、安全标头等等。

特点

  • 精简、灵活、web 程序框架、单页 web 程序、多页和混合的 web 程序

Is Express opinionated?

  • Web框架通常将自己称为”固执己见”或”不固执己见”
  • 固执的框架认为应该有一套”标准答案”来解决各类具体任务。通常支持特定领域的快速开发(解决特定类型的问题)。因为标准答案通常易于理解且文档丰富。然而在解决主领域之外的问题时, 就会显得不那么灵活, 可用的组件和方法也更少
  • Express是不固执己见的,是高度包容的
    • 几乎可以将任何您喜欢的任何兼容的中间件插入到请求处理链中
    • 可以在一个文件或多个文件中构造该应用程序, 并使用任何目录结构
    • 有太多的选择!

Express 开发环境概述

完整的 Express 本地开发环境包括

  • Nodejs
  • NPM 包管理器
  • Express 应用生成器(可选)

Hello world

1
2
3
4
5
6
7
8
9
10
11
// npm init
// npm install express —save
// app.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});

应用生成器工具

通过应用生成器工具 express-generator 可以快速创建一个应用的⻣架

image-20230129002408387

文件结构

image-20230129002447793

2. 路由

处理数据流

  • 下图展示了 HTTP 请求/响应处理的主数据流和需要实现的行为
  • 路由: 把需要支持的请求(以及请求 URL 中包含的任何信息)转发到适当的控制器函数
  • 控制器: 从模型中获取请求的数据, 创建一个 HTML 页面显示出数据, 并将页面返回给用户, 以便在浏览器中查看
  • 视图(模板): 供控制器用来渲染数据。

image-20230129002557988

基本路由

  • 路由用于确定应用程序如何响应对特定端点的客户机请求, 包含一个 URI(或路径)和一个特定的 HTTP 请求方法(GET、POST 等)
  • 每个路由可以具有一个或多个处理程序函数, 这些函数在路由匹配时执行
  • 路由定义采用以下结构:
    • app.METHOD(PATH, HANDLER)
    • 其中:
      • app 是 express 的实例
      • METHOD 是 HTTP 请求方法
      • PATH 是服务器上的路径
      • HANDLER 是在路由匹配时执行的函数。

路由示例

以主页上的 Hello World! 进行响应:

1
2
3
app.get('/', function (req, res) {
res.send('Hello World!');
});

在根路由 (/) 上(应用程序的主页)对 POST 请求进行响应:

1
2
3
app.post('/', function (req, res) {
res.send('Got a POST request');
});

对 /user 路由的 PUT 请求进行响应:

1
2
3
app.put('/user', function (req, res) {
res.send('Got a PUT request at /user');
});

路由方法

路由方法派生自 HTTP 方法之一, 附加到 express 类的实例

1
2
3
4
5
6
7
8
// GET method route
app.get('/', function (req, res) {
res.send('GET request to the homepage');
});
// POST method route
app.post('/', function (req, res) {
res.send('POST request to the homepage');
});

特殊路由方法: app.all()

  • 有一种特殊路由方法: app.all(), 它并非派生自 HTTP 方法。该方法用于在所有请求方法的路径中装入中间件函数
  • 在以下示例中, 无论使用 GET、POST、PUT、DELETE 还是在 http 模块中支持的其他任何 HTTP 请求方法, 都将为针对”/ secret”的请求执行处理程序
1
2
3
4
app.all('/secret', function (req, res, next) {
console.log('Accessing the secret section ...');
next(); // pass control to the next handler
});

路由路径

  • 路由路径与请求方法相结合, 用于定义可以在其中提出请求的端点。路由路径可以是字符串、字符串模式或正则表达式

  • 路由路径用于定义可请求的端点。之前示例中路径都是字符串, 并且必须精确写为: ‘/‘、’/about’、’/book’等等

  • 路由路径也可以是字符串模式(String Pattern)。可用部分正则表达式语法来定义端点的模式。以下是所涉及的正则表达式(注意, 连字符(-)和点(.)在字符串路径中解释为字面量, 不能作为正则表达式):

    • ?: 问号之前的一个字符只能出现零次或一次。
      • 例如, 路由路径 ‘/ab?cd’ 路径匹配端点 acd 或 abcd
    • +: 加号之前的一个字符至少出现一次
      • 例如, 路径路径 ‘/ab+cd’ 匹配端点 abcd、abbcd、abbbcd 等
    • *: 星号可以替换为任意字符串
      • 例如, 路由路径 ‘/ab*cd’ 匹配端点 abcd、abXcd、abSOMErandomTEXTcd 等
    • (): 将一个字符串视为一体以执行 ?、+、 * 操作
      • 例如。 ‘/ab(cd)?e’ 将对 (cd) 进行匹配, 将匹配到 abe 和 abcde
  • 路由路径也可以是 JavaScript 正则表达式。

    • 例如, 下面的路由路径将匹配 catfish 和 dogfish, 但不会匹配 catflap、catfishhead 等。

      1
      2
      3
      app.get(/.*fish$/, (req, res) => {
      ...
      });
    • 注意, 正则表达式路径不再用引号 “…” 括起来, 而是正则表达式语法 /…/

  • Express 使用 path-to-regexp 来匹配路由路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 此路由路径将请求与根路由 / 匹配。
app.get('/', function (req, res) {
res.send('root');
});
// 此路由路径将请求与 /about 匹配。
app.get('/about', function (req, res) {
res.send('about');
});
// 此路由路径将请求与 /random.text 匹配。
app.get('/random.text', function (req, res) {
res.send('random.text');
});
// 基于字符串模式的路由路径的示例。此路由路径将匹配 acd 和 abcd。
app.get('/ab?cd', function(req, res) {
res.send('ab?cd');
});

路由参数

  • 路径参数是命名的 URL 段, 用于捕获在 URL 中的位置指定的值。命名段以冒号为前缀, 然后是名称
    • 例如 /:your_parameter_name/。捕获的值保存在 req.params 对象中, 键即参数名
      • 例如 req.params.your_parameter_name
  • 路由参数名必须由”单词字符”(/[A-Za-z0-9_]/)组成
  • 举例说, 一个包含用户和藏书信息的 URL: http://localhost:3000/users/34/books/8989, 可以这样提取信息(使用 userId 和 bookId 路径参数):
1
2
3
4
5
app.get('/users/:userId/books/:bookId', (req, res) => {
// 通过 req.params.userId 访问 userId
// 通过 req.params.bookId 访问 bookId
res.send(req.params);
});

路由处理程序

  • 可以提供多个回调函数, 以类似于中间件的行为方式来处理请求。唯一例外是这些回调函数可能调用 next(‘route’)来绕过剩余的路由回调。可以使用此机制对路由施加先决条件, 在没有理由继续执行当前路由的情况下, 可将控制权传递给后续路由
  • 路由处理程序的形式可以是一个函数、一组函数或者两者的结合, 如以下示例中所示
  • 单个回调函数可以处理一个路由。例如:
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
31
32
33
34
35
36
37
38
39
40
41
42
// 单个回调函数可以处理一个路由。例如: 
app.get('/example/a', function (req, res) {
res.send('Hello from A!');
});

// 多个回调函数可以处理一个路由(确保您指定 next 对象)。例如:
app.get('/example/b', function (req, res, next) {
console.log('the response will be sent by the next function ...');
next();
}, function (req, res) {
res.send('Hello from B!');
});

// 一组回调函数可以处理一个路由。例如:
var cb0 = function (req, res, next) {
console.log('CB0');
next();
}
var cb1 = function (req, res, next) {
console.log('CB1');
next();
}
var cb2 = function (req, res) {
res.send('Hello from C!');
}
app.get('/example/c', [cb0, cb1, cb2]);

// 独立函数与一组函数的组合可以处理一个路由。例如:
var cb0 = function (req, res, next) {
console.log('CB0');
next();
}
var cb1 = function (req, res, next) {
console.log('CB1');
next();
}
app.get('/example/d', [cb0, cb1], function (req, res, next) {
console.log('the response will be sent by the next function ...');
next();
}, function (req, res) {
res.send('Hello from D!');
});

响应方法

下表中响应对象 (res) 的方法可以向客户机发送响应, 并终止请求/响应循环。如果没有从路由处理程序调用其中任何方法, 客户机请求将保持挂起状态。

方法 描述
res.download() 提示将要下载文件
res.end() 结束响应进程
res.json() 发送 JSON 响应
res.jsonp() 在 JSONP 的支持下发送 JSON 响应
res.redirect() 重定向请求
res.render() 呈现视图模板
res.send() 发送各种类型的响应
res.sendFile() 以⼋位元流形式发送文件
res.sendStatus() 设置响应状态码并以响应主体形式发送其字符串表示

app.route()

  • 可以使用 app.route() 为路由路径创建链式路由处理程序。因为在单一位置指定路径, 所以可以减少冗余和输入错误。有关路由的更多信息, 请参阅 Router()文档。 以下是使用 app.route() 定义的链式路由处理程序的示例。
1
2
3
4
5
6
7
8
9
10
app.route('/book')
.get(function(req, res) {
res.send('Get a random book');
})
.post(function(req, res) {
res.send('Add a book');
})
.put(function(req, res) {
res.send('Update the book');
});

express.Router

  • 使用 express.Router 类来创建可安装的模块化路由处理程序
  • Router 实例是完整的中间件和路由系统;因此, 常常将其称为”微型应用程序”
  • 以下示例将路由器创建为模块, 在其中装入中间件, 定义一些路由, 然后安装在主应用程序的路径中
  • 在应用程序目录中创建名为 birds.js 的路由器文件, 其中包含以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var router = express.Router();
// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
console.log('Time: ', Date.now());
next();
});
// define the home page route
router.get('/', function(req, res) {
res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
res.send('About birds');
});

module.exports = router;
  • 接着, 在应用程序中装入路由器模块:
1
2
3
var birds = require('./birds');
...
app.use('/birds', birds);

3. 中间件

编写中间件

  • Connect 创造了”中间件”(middleware)这个术语来描述插入式的 Node 模块
  • 从概念上讲, 中间件是一种功能的封装方式, 具体来说就是封装在程序中处理 HTTP 请求的功能
  • 中间件是在管道中执行的。
    • 在 Express 程序中, 通过调用 app.use() 向管道中插入中间件。

image-20230129145316636

Express工作重点

  • 路由处理器(app.get、app.post 等, 经常被统称为app.VERB)可以被看作只处理特定 HTTP动词 (GET、POST等)的中间件。同样, 也可以将中间件看作可以处理全部 HTTP动词 的路由处理器(基本上等同于app.all, 可以处理任何HTTP动词; 对于 PURGE 之类特别的动词会有细微的差别, 但对于普通的动词而言, 效果是一样的)
  • 路由处理器的第一个参数必须是路径。如果你想让某个路由匹配所有路径, 只需用/* 。中间件也可以将路径作为第一个参数, 但它是可选的(如果忽略这个参数, 它会匹配所有路径, 就像指定了 /* 一样)
  • 路由处理器和中间件的参数中都有回调函数, 这个函数有2个、3个或4个参数(从技术上讲也可以有 0 或 1 个参数, 但这些形式没有意义)
    • 如果有 2 个或 3 个参数, 头两个参数是请求和响应对象, 第三个参数是 next 函数
    • 如果有 4 个参数, 它就变成了错误处理中间件, 第一个参数变成了错误对象, 然后依次是请求、响应和 next 对象
  • 如果不调用 next(), 管道就会被终止, 也不会再有处理器或中间件做后续处理。如果不调用 next(), 则应该发送一个响应到客户端(res.send、res.json、res.render 等); 如果你不这样做, 客户端会被挂起并最终导致超时
  • 如果调用了 next(), 一般不宜再发送响应到客户端。如果你发送了, 管道中后续的中间件或路由处理器还会执行, 但它们发送的任何响应都会被忽略

中间件函数的简单示例

  • 此函数仅在应用程序的请求通过它时显示”LOGGED”。中间件函数会分配给名为 myLogger 的变量
  • 请注意以上对 next() 的调用
    • 调用此函数时, 将调用应用程序中的下一个中间件函数
    • next() 函数不是 Node.js 或 Express API 的一部分, 而是传递给中间件函数的第三自变量
    • next() 函数可以命名为任何名称, 但是按约定, 始终命名为”next”
    • 为了避免混淆, 请始终使用此约定。
1
2
3
4
var myLogger = function (req, res, next) {
console.log('LOGGED');
next();
};
  • 要装入中间件函数, 请调用 app.use() 并指定中间件函数。例如, 以下代码在根路径 (/) 的路由之前装入 myLogger 中间件函数
1
2
3
4
5
6
7
8
9
10
11
var express = require('express');
var app = express();
var myLogger = function (req, res, next) {
console.log('LOGGED');
next();
};
app.use(myLogger);
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000);
  • 中间件装入顺序很重要
    • 首先装入的中间件函数也首先被执行
  • 如果在根路径的路由之后装入 myLogger, 那么请求永远都不会到达该函数, 应用程序也不会显示”LOGGED”, 因为根路径的路由处理程序终止了请求/响应循环
  • 中间件函数 myLogger 只是显示消息, 然后通过调用 next() 函数将请求传递到堆栈中的下一个中间件函数

示例

名为 requestTime 的属性添加到请求对象

1
2
3
4
var requestTime = function (req, res, next) {
req.requestTime = Date.now();
next();
};

现在, 该应用程序使用 requestTime 中间件函数。此外, 根路径路由的回调函数使用由中间件函数添加到 req(请求对象)的属性

1
2
3
4
5
6
7
app.use(requestTime);
app.get('/', function (req, res) {
var responseText = 'Hello World!';
responseText += 'Requested at: ' + req.requestTime + '';
res.send(responseText);
});
app.listen(3000);

使用中间件

  • Express 是一个路由和中间件 Web 框架, 其自身只具有最低程度的功能:
    • Express 应用程序基本上是一系列中间件函数调用
  • 中间件函数能够访问请求对象 (req)、响应对象 (res) 以及应用程序的请求/响应循环中的下一个中间件函数。下一个中间件函数通常由名为 next 的变量来表示
  • 中间件函数可以执行以下任务:
    • 执行任何代码
    • 对请求和响应对象进行更改
    • 结束请求/响应循环
    • 调用堆栈中的下一个中间件函数
  • 如果当前中间件函数没有结束请求/响应循环, 那么它必须调用 next(), 以将控制权传递给下一个中间件函数。否则, 请求将保持挂起状态。

  • Express 应用程序可以使用以下类型的中间件:

    • 应用层中间件
    • 路由器层中间件
    • 错误处理中间件
    • 内置中间件
    • 第三方中间件
  • 可以使用可选安装路径来装入应用层和路由器层中间件。还可以将一系列中间件函数一起装入, 这样会在安装点创建中间件系统的子堆栈。

应用层中间件

  • 使用 app.use()app.METHOD() 函数将应用层中间件绑定到应用程序对象的实例, 其中 METHOD 是中间件函数处理的请求的小写 HTTP 方法(例如 GET、PUT 或 POST)
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
// 此示例显示没有安装路径的中间件函数。应用程序每次收到请求时执行该函数。
var app = express();
app.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});

// 此示例显示安装在 /user/:id 路径中的中间件函数。在 /user/:id 路径中为任何类型的 HTTP 请求执行此函数。
app.use('/user/:id', function (req, res, next) {
console.log('Request Type:', req.method);
next();
});

// 此示例显示一个路由及其处理程序函数(中间件系统)。此函数处理针对 /user/:id 路径的 GET 请求。
app.get('/user/:id', function (req, res, next) {
res.send('USER');
});

// 在安装点使用安装路径装入一系列中间件函数的示例。演示一个中间件子堆栈, 用于显示针对 /user/:id 路径的任何类型 HTTP 请求的信息。
app.use('/user/:id', function(req, res, next) {
console.log('Request URL:', req.originalUrl);
next();
}, function (req, res, next) {
console.log('Request Type:', req.method);
next();
});
  • 路由处理程序可以为一个路径定义多个路由。以下示例为针对 /user/:id 路径的 GET 请求定义两个路由。第⼆个路由不会导致任何问题, 但是永远都不会被调用, 因为第一个路由结束了请求/响应循环。

  • 此示例显示一个中间件子堆栈, 用于处理针对 /user/:id 路径的 GET 请求。

1
2
3
4
5
6
7
8
9
10
app.get('/user/:id', function (req, res, next) {
console.log('ID:', req.params.id);
next();
}, function (req, res, next) {
res.send('User Info');
});
// handler for the /user/:id path, which prints the user ID
app.get('/user/:id', function (req, res, next) {
res.end(req.params.id);
});
  • 要跳过路由器中间件堆栈中剩余的中间件函数, 请调用 next('route') 将控制权传递给下一个路由

  • next(‘route’) 仅在使用 app.METHOD()router.METHOD() 函数装入的中间件函数中有效

  • 此示例显示一个中间件子堆栈, 用于处理针对 /user/:id 路径的 GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/user/:id', function (req, res, next) {
// if the user ID is 0, skip to the next route
if (req.params.id == 0) next('route');
// otherwise pass the control to the next middleware function in this stack
else next(); //
}, function (req, res, next) {
// render a regular page
res.render('regular');
});

// handler for the /user/:id path, which renders a special page
app.get('/user/:id', function (req, res, next) {
res.render('special');
});

路由器层中间件

  • 路由器层中间件的工作方式与应用层中间件基本相同, 差异之处在于它绑定到 express.Router() 的实例
1
var router = express.Router();
  • 使用 router.use()router.METHOD() 函数装入路由器层中间件
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
31
32
33
34
35
36
37
var app = express();
var router = express.Router();

// a middleware function with no mount path. This code is executed for every request to the router
router.use(function (req, res, next) {
console.log('Time:', Date.now());
next();
});

// a middleware sub-stack shows request info for any type of HTTP request to the /user/:id path
router.use('/user/:id', function(req, res, next) {
console.log('Request URL:', req.originalUrl);
next();
}, function (req, res, next) {
console.log('Request Type:', req.method);
next();
});

// a middleware sub-stack that handles GET requests to the /user/:id path
router.get('/user/:id', function (req, res, next) {
// if the user ID is 0, skip to the next router
if (req.params.id == 0) next('route');
// otherwise pass control to the next middleware function in this stack
else next(); //
}, function (req, res, next) {
// render a regular page
res.render('regular');
});

// handler for the /user/:id path, which renders a special page
router.get('/user/:id', function (req, res, next) {
console.log(req.params.id);
res.render('special');
});

// mount the router on the app
app.use('/', router);

错误处理中间件

  • 错误处理中间件始终采用四个自变量。必须提供四个自变量, 以将函数标识为错误处理中间件函数。即使无需使用 next 对象, 也必须指定该对象以保持特征符的有效性。否则, next 对象将被解释为常规中间件, 从而无法处理错误
  • 错误处理中间件函数的定义方式与其他中间件函数基本相同, 差别在于错误处理函数有四个自变量而不是三个, 专门具有特征符 (err, req, res, next):
1
2
3
4
app.use(function(err, req, res, next) {
console.error(err.stack);
res.status(500).send('Something broke!');
});

内置中间件

  • 自 V4.x 起, Express 不再依赖于 Connect。除 express.static 外, 先前 Express 随附的所有中间件函数现在以单独模块的形式提供。请查看中间件函数的列表。
  • express.static(root, [options])
    • Express 中唯一内置的中间件函数是 express.static。此函数基于 serve-static, 负责提供 Express 应用程序的静态资源
    • root 自变量指定从其中提供静态资源的根目录
    • 可选的 options 对象可以具有以下属性:

image-20230129172729202

  • 以下示例将使用了 express.static 中间件, 并且提供了一个详细的 ‘options’ 对象(作为示例):
1
2
3
4
5
6
7
8
9
10
11
12
var options = {
dotfiles: 'ignore',
etag: false,
extensions: ['htm', 'html'],
index: false,
maxAge: '1d',
redirect: false,
setHeaders: function (res, path, stat) {
res.set('x-timestamp', Date.now());
}
}
app.use(express.static('public', options));
  • 对于每个应用程序, 可以有多个静态目录
1
2
3
app.use(express.static('public'));
app.use(express.static('uploads'));
app.use(express.static('files'));

第三方中间件

  • 使用第三方中间件向 Express 应用程序添加功能
  • 安装具有所需功能的 Node.js 模块, 然后在应用层或路由器层的应用程序中将其加装入
  • 以下示例演示如何安装和装入 cookie 解析中间件函数 cookie-parser
1
2
3
4
5
6
7
$ npm install cookie-parser

var express = require('express');
var app = express();
var cookieParser = require('cookie-parser');
// load the cookie-parsing middleware
app.use(cookieParser());

4. 模板引擎

将模板引擎用于 Express

  • Express 应用生成器支持多款流行的视图/模板引擎, 包括 EJS、Hbs、Pug (Jade)、Twig 和 Vash, 缺省选项是 Jade。Express 本身也支持大量其他模板语言, 开箱即用
  • 在 Express 可以呈现模板文件之前, 必须设置以下应用程序设置:
    • views: 模板文件所在目录。例如: app.set('views', './views')
    • view engine: 要使用的模板引擎。例如: app.set('view engine', 'pug')
  • 然后安装对应的模板引擎 npm 包:
    • $ npm install pug —save
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 在设置视图引擎之后, 不必指定该引擎或者在应用程序中装入模板引擎模块;Express 在内部装入此模块, 如下所示(针对以上示例)
app.set('view engine', 'pug');

// 在 views 目录中创建名为 index.pug 的 Pug 模板文件, 其中包含以下内容:
html
head
title= title
body
h1= message

// 随后创建路由以呈现 index.pug 文件。如果未设置 view engine 属性, 必须指定 view 文件的扩展名。否则, 可以将其忽略。
app.get('/', function (req, res) {
res.render('index', { title: 'Hey', message: 'Hello there!'});
});

// 向主页发出请求时, index.pug 文件将呈现为 HTML。

选用模板引擎需要考虑的因素

一般来说, 你应该选择一个大而全的模板引擎, 可以尽快进入生产状态。就像你选择其他组件一样!选用模板引擎需要考虑以下因素:

  • 进入生产状态的时间 —— 如果你的团队已经有某个模板语言的经验, 那么用它可能更快进入生产状态。否则你应该考虑所选模板引擎的学习曲线
  • 流行度和活跃度 —— 要评估所选引擎的流行程度, 以及它是否拥有活跃的社区。在网站的生命周期中遇到问题时, 是否能够获得相关支持非常重要
  • 风格 —— 某些模板引擎使用特定标记, 来标识插入”普通”HTML 中的内容, 而另一些模板引擎使用不同的语法(例如使用缩进和块名称)构造 HTML
  • 性能/渲染时间
  • 功能——你应该考虑所选引擎是否具有以下功能:
    • 布局继承: 可以定义基本模板, 然后”继承”它的一部分, 使不同页面可以有不同的呈现。这通常比包含大量所需组件, 或每次从头开始构建模板更好
    • “包含”支持: 可以通过包含其他模板来构建新模板
    • 简明的变量和循环控制语法
    • 能够在模板级别过滤变量值(例如, 将变量设置为大写, 或格式化日期值)
    • 能够生成 HTML 以外的输出格式(例如 JSON 或 XML)
    • 支持异步操作和流
    • 可以同时在客户端和服务器上使用。如果一款模板引擎可以在客户端使用, 那么就使在客户端托管数据并完成所有(或大多数)渲染成为可能。

为 Express 开发模板引擎

  • 可以使用 app.engine(ext, callback) 方法创建自己的模板引擎
    • ext 表示文件扩展名
    • callback 表示模板引擎函数, 它接受以下项作为参数: 文件位置、选项对象和回调函数
  • 以下代码示例实现非常简单的模板引擎以呈现 .ntl 文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// this engine requires the fs module
var fs = require('fs');
// define the template engine
app.engine('ntl', function (filePath, options, callback) {
fs.readFile(filePath, function (err, content) {
if (err) return callback(new Error(err));
// this is an extremely simple template engine
var rendered = content.toString()
.replace('#title#', ''+ options.title +'')
.replace('#message#', ''+ options.message +'');
return callback(null, rendered);
});
});
app.set('views', './views'); // specify the views directory
app.set('view engine', 'ntl'); // register the template engine

// 应用程序现在能够呈现 .ntl 文件。在 views 目录中创建名为 index.ntl 且包含以下内容的文件:
#title#
#message#
// 然后, 在应用程序中创建以下路径:
app.get('/', function (req, res) {
res.render('index', { title: 'Hey', message: 'Hello there!'});
});
// 您向主页发出请求时, index.ntl 将呈现为 HTML。

调试 Express

  • Express 在内部使用调试模块来记录关于路由匹配、使用的中间件函数、应用程序模式以及请求/响应循环流程的信息
  • debug 就像是扩充版的 console.log, 但是与 console.log 不同, 不必注释掉生产代码中的 debug 日志。缺省情况下, 日志记录功能已关闭, 可以使用 DEBUG 环境变量有条件地开启日志记录
  • 要查看 Express 中使用的所有内部日志, 在启动应用程序时, 请将 DEBUG 环境变量设置为 express:*
    • $ DEBUG=express:* node index.js
  • 在 Windows 上, 使用对应的命令
    • > set DEBUG=express:* & node index.js

image-20230222224055948