Nodejs(含js模块化+npm+express)

1. 简介

1.1 运行环境

  • 浏览器是 js 的前端运行环境

  • Node.js 是 js 的后端运行环境

  • Node.js 中无法调用 DOM 和 BOM 等浏览器内置 API

1.2 Node.js 可以做什么

  • 基于 Express 框架可以快速构建 Web 应用

  • 基于 Electron 框架可以快速构建跨平台的桌面应用

  • 基于 restify 框架可以快速构建 API 接口项目

  • 读取和操作数据库,创建实用的命令行工具辅助前端开发

  • ...

1.3 安装与运行

  • 下载稳定版node.js

  • 安装完查看 node.js 的版本

node -v
  • 创建测试文件,通过命令行运行(需要切换到文件所在目录)
node test.js

2. fs 文件系统模块

fs 模块是 Node.js 官方提供的用来操作文件的模块,提供了一系列的方法和属性,用来满足用户对文件的操作需求

  • 如果要在 js 代码中使用 fs 模块来操作文件,则需要先导入
const fs = require("fs");

2.1 读取指定文件中的内容

  • 使用fs.readFile()读取指定文件中的内容
fs.readFile(path[, options), callback)
  • 参数解读

    • path:必选,读取的文件路径(字符串)

    • options:可选,以什么编码格式来读取文件,默认指定utf8

    • callback:必选,文件读取完成后,通过回调函数拿到读取的失败和成功的结果,err 和 dataObj

  • 示例:

const fs = require("fs");
fs.readFile("./files/1.txt", "utf-8", function (err, dataObj) {
	// 读取成功,err为null,否则为错误对象。因此能以此进行判断
	if (err) {
	return console.log("文件读取失败!" + err.message);
	}
	// 读取成功的结果,失败则为undefined
	console.log("文件读取成功,内容是:" + dataObj);
});

2.2 向指定文件中写入内容

  • 使用fs.writeFile()向指定文件写入内容
fs.writeFile(file, data[, options], callback)
  • 参数解读

    • file:必选,文件存放的路径(字符串)

    • data:必选,要写入的内容

    • options:可选,以什么格式写入文件内容,默认utf8

    • callback:必选,文件写入完成后的回调函数

  • 示例

const fs = require("fs");
fs.writeFile("F:/files/2.txt", "hello world", function (err) {
	// 写入成功,err为null,否则为错误对象
	if (err) {
	return console.log("写入文件失败!" + err.message);
	}
	console.log("文件写入成功!");
});

2.3 小练习

  • 需求:整理成绩.txt中的数据,并写入成绩-ok.txt

  • 源数据与期望格式数据如下:

  • 代码实现
const fs = require("fs");
fs.readFile("./files/成绩.txt", function (err, dataObj) {
	if (err) {
	return console.log("文件读取失败!" + err.message);
	}
	let dataStr = dataObj.toString();
	dataStr = dataStr.replaceAll("=", ":");
	dataStr = dataStr.replaceAll(" ", "\n");
	fs.writeFile("./files/成绩-ok.txt", dataStr, function (err) {
	if (err) {
	return console.log("文件写入失败!" + err.message);
	}
	});
});

2.4 路径动态拼接的问题

  • 在使用 fs 模块操作文件时,如果使用相对路径,很容易出现动态路径拼接错误的问题

  • 原因:代码在运行时,会以执行 node 命令所处的目录,动态拼接出被操作文件的完整路径

  • 解决

    • 提供完整路径:移植性差,不利于维护

    • 使用__dirname_ + '/files/data.txt'__dirname表示当前文件所在的目录

  • 使用相对路径,并在文件所在目录上一级执行命令

  • 优化后的代码
const fs = require("fs");
fs.readFile(__dirname + "/files/data.txt", function (err, dataObj) {
	if (err) {
	return console.log("文件读取失败!" + err.message);
	}
	console.log(dataObj.toString());
});

3. Path 路径模块

path 模块是 Node.js 官方提供的用来处理路径的模块。它提供了一系列的方法和属性,用来满足用户对路径的处理需求

  • 如果要在 js 代码中使用 path 模块来处理路径,则需要先导入
const path = require("path");

3.1 路径拼接

  • 使用path.join()把多个路径片段拼接为完整的路径字符串
path.join([...paths]);
  • 参数解读

    • ...paths<string>:路径片段的序列

    • 返回值:<string>

  • 示例

const fs = require("fs");
const path = require("path");
// ../ 会抵消一级路径
const pathStr = path.join("/a", "/b/c", "../", "./d", "e");
console.log(pathStr);
fs.readFile(path.join(__dirname, "/files/data.txt"), function (err, dataObj) {
	if (err) {
	return console.log("文件读取失败!" + err.message);
	}
	console.log(dataObj.toString());
});
  • 注:以后涉及路径拼接的操作,都要用path.join()进行处理,如果直接使用+进行拼接,可能会有问题,如下图所示

3.2 获取路径中的文件名

  • 使用path.basename()方法获取路径中的最后一部分,经常用它获取路径中的文件名
path.basename(path[, ext])
  • 参数解读

    • path:必选,表示一个路径的字符串

    • ext:可选,表示文件扩展名

    • 返回值:表示路径中的最后一部分

  • 示例

const path = require("path");
// 不加第二个参数,会连扩展名一起输出
const fileName = path.basename("/a/b/c/index.html", ".html");
console.log(fileName);

3.3 获取路径中的文件扩展名

  • 使用path.extname()获取路径中的扩展名
const path = require("path");
const extName = path.extname("/a/b/c/index.html");
console.log(extName);
  • 参数解读

    • path:必选,表示路径字符串

    • 返回值:扩展名字符串

  • 示例

const path = require("path");
const extName = path.extname("/a/b/c/index.html");
console.log(extName);

3.4 小练习

  • 需求:将Clock.html拆分为三个文件,clock/index.htmlclock/index.jsclock/index.css,并引入 css、js 文件(找一个含 html、css、js 的文件进行练习即可)

  • 思路

    • 设置正则表达式匹配<style></style><script></script>中的内容

    • 使用 fs 模块读取Clock.html文件

    • 编写三个方法处理 css、js、html 内容写入文件中

  • 目录结构

  • 代码实现
const fs = require("fs");
const path = require("path");
// 先设置正则表达式,提取<style></style>和<script></script>的内容
const regStyle = /<style>[\s\S]*<\/style>/;
const regScript = /<script>[\s\S]*<\/script>/;
// 读取html文件
fs.readFile(path.join(__dirname, "../clockHtml/Clock.html"), function (err, dataObj) {
	if (err) return console.log("文件读取失败!" + err.message);
	// 读取文件成功,调用三个方法将内容拆分成三个文件
	resolveCss(dataObj.toString());
	resolveJs(dataObj.toString());
	resolveHtml(dataObj.toString());
});
// 处理css
function resolveCss(htmlStr) {
	const cssStr = regStyle.exec(htmlStr);
	cssStr[0] = cssStr[0].replace("<style>", "").replace("</style>", "");
	fs.writeFile(path.join(__dirname, "./clock/index.css"), cssStr[0], function (err) {
	if (err) return console.log("文件写入失败!" + cssStr);
	});
	console.log("css文件写入成功!");
}
// 处理js
function resolveJs(htmlStr) {
	const jsStr = regScript.exec(htmlStr);
	jsStr[0] = jsStr[0].replace("<script>", "").replace("</script>", "");
	fs.writeFile(path.join(__dirname, "./clock/index.js"), jsStr[0], function (err) {
	if (err) return console.log("文件写入失败!" + jsStr);
	});
	console.log("js文件写入成功!");
}
// 处理html
function resolveHtml(htmlStr) {
	const newStr = htmlStr
	.replace(regStyle, '<link rel="stylesheet" href="index.css">')
	.replace(regScript, '<script src="index.js"></script>');
	fs.writeFile(path.join(__dirname, "./clock/index.html"), newStr, function (err) {
	if (err) console.log("文件写入失败!" + err.message);
	console.log("html文件写入成功!");
	});
}
  • 两个注意点

    • fs.writeFile()只能用来创建文件,不能用来创建路径

    • 重复调用fs.writeFile()写入同一个文件,新写入的内容会覆盖之前的内容

4. http 模块

4.1 简介

http 模块是 Node.js 官方提供的用来创建 web 服务器的模块

  • 客户端:在网络节点中,负责消费资源的电脑

  • 服务器:负责对外提供网络资源的电脑

  • 服务器和普通电脑的区别在于:服务器上安装了 web 服务器软件,如 IIS、Apache 等,通过安装这些服务器软件,就能把一台普通的电脑变成一台 web 服务器

  • 在 Node.js 中不需要使用 IIS、Apache 等第三方 web 服务器软件,可以基于 Node.js 提供的 http 模块轻松手写一个服务器软件

4.2 创建最基本的 web 服务器

  1. 导入
const http = require("http");
  1. 调用http.createServer()创建 web 服务器实例
const server = http.createServer();
  1. 为服务器实例绑定request事件,监听客户端的请求
server.on("request", (req, res) => {
	// 只要有客户端来请求服务器,就会触发request事件,从而调用这个事件处理函数
	console.log("Someone visit our web server.");
});
  1. 调用listen()启动当前 web 服务器实例
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});
  • 运行之后用浏览器访问该地址

4.3 req 请求对象

  • 只要服务器收到了客户端的请求,就会调用通过server.on()为服务器绑定的request事件处理函数

  • req是请求对象,包含了与客户端相关的数据和属性

    • req.url:客户端请求的 url 地址

    • req.method:客户端的 method 请求类型

  • 示例

const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
	console.log(`Your request url is ${req.url}, and request method is ${req.method}`);
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});

4.4 res 响应对象

  • res是响应对象,包含与服务器相关的数据和属性

    • res.end():向客户端发送指定的内容,并结束本次请求的处理过程
  • 示例

const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
	const str = `Your request url is ${req.url}, and request method is ${req.method}`;
	res.end(str);
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});

  • 通过一些接口测试软件测试一下其他请求方式,此处使用Apifox

4.5 解决中文乱码问题

  • 当调用res.end()向客户端发送中文内容时,会出现乱码,此时需要手动设置内容的编码格式
res.setHeader("Content-Type", "text-html; charset=utf-8");
  • 示例
const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
	const str = `您的请求地址是:${req.url},请求方式是:${req.method}`;
	res.setHeader("Content-Type", "text/html; charset=utf-8");
	res.end(str);
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});

4.6 小练习

4.6.1 根据不同的 url 响应不同的 html 内容

  • 实现步骤

    • 获取请求的 url

    • 路径为//index.html,访问的是首页

    • 路径为/about.html,访问的是关于页面

    • 其他则显示404 Not Found

    • 设置Content-Type响应头,防止中文乱码

    • 使用res.end()响应给客户端

  • 代码实现

const http = require("http");
const server = http.createServer();
server.on("request", (req, res) => {
	let content = "<h1>404 Not Found</h1>";
	console.log(req.url);
	if (req.url === "/" || req.url === "/index.html") {
	content = "<h1>首页</h1>";
	} else if (req.url === "/about.html") {
	content = "<h1>关于</h1>";
	}
	res.setHeader("Content-Type", "text/html; charset=utf-8");
	res.end(content);
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});

4.6.2 实现时钟的 web 服务器

  • 思路:把文件的实际存放路径,作为每个资源的请求 url 地址

  • 代码实现
const http = require("http");
const fs = require("fs");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
	if (req.url !== "/favicon.ico") {
	fs.readFile(path.join(__dirname, req.url), function (err, dataObj) {
	if (err) {
	return res.end(`<h1>404 Not Found</h1>`);
	}
	res.end(dataObj.toString());
	});
	}
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});
  • 优化资源请求路径

    • 访问/时默认也访问/clock/index.html

    • 简化路径输入/clock/index.html --> /index.html

const http = require("http");
const fs = require("fs");
const path = require("path");
const server = http.createServer();
server.on("request", (req, res) => {
	// 优化资源请求路径
	let fpath = "";
	if (req.url === "/") {
	fpath = path.join(__dirname, "./clock/index.html");
	} else {
	fpath = path.join(__dirname, "/clock", req.url);
	}
	if (req.url !== "/favicon.ico") {
	fs.readFile(fpath, function (err, dataObj) {
	if (err) {
	return res.end(`<h1>404 Not Found</h1>`);
	}
	res.end(dataObj.toString());
	});
	}
});
server.listen("8080", () => {
	console.log("http server running at http://127.0.0.1:8080");
});

5. js 模块化规范

5.1 模块化概述

5.1.1 什么是模块化

  • 将程序⽂件依据⼀定规则拆分成多个⽂件,这种编码⽅式就是模块化的编码方式

  • 拆分出来每个⽂件就是⼀个模块,模块中的数据都是私有的,模块之间互相隔离

  • 同时也能通过一些手段,可以把模块内的指定数据“交出去”,供其他模块使用

5.1.2 为什么需要模块化

  • 随着应用的复杂度越来越高,其代码量和文件数量都会急剧增加,会逐渐引发以下问题:

    • 全局污染问题

    • 依赖混乱问题

    • 数据安全问题

  • 好处

    • 复用性

    • 可维护性

    • 可实现按需加载

5.2 有哪些模块化规范

  • CommonJS——服务端应用广泛

  • AMD(了解)

  • CMD(了解)

  • ES6 模块化——浏览器端应用广泛

5.3 导入和导出的概念

模块化的核心思想就是:模块之间是隔离的,通过导入和导出进行数据和功能的共享

  • 导出(暴露):模块公开其内部的⼀部分(如变量、函数等),使这些内容可以被其他模块使用

  • 导入(引入):模块引入和使用其他模块导出的内容,以重用代码和功能

5.4 Node.js 中的模块化

5.4.1 分类

  • 根据来源的不同,分为三大类

    • 内置模块:如 fs、path、http 等

    • 自定义模块:用户创建的每个.js文件都是自定义模块

    • 第三方模块:由第三方开发出来的模块,使用前需要提前下载

5.4.2 加载模块

// 1、加载内置的fs模块
const fs = require("fs");
// 2、加载自定义模块,.js后缀可省略
const custom = require("./custom.js");
// 3、加载第三方模块
const moment = require("moment");

5.4.3 模块作用域与 module 对象

  • 模块作用域:只能在当前模块内被访问

  • 好处:防止全局变量污染问题

  • module 对象:每个.js自定义模块中都有一个module对象,里面存储了和当前模块有关的信息

5.5 CommonJS 规范

Node.js 遵循了 CommonJS 模块化规范,CommonJS 规定了模块的特性和各模块之间如何相互依赖

  • CommonJS 规定

    • 每个模块内部,module 变量代表当前模块

    • module 变量是一个对象,其exports属性(即module.exports)是对外的接口

    • 加载某个模块,其实就是加载该模块的module.exports属性,require()方法用于加载模块

5.5.1 初步体验

  • school.js
const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 通过给exports对象添加属性的方式,来导出数据
// 此处不导出getCities
exports.name = name;
exports.slogan = slogan;
exports.getTel = getTel;
  • student.js
const name = "张三";
const motto = "相信明天会更好!";
function getTel() {
	return "13877889900";
}
function getHobby() {
	return ["抽烟", "喝酒", "烫头"];
}
// 通过给exports对象添加属性的方式,来导出数据
// 此处不导出getHobby
exports.name = name;
exports.motto = motto;
exports.getTel = getTel;
  • index.js
// 引入school模块暴露的所有内容
const school = require("./school.js");
// 引入student模块暴露的所有内容
const student = require("./student.js");
console.log(school);
console.log(student);

5.5.2 导出数据

  • CommonJS标准中,导出数据有两种方式:

    • 第一种方式:module.exports = value

    • 第二种方式:exports.name = value

  • 注:

    • 每个模块内部的:thisexportsmodules.exports在初始时,都指向同一个空对象,该空对象就是当前模块导出的数据,如下图:
    • 无论如何修改导出对象,最终导出的都是module.exports的值

    • exports是对module.exports的初始引用,仅为了方便给导出添加属性,所以不能用exports={}的形式导出数据,但是可以用module.exports={}导出数据

    • 注:为了防止混乱,建议不要在同一模块中同时使用exportsmodule.exports

  • school.js

const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
module.exports = { name, slogan, getTel };
// this.c =789
// exports = {a:1}
// exports.b = 2
// module.exports.c = 3
// module.exports = {d:4}	// 最终导出成功的是这个
// console.log(this)
// console.log(exports)
// console.log(module.exports)
// console.log(this === exports && exports === module.exports)
exports.name = name;
exports.slogan = slogan;
exports.getTel = getTel;
  • 解释

    • 一开始module.exportsexports指向同一个空对象

    • exports = {a:1}exports就指向了{a:1}这个新对象,module.exports仍指向空对象

    • exports.b = 2:向exports指向的对象添加属性b

    • module.exports.c = 3:向module.exports指向的对象添加属性c

    • module.exports = {d:4}module.exports指向了新对象{d:4}

    • 无论如何修改导出对象,最终导出的都是module.exports的值

5.5.3 导入数据

在 CJS 模块化标准中,使用内置的 require 函数进行导入数据

//直接引入模块
const school = require("./school.js");
//引入同时解构出要用的数据
const { name, slogan, getTel } = require("./school.js");
//引入同时解构+重命名
const { name: stuName, motto, getTel: stuTel } = require("./student.js");

5.5.4 扩展理解

  • 一个 JS 模块在执行时,是被包裹在一个内置函数中执行的,所以每个模块都有自己的作用域,可以通过如下方式验证这一说法:
console.log(arguments);
console.log(arguments.callee.toString());
  • 内置函数的大致形式如下:
function (exports, require, module, __filename, __dirname){
	/**************************/
}

5.5.5 浏览器端运行

  • Node.js 默认是支持 CommonJS 规范的,但浏览器端不支持,所以需要经过编译,步骤如下:

    • 第一步:全局安装 browserify
    npm i browserify -g
    
    • 第二步:编译
    browserify index.js -o build.js
    
    • 注:index.js 是源文件,build.js 是输出的目标文件

    • 第三步:页面中引入使用

    <script type="text/javascript" src="./build.js"></script>
    

5.6 ES6 模块化规范

ES6 模块化规范是一个官方标准的规范,它是在语言标准的层面上实现了模块化功能,是目前最流行的模块化规范,且浏览器与服务端均支持该规范

5.6.1 初步体验

  • school.js
// 导出name
export const name = "尚硅谷";
// 导出slogan
export const slogan = "让天下没有难学的技术!";
// 导出getTel
export function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
  • student.js
export const name = "张三";
export const motto = "相信明天会更好!";
export function getTel() {
	return "13877889900";
}
function getHobby() {
	return ["抽烟", "喝酒", "烫头"];
}
  • index.js
// 引入school模块暴露的所有内容
import * as school from "./school.js";
// 引入student模块暴露的所有内容
import * as student from "./student.js";
  • 页面中引入 index.js
<script type="module" src="./index.js"></script>

5.6.2 Node 中运行 ES6 模块

  • Node.js 中运行 ES6 模块代码有两种方式:

    • 方式一:将 JavaScript 文件后缀从.js改为.mjs,Node 则会自动识别 ES6 模块

    • 方式二:在package.json中设置type属性值为module

5.6.3 导出数据

ES6 模块化提供 3 种导出方式:① 分别导出、② 统一导出、③ 默认导出

  • 分别导出
// 导出name
export const name = "尚硅谷";
// 导出slogan
export const slogan = "让天下没有难学的技术!";
// 导出getTel
export function getTel() {
	return "010-56253825";
}
  • 统一导出
const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 统一导出了:name、slogan、getTel
export { name, slogan, getTel };
  • 默认导出
const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
//默认导出了:name、slogan、getTel
export default { name, slogan, getTel };
  • 注:上述多种导出方式,可以同时使用
// 导出name —— 分别导出
export const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 导出slogan —— 统一导出
export { slogan };
// 导出getTel —— 默认导出
export default getTel;

5.6.4 导入数据

对于 ES6 模块化来说,使用何种导入方式,要根据导出方式决定

?️ 导入全部(通用)
  • 可以将模块中的所有导出内容整合到一个对象中
import * as school from "./school.js";
?️ 命名导入(对应到处方式:分别导出、统一导出)
  • 导出数据的模块
// 分别导出
export const name = "尚硅谷";
// 分别导出
export const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 统一导出
export { getTel };
  • 命名导入
import { name, slogan, getTel } from "./school.js";
  • 通过as重命名
import { name as myName, slogan, getTel } from "./school.js";
?️ 默认导出(对应导出方式:默认导出)
  • 导出数据的模块
const name = "尚硅谷";
const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 默认导出了:name、slogan、getTel
export default { name, slogan, getTel };
  • 默认导入
import school from "./school.js"; // 默认导出的名字可以修改,不是必须为school
?️ 命名导入与默认导入可以混合使用
  • 导出数据的模块
// 分别导出
export const name = "尚硅谷";
// 分别导出
export const slogan = "让天下没有难学的技术!";
function getTel() {
	return "010-56253825";
}
function getCities() {
	return ["北京", "上海", "深圳", "成都", "武汉", "西安"];
}
// 默认导出
export default getTel;
  • 命名导入与默认导入混合使用,且默认导入的内容必须放在前方
import getTel, { name, slogan } from "./school.js";
?️ 动态导入(通用)
  • 允许在运行时按需加载模块,返回值是一个 Promise
const school = await import("./school.js");
console.log(school);
?️import 可以不接收任何数据
  • 例如只是让 mock.js 参与运行
import "./mock.js";

5.6.5 数据引用问题

  • 思考1:如下代码的输出结果是什么?
function count() {
	let sum = 1;
	function increment() {
	sum += 1;
	}
	return { sum, increment };
}
const { sum, increment } = count();
console.log(sum); // 1
increment();
increment();
console.log(sum); // 1
  • 思考2:使用 CommnJS 规范,编写如下代码,输出结果是什么?

  • count.js

let sum = 1;
function increment() {
	sum += 1;
}
module.exports = { sum, increment };
  • index.js
const { sum, increment } = require("./count.js");
console.log(sum); // 1
increment();
increment();
console.log(sum); // 1
  • 说明:cjs 导入的变量是复制品,无论调用的函数怎么修改,改的还是模块内部的变量

  • 思考3:使用 ES6 模块化规范,编写如下代码,输出结果是什么?

  • count.js

let sum = 1;
function increment() {
	sum += 1;
}
export { sum, increment };
  • index.js
import { sum, increment } from "./count.js";
console.log(sum); // 1
increment();
increment();
console.log(sum); // 3
  • 说明:es6 导入的变量和模块中的变量公用同一块内存,因此会修改变量的值

  • 使用原则:导出的常量,务必使用const定义

6. 包与 npm

6.1 简介

  • 包:Node.js 中的第三方模块

  • 包的来源:由第三方个人或团队开发出来的,免费供所有人使用(免费开源)

  • 为什么需要包

    • Node.js 的内置模块仅提供一些底层的 API,在基于内置模块进行项目开发时效率较低

    • 包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大提高了开发效率

  • 从哪下载包

    • 搜索需要的包:npmjs

    • 从https://registry.npmjs.org/服务器上下载自己需要的包

  • 如何下载

    • 包管理工具 npm:Node Package Manager

6.2 安装包

# 完整写法,默认下载最新版的包
npm install 包名
# 简写
npm i 包名
# 安装指定版本的包
npm i 包名@2.22.2
  • 安装完后,查看文档学习该模块的使用方法

  • 示例:安装moment对时间进行格式化

const moment = require("moment");
const datetime = moment().format("YYYY-MM-DD HH:MM:SS");
console.log(datetime);
  • 初次装包完成后,项目文件夹下多了node_modules文件夹和package-lock.json的配置文件

  • 其中

    • node_modules文件夹用来存放所有已安装到项目中的包,require()就是从这个目录中查找并加载包

    • package-lock.json配置文件用来记录node_modules目录下的每一个包的下载信息,如包名、版本号、下载地址等

  • 注:不要手动修改node_modulespackage-lock.js文件中的任何代码,npm 包管理工具会自动维护它们

6.3 包的语义化版本规范

  • 包的版本号是以“点分十进制”形式进行定义的,总共三位数字,例如:2.24.0

  • 其中每一位数字所代表的含义如下:

    • 第 1 位数字:大版本,当发生了底层重构时,大版本+1

    • 第 2 位数字:功能版本,当新增了一些功能时,功能版本+1

    • 第 3 位数字:Bug 修复版本,对 bug 进行修复后,bug 修复版本+1

  • 版本号提升规则:只要前面的版本号增长了,则后面的版本号归零

6.4 包管理配置文件

npm 规定,在项目根目录中,必须提供名为package.json的包管理配置文件,用来记录与项目有关的一些配置信息,如:

  • 项目名称、版本号、描述等

  • 项目中都用到了哪些包

  • 哪些包只在开发期间会用到

  • 哪些包在开发和部署时都需要用到

  • 多人协作的问题

    • 整个项目的体积是 30.4M,第三方包的体积是 28.8M,项目源代码的体积 1.6M

    • 问题:第三方包体积过大,不方便团队成员之间共享项目源代码

    • 解决:共享时剔除node_modules

  • 如何记录项目中安装了哪些包

    • 在项目根目录中,创建package.json配置文件,即可用来记录项目中安装了哪些包,从而方便剔除node_modules目录后,在团队成员之间共享项目的源代码

      • 注:在项目开发中,一定要把node_modules文件夹添加到.gitignore忽略文件中
  • 快速创建package.json

npm init -y
  • 说明

    • 在执行命令所处的目录中,快速新建package.json文件

    • 还未写任何代码前先创建该文件

    • 该命令只能在英文的目录下成功运行,不能含中文、空格

    • 运行npm install 包名时,npm 包管理工具会自动把包的名称和版本号记录到package.json

6.4.1 dependencies 节点

  • package.json文件中有一个dependencies节点,专门用来记录用npm install安装了哪些包

  • 一次性安装所有包

    • 当拿到一个剔除了node_modules的项目后,需要先把所有的包下载到项目中,项目才能运行起来

    • 执行npm install命令时,npm 包管理工具会先读取package.json中的dependencies节点

    • 读取到记录的所有依赖包名称和版本号后,npm 包管理工具会把这些包一次性下载到项目中

npm install
npm i

6.4.2 devDependencies 节点

  • 如果某些包只在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到devDependencies节点中

  • 如果在开发和项目上线之后都需要用到,则建议把这些包记录到dependencies节点中

  • 使用如下命令安装指定包,并记录到devDependencies节点中

# 简写
npm i 包名 -D
# 完整写法
npm install 包名 --save-dev

6.5 卸载包

npm uninstall 包名
  • 注:npm uninstall执行成功后,会把卸载的包自动从package.jsondependencies中移除

6.6 解决下包速度慢的问题

  • 为什么下载速度慢

    • 在使用 npm 下包时,默认从国外的服务器进行下载,此时,网络数据的传输需要经过漫长的海底光缆,因此下包速度会很慢
  • npm 镜像服务器

    • 淘宝在国内搭建了一个服务器,专门把国外官方服务器上的包同步到国内的服务器,并在国内提供下包的服务,从而提高了下包的速度

    • 扩展:镜像是一种文件存储形式,一个磁盘上的数据在另一个磁盘上存在一个完全相同的副本即为镜像

  • 切换 npm 的下包镜像源
# 查看当前的下包镜像源
npm config get registry
# 切换镜像源,选择一个即可
npm config set registry https://registry.npmmirror.com	# 淘宝
npm config set registry https://npm.aliyun.com	# 阿里云
npm config set registry http://mirrors.cloud.tencent.com/npm/	# 腾讯云
npm config set registry https://mirrors.huaweicloud.com/repository/npm/	# 华为云
# 检查镜像源是否切换成功
npm config get registry

  • nrm

    • 为了更方便的切换下包的镜像源,可以安装nrm工具,利用其提供的终端命令,可以快速查看和切换下包的镜像源
# 将nrm安装为全局可用的工具
npm i nrm -g
# 查看所有可用的镜像源
nrm ls
# 将下包的镜像源切换为淘宝镜像
nrm use taobao

6.7 包的分类

  • 分为两大类

    • 项目包

      • 开发依赖包:被记录到devDependencies节点中的包,只在开发期间会用到

      • 核心依赖包:被记录到dependencies节点中的包,在开发期间和项目上线之后都会用到

    • 全局包

      • 执行npm install使用了-g参数

      • 全局包会被安装到C:\User\用户目录\AppData\Roaming\npm\node_modules目录下

      npm i 包名 -g	# 全局安装指定的包
      npm uninstall 包名 -g	# 卸载全局安装的包
      
    • 注:

      • 只有工具性质的包才有全局安装的必要性,因为它们提供了好用的终端命令

      • 判断某个包是否需要全局安装才能使用,可以参考官方提供的使用说明

  • i5ting_toc工具进行示例,它是一个可以把md文档转为html页面的小工具

# 将i5ting_toc安装为全局包
npm install -g i5ting_toc
# 调用i5ting_toc,轻松实现md转html的功能
# -o是转换成功后以默认浏览器打开
i5ting_toc -f 要转换的md文件路径 -o

6.8 规范的包结构

  • 一个规范的包,其组成结构必须符合以下3点要求

    • 包必须以单独的目录存在

    • 包的顶级目录下必须包含package.json这个包管理配置文件

    • package.json中必须包含nameversionmain这三个属性,分别代表包的名字、版本号、包的入口

  • 注:以上3点要求是一个规范的包结构必须遵守的格式,关于更多约束可以参考https://classic.yarnpkg.com/en/docs/package-json

6.9 开发属于自己的包

  • 需求:

    • 格式化日期

    • 转义 HTML 中的特殊字符

    • 还原 HTML 中的特殊字符

  • 初始化包的基本结构

    • 新建my-tools文件夹,作为包的根目录

    • my-tools文件夹中,新建如下三个文件

      • package.json:包管理配置文件

      • index.js:包的入口文件

      • README.md:包的说明文档

  • 初始化package.json

{
	"name": "my-tools",
	"version": "1.0.0",
	"main": "index.js",
	"description": "提供了格式化时间,HTMLEscape的功能",
	"keywords": ["dateFormat", "escape"],
	"license": "ISC"
}
  • 关于更多license许可协议相关的内容,可参考https://www.jianshu.com/p/86251523e898

  • index.js中定义格式化时间的方法

// 包的入口文件
function dateFormat(datetime) {
	const date = new Date(datetime);
	const y = date.getFullYear();
	const m = addZero(date.getMonth() + 1);
	const d = addZero(date.getDate());
	const hh = addZero(date.getHours());
	const mm = addZero(date.getMinutes());
	const ss = addZero(date.getSeconds());
	return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;
}
function addZero(n) {
	return n > 9 ? n : "0" + n;
}
module.exports = { dateFormat };
  • test.js测试一下模块是否可以使用
const myTools = require("./my-tools");
const datetime = myTools.dateFormat(new Date());
console.log(datetime);

  • index.js中定义转义 HTML 的方法
function HTMLEscape(htmlStr) {
	return htmlStr.replace(/<|>|"|&/g, match => {
	switch (match) {
	case "<":
	return "&lt;";
	case ">":
	return "&gt;";
	case '"':
	return "&quot;";
	case "&":
	return "&amp;";
	}
	});
}
  • index.js中定义还原 HTML 的方法
function htmlUnEscape(htmlStr) {
	return htmlStr.replace(/&lt;|&gt;|&quot;|&amp;/g, match => {
	switch (match) {
	case "&lt;":
	return "<";
	case "&gt;":
	return ">";
	case "&quot;":
	return '"';
	case "&amp;":
	return "&";
	}
	});
}
  • 将不同的功能进行模块化拆分

    • 将格式化时间的功能拆分到src/dateFormat.js

    • 将处理 HTML 字符串的功能,拆分到src/htmlEscape.js

    • index.js中,导入两个模块,得到需要向外共享的方法

    • index.js中,使用module.exports把对应的方法共享出去(解构)

  • index.js

// 包的入口文件
const date = require("./src/dateFormat");
const htmlEscape = require("./src/htmlEscape");
module.exports = {
	...date,
	...htmlEscape,
};
  • 测试
const myTools = require("./my-tools");
const datetime = myTools.dateFormat(new Date());
console.log(datetime);
const htmlStr = "<h1 ttile='abc'>这是h1标签<span>123&nbsp;</span></h1>";
const str = myTools.HTMLEscape(htmlStr);
const newStr = myTools.htmlUnEscape(str);
console.log(newStr);

  • 编写包的说明文档

    • 能清晰地将包的作用、用法、注意事项等描述清楚即可

    • 以下README.md包含以下内容

      • 安装方式、导入方式、格式化时间、转义 HTML 中的特殊字符、还原 HTML 中的特殊字符、开源协议
# 安装
npm i my-tools
# 导入
const myTools = require('./my-tools')
# 格式化时间
```
// 格式:YYYY-MM-DD hh:mm:ss
const datetime = myTools.dateFormat(new Date())
console.log(datetime)
```
# 转义 HTML 中的特殊字符
```
const htmlStr = "<h1 ttile='abc'>这是h1标签<span>123&nbsp;</span></h1>"
// 结果:&lt;h1 ttile='abc'&gt;这是h1标签&lt;span&gt;123&amp;nbsp;&lt;/span&gt;&lt;/h1&gt;
const str = myTools.HTMLEscape(htmlStr)
```
# 还原 HTML 中的特殊字符
```
const htmlStr = "<h1 ttile='abc'>这是h1标签<span>123&nbsp;</span></h1>"
const str = myTools.HTMLEscape(htmlStr)
const newStr = myTools.htmlUnEscape(str)
console.log(newStr)
```
# 开源协议
ISC

6.10 发布包

  • 注册 npm 账号

  • 登录 npm 账号

    • 在终端执行npm login命令

    • 注意,不是在官网登录,而是在命令行

    • 在运行npm login之前,必须先把下包的服务器地址切换为 npm 官方服务器,否则会导致发布包失败

  • 切换到包的根目录,运行npm publish,即可将包发布到 npm 上(注:包名不能雷同)

  • 删除已发布的包

    • npm unpublish 包名 --force

    • 注:

      • 只能删除 72h 以内发布的包

      • 删除后 24h 内不允许重复发布

      • 发布包时要谨慎,尽量不要往 npm 上发布没有意义的包!

6.11 模块的加载机制

6.11.1 优先从缓存中加载

  • 模块在第一次加载后会被缓存,即多次调用require()不会导致模块的代码被执行多次

  • 注:不论是内置模块、用户自定义模块还是第三方模块,都会优先从缓存中加载,从而提高模块的加载效率

6.11.2 内置模块的加载机制

  • 内置模块的加载优先级最高

  • 如:require('fs')始终返回内置的 fs 模块,即使node_modules目录下有同名包 fs

6.11.3 自定义模块的加载机制

  • 使用require()加载自定义模块时,必须指定以./../开头的路径标识符,在加载自定义模块时,如果没有指定./../这样的路径标识符,node 会把它当作内置模块或第三方模块进行加载

  • 在使用require()导入自定义模块时,若省略了文件的扩展名,则 Node.js 会按顺序分别尝试加载以下文件

    • 按照确切的文件名进行加载

    • 补全.js扩展名进行加载

    • 补全.json扩展名进行加载

    • 补全.node扩展名进行加载

    • 加载失败,终端报错

6.11.4 第三方模块的加载机制

  • 如果传递给require()的模块标识符不是一个内置模块,也没有./../开头,则 Node.js 会从当前模块的父目录开始,尝试从/node_modules文件夹中加载第三方模块

  • 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录

  • 例如,假设在C:\Users\itheima\project\foo.js文件里调用了require('tools'),则 Node.js 会按以下顺序查找:

    • C:\Users\itheima\project\node_modules\tools

    • C:\Users\itheima\node_modules\tools

    • C:\Users\node_modules\tools

    • C:\node_modules\tools

6.11.5 目录作为模块

  • 当把目录作为模块标识符传递给require()进行加载时,有三种加载方式

    • 在被加载的目录下查找一个叫package.json的文件,并寻找main属性,作为require()加载的入口

    • 如果目录里没有package.json文件,或者main入口不存在或无法解析,则 Node.js 会试图加载目录下的index.js文件

    • 若以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error:Cannot find module 'xxx'

7. express

7.1 简介

7.1.1 是什么

  • Express是基于 Node.js 平台,快速、开放、极简的 Web 开发框架

  • 简单理解:Express 的作用和 Node.js 内置的 http 模块类似,是专门用来创建 Web 服务器的

  • Express 的本质:npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法

7.1.2 进一步理解

  • 不使用 Express 能否创建 Web 服务器

    • 能,使用原生的 http 模块
  • 有了 http 内置模块,为什么还要用 Express

    • http 模块使用较复杂,开发效率低;Express 是基于内置的 http 模块进一步封装出来的,能够提高开发效率

7.1.3 Express 能够做什么

  • 对于前端程序员来说,最常见的两种服务器,分别是

    • Web 网站服务器:专门对外提供 Web 网页资源的服务器

    • API 接口服务器:专门对对外提供 API 接口的服务器

  • 使用 Express,可以方便、快速的创建 Web 网站服务器或 API 接口服务器

7.2 基本使用

7.2.1 安装

  • 在项目所处的目录中安装 express
npm i express

7.2.2 创建基本的 Web 服务器

// 导入
const express = require("express");
// 创建web服务器
const app = express();
// 调用app.listen(端口号, callback),启动服务器
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});

7.2.3 监听 GET 请求

  • 通过app.get()可以监听客户端的GET请求
app.get("请求url", function (req, res) {
	/* 处理函数 */
});
  • req:请求对象,包含了与请求相关的属性和方法

  • res:响应对象,包含了与响应相关的属性和方法

7.2.4 监听 POST 请求

  • 通过app.post()可以监听客户端的POST请求
app.post("请求url", function (req, res) {
	/* 处理函数 */
});

7.2.5 把内容响应给客户端

  • 通过res.send()方法,可以把处理好的内容发送给客户端
app.get("/user", function (req, res) {
	// 向客户端发送JSON对象
	res.send({ name: "zs", age: 18, gender: "男" });
});
app.post("/user", function (req, res) {
	// 向客户端发送文本内容
	res.send("请求成功!");
});

7.2.6 获取 url 中携带的查询参数

  • req.query默认是一个空对象

  • 客户端使用?name=zs&age=18这种查询字符串形式发送到服务器,可以通过req.query对象访问到,如:req.query.namereq.query.age

app.get("/", function (req, res) {
	console.log(req.query);
	res.send(req.query);
});

7.2.7 获取 url 中的动态参数

  • 通过req.params对象,可以访问到 url 中通过:匹配到的动态参数

  • req.params默认是一个空对象

  • 动态参数可以有多个,如:/user/:id/:name

// 此处:id是一个动态参数
app.get("/user/:id", function (req, res) {
	console.log(req.params);
	res.send(req.params);
});

7.3 托管静态资源

7.3.1 express.static()

  • 通过express.static()可以非常方便地创建一个静态资源服务器

  • 示例:将 clock 目录下的文件对外开放访问

app.use(express.static("./clock"));
  • 此时,可以访问clock目录下的所有文件了

    • http://127.0.0.1/index.html
  • 注:Express 在指定的静态目录中查找文件,并对外提供资源的访问路径,存放静态文件的目录名不会出现在 url 中

  • 如果要托管多个静态资源目录,需要多次调用express.static()
app.use(express.static("./clock"));
app.use(express.static("./files"));
  • 注:访问静态资源文件时,express.static()会根据目录的添加顺序查找所需的文件,即如果两个文件夹中存在同名文件,以前面的为主

  • ./files放前面,访问到的就是files中的index.html文件

7.3.2 挂载路径前缀

  • 如果希望在托管的静态资源访问路径之前挂载路径前缀,可使用如下方式
app.use("/clock", express.static("./clock"));
  • 注:此后访问资源时都必须加上前缀

7.4 nodemon

  • 在编写调试 Node.js 项目时,如果修改了项目的代码,需要频繁手动关闭再重启,比较繁琐

  • 此时,可以使用nodemon工具,它可以监听项目文件的变动,当代码被修改时,nodemon会自动重启项目,方便开发和调试

  • 安装

npm i -g nodemon
  • nodemon app.js代替传统的node app.js启动项目

7.5 路由

7.5.1 概念

  • 在 Express 中,路由指的是客户端的请求与服务器处理函数之间的映射关系

  • Express 中的路由分为 3 部分组成,分别是请求的类型、请求的 url 地址、处理函数

app.method(path, handler);
  • 前面使用过的app.get()app.post()便是路由

7.5.2 路由的匹配过程

  • 每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后才会调用对应的处理函数

  • 匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的 url 同时匹配成功,则 Express 会将这次请求转交给对应的 function 函数进行处理

  • 路由匹配注意点

    • 按照定义的先后顺序进行匹配

    • 请求类型和请求的 url 同时匹配成功,才会调用对应的处理函数

7.5.3 使用

  • 为了方便对路由进行模块化的管理,Express 不建议将路由直接挂载到 app 上,而是推荐将路由抽离为单独的模块

  • 步骤

    • 创建路由模块对应的.js文件

    • 调用express.Router()函数创建路由对象

    • 向路由对象上挂载具体的路由

    • 使用module.exports向外共享路由对象

    • 使用app.use()注册路由模块

  • router.js

// 导入express,创建路由对象
const express = require("express");
const router = express.Router();
// 挂载获取用户列表的路由
router.get("/user/list", function (req, res) {
	res.send("Get user list.");
});
// 挂载添加用户的路由
router.post("/user/add", function (req, res) {
	res.send("Add new user.");
});
// 向外导出路由对象
module.exports = router;
  • test.js
const express = require("express");
const router = require("./router");
const app = express();
app.use(router);
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});
  • 注:app.use()的作用就是用来注册全局中间件

7.5.4 为路由模块添加前缀

// 导入路由模块
const userRouter = require("./router/user.js");
// 使用app.use()注册路由模块,并添加统一的访问前缀api
app.use("/api", userRouter);

7.6 中间件

7.6.1 概念

  • 中间件:特指业务流程的中间处理环节

  • 生活中的例子

    • 在处理污水时,一般要经过三个处理环节,从而保证处理过后的废水达到排放标准

    • 处理污水的这三个中间处理环节,可以叫做中间件

  • 当一个请求到达 Express 的服务器后,可以连续调用多个中间件,从而对这次请求进行预处理

7.6.2 格式

  • Express 的中间件,本质上是一个 function 处理函数,格式如下:

  • 注:中间件函数的形参列表中必须包含next参数,而路由处理函数中只包含reqres

  • next()是实现多个中间件连续调用的关键,它表示把流转关系转交给下一个中间件或路由

7.6.3 定义中间件

const mw = function (req, res, next) {
	console.log("这是一个最简单的中间件函数");
	// 在当前中间件的业务处理完毕后,必须调用next()
	// 表示把流转关系转交给下一给中间件或路由
	next();
};

7.6.4 全局生效的中间件

  • 客户端发起的任何请求到达服务器后,都会触发的中间件,叫做全局生效的中间件

  • 通过调用app.use(中间件函数),即可定义一个全局生效的中间件

const mw = function (req, res, next) {
	console.log("这是一个最简单的中间件函数");
	next();
};
// 全局生效的中间件
app.use(mw);
// 简写
app.use(function (req, res, next) {
	console.log("这是一个最简单的中间件函数");
	next();
});
  • 多个中间件之间共享一份reqres

  • 基于这样的特性,可以在上游的中间件中,统一为reqres对象添加自定义的属性或方法,供下游的中间件或路由使用

const express = require("express");
const app = express();
app.use(function (req, res, next) {
	req.name = "张三";
	next();
});
app.use(function (req, res, next) {
	res.age = 18;
	next();
});
app.get("/", (req, res) => {
	console.log(req.name, res.age);
	res.send("Home page.");
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});

7.6.5 定义多个全局中间件

  • 可以使用app.use()连续定义多个全局中间件,客户端请求到达服务器之后,会按照中间件定义的顺序依次进行调用
app.use(function (req, res, next) {
	console.log("调用了第1个全局中间件");
	next();
});
app.use(function (req, res, next) {
	console.log("调用了第2个全局中间件");
	next();
});
app.get("/", (req, res) => {
	res.send("Home page.");
});

7.6.6 局部生效的中间件

  • 不使用app.use()定义的中间件,即局部生效的中间件
const mw = function (req, res, next) {
	console.log("这是中间件函数");
	next();
};
app.get("/", mw, function (req, res) {
	res.send("Home page.");
});
// mw这个中间件不会影响下面这个路由
app.get("/user", function (req, res) {
	res.send("User page.");
});

7.6.7 定义多个局部中间件

  • 以下两种方式都可以定义多个局部中间件
app.get("/user", mw1, mw2, (req, res) => {
	res.send("User page.");
});
app.get("/user", [mw1, mw2], (req, res) => {
	res.send("User page.");
});

7.6.8 注意事项

  • 一定要在路由之前注册中间件

  • 客户端发送过来的请求,可以连续调用多个中间件进行处理

  • 执行完中间件的业务代码后,要调用next()

  • 为防止代码逻辑混乱,调用next()后不要再写额外代码

  • 连续调用多个中间件时,多个中间件之间共享reqres对象

7.6.9 分类

  • Express 官方把常见的中间件用法分成了 5 大类

    • 应用级别的中间件

    • 路由级别的中间件

    • 错误级别的中间件

    • Express 内置的中间件

    • 第三方的中间件

  • 应用级别的中间件

    • 通过app.use()app.get()等绑定到 app 实例上的全局/局部中间件
  • 路由级别的中间件

    • 绑定到express.Router()实例上的中间件,其用法与应用级别的中间件没有区别
const app = express();
const router = express.Router();
// 路由级别的中间件
router.use((req, res, next) => {
	console.log("Time:", Date.now());
	next();
});
app.use("/", router);
  • 错误级别的中间件

    • 专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题

    • 格式:错误级别中间件的处理函数中含四个参数function(err, req, res, next)

const express = require("express");
const app = express();
app.get("/", (req, res) => {
	throw new Error("出错了!");
	res.send("Home page.");
});
app.use((err, req, res, next) => {
	res.send(err.message);
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});
  • 注:错误级别的中间件必须注册在所有路由之,否则不生效!

  • Express 内置的中间件(常用的 3 个)

    • express.static():快速托管静态资源(无兼容性问题)

    • express.json:解析 JSON 格式的请求体数据(4.16.0+ 可用)

    • express.urlencoded:解析 URL-encoded 格式的请求体数据(4.16.0+ 可用)

// 配置解析application/json格式数据的内置中间件
app.use(express.json());
// 配置解析application/x-www-urlencoded格式数据的内置中间件
app.use(express.urlencoded({ extended: false }));
  • 示例1:
const express = require("express");
const app = express();
app.use(express.json());
app.get("/user", (req, res) => {
	// 没配置express.json()中间件时,默认是undefined
	// 配置之后:{ name: 'zhangsan', age: 18 }
	console.log(req.body);
	res.send("ok");
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});
  • 示例 2:
const express = require("express");
const app = express();
// 解析表单中的url-encoded格式的数据
app.use(express.urlencoded({ extended: false }));
app.post("/book", (req, res) => {
	// 在服务器中可以使用req.body来接收客户端发送过来的请求体数据
	// 结果:[Object: null prototype] { bookname: '西游记', count: '10' }
	console.log(req.body);
	res.send("ok");
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});
  • 第三方的中间件

    • 由第三方开发出来的中间件。在项目中可以按需下载并配置第三方中间件,从而提高开发效率

    • 此处以body-parser为例,该中间件用来解析请求体数据

      • 安装:npm i body-parser

      • 导入:require('body-parser')

      • 注册使用:app.use()

    • Express 内置的express.urlencoded中间件就是基于body-parser进一步封装出来的

const express = require("express");
const app = express();
const parser = require("body-parser");
// 解析表单中的url-encoded格式的数据
app.use(parser({ extended: false }));
app.post("/book", (req, res) => {
	// 在服务器中可以使用req.body来接收客户端发送过来的请求体数据
	console.log(req.body);
	res.send("ok");
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});

7.6.10 自定义中间件

  • 需求:模拟一个类似于express.urlencoded的中间件来解析 post 提交到服务器的表单数据

  • 实现步骤

    • 定义中间件

    • 监听reqdata事件和end事件

    • 使用querystring模块解析请求体数据

    • 将解析出来的数据对象挂载为req.body

    • 将自定义中间件封装为模块

  • 说明:

    • 在中间件中,需要监听req对象的data事件来获取客户端发送到服务器的数据

    • 如果数据量较大,无法一次性发送完毕,则客户端会把数据切割后分批发送到服务器,所以data事件可能会触发多次,每次触发data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接

    • 当请求体数据接收完毕后,会自动触发reqend事件

    • 因此,可以在reqend事件中拿到并处理完整的请求体数据

    • Node.js 内置了querystring模块,专门用来处理查询字符串,通过该模块的parse()可以将查询字符串解析成对象的格式

    • 将解析出来的数据挂载为req的自定义属性,命名为req.body,供下游使用

    • 最后将自定义的中间件封装为独立的模块

  • custom-body-parser/index.js

// 导入querystring模块解析请求体数据
const qs = require("querystring");
const parser = (req, res, next) => {
	// 存储客户端发送过来的请求体数据
	let str = "";
	req.on("data", chunk => {
	// 拼接请求体数据
	str += chunk;
	});
	req.on("end", () => {
	// 打印完整的请求体数据
	console.log(str);
	// 调用qs.parse()把查询字符串解析为对象,并挂载为req.body
	req.body = qs.parse(str);
	next();
	});
};
module.exports = parser;
  • test.js
const express = require("express");
const app = express();
const parser = require("./custom-body-parser");
app.use(parser);
app.post("/book", (req, res) => {
	// 在服务器中可以使用req.body来接收客户端发送过来的请求体数据
	console.log(req.body);
	res.send("ok");
});
app.listen(80, () => {
	console.log("express server running at http://127.0.0.1");
});

7.7 使用 Express 写接口

7.7.1 创建基本的服务器&创建 API 路由模块

  • test.js
const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
app.use("/api", apiRouter);
app.listen(80, () => {
	console.log("express running at http://127.0.0.1");
});
  • apiRouter.js
const express = require("express");
const router = express.Router();
module.exports = router;

7.7.2 编写 GET 接口

router.get("/get", (req, res) => {
	// 获取客户端通过查询字符串发送到服务器的数据
	const query = req.query;
	// 调用res.send()把数据响应给客户端
	res.send({
	status: 0, // 状态:0表示成功,1表示失败
	msg: "GET请求成功!", // 状态描述
	data: query, // 需要响应给客户端的具体数据
	});
});

7.7.3 编写 POST 接口

router.post("/post", (req, res) => {
	// 获取客户端通过请求体发送到服务器的URL-encoded数据
	const body = req.body;
	// 调用res.send()方法把数据响应给客户端
	res.send({
	status: 0, // 状态:0表示成功,1表示失败
	msg: "POST请求成功!", // 状态描述消息
	data: body, // 需要响应给客户端的具体数据
	});
});
  • 注:如果要获取URL-encoded格式的请求体数据,必须配置中间件app.use(express.urlencoded({extended: false}))

  • test.js

const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
app.use(express.urlencoded({ extended: false }));
app.use("/api", apiRouter);
app.listen(80, () => {
	console.log("express running at http://127.0.0.1");
});

7.7.4 跨域问题

  • 前面写的 GET 和 POST 接口不支持跨域请求

  • 当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域

  • 解决接口跨域问题的方案主要有两种

    • CORS:主流的解决方法,推荐使用

    • JSONP:有缺陷,只支持 GET 请求

7.7.5 使用 cors 中间件解决跨域问题

  • cors 是 Express 的第三方中间件,通过安装和配置 cors 中间件,可以很方便地解决跨域问题

  • 使用步骤

    • 安装:npm install cors

    • 导入:const cors = require('cors')

    • 在路由之前调用app.use(cors())配置中间件

  • 编写简单的 html 文件测试

<body>
	<button id="get">get</button>
	<button id="post">post</button>
</body>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
	const getBtn = document.querySelector("#get");
	const postBtn = document.querySelector("#post");
	getBtn.addEventListener("click", () => {
	axios({
	url: "/api/get",
	method: "get",
	query: {
	name: "张三",
	age: 18,
	},
	});
	});
	postBtn.addEventListener("click", () => {
	axios({
	url: "/api/post",
	method: "get",
	params: {
	name: "李四",
	age: 18,
	},
	});
	});
</script>
  • test.js
const express = require("express");
const app = express();
const apiRouter = require("./apiRouter");
const cors = require("cors");
app.use(cors());
app.use(express.urlencoded({ extended: false }));
app.use("/api", apiRouter);
app.listen(80, () => {
	console.log("express running at http://127.0.0.1");
});

7.7.6 CORS

  • CORS(Cross-Origin Resource Sharing,跨域资源共享):由一系列 HTTP 响应头组成,这些 HTTP 响应头决定浏览器是否阻止前端 js 代码跨域获取资源

  • 浏览器的同源安全策略默认会阻止网页“跨域”获取资源,但如果接口服务器配置了 cors 相关的 http 响应头,就可以解除浏览器端的跨域访问限制

  • 注意:

    • CORS 主要在服务器端进行配置,客户端浏览器无需做任何额外的配置,即可请求开启了 CORS 的接口

    • CORS 在浏览器中有兼容性,只支持XMLHttpRequest Level2的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE 10+、Chrome4+、FireFox3.5+)

7.7.7 CORS 响应头部

?️ Access-Control-Allow-Origin
  • 响应头部中可以携带Access-Control-Allow-Origin字段,格式如下
Access-Control-Allow-Origin: <origin> | *
  • 其中,origin 参数的值指定了允许访问该资源的外域 url

  • 例如,下面的字段值只允许来自http://itcast.cn的请求

res.setHeader("Access-Control-Allow-Origin", "http://itcast.cn");
  • 以下代码表示允许来自任何域的请求
res.setHeader("Access-Control-Allow-Origin", "*");
?️ Access-Control-Allow-Headers
  • 默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头

    • AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(值仅限于text/plainmultipart/form-dataapplication/x-www-form-urlencoded三者之一)
  • 如果客户端向服务器发送了额外的请求体信息,则需要在服务器端通过Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败!

// 运行客户端额外向服务器发送Content-Type请求头和X-Custom-Header请求头
// 注:多个请求头之间用英文逗号隔开
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Custom-Header");
?️ Access-Control-Allow-Methods
  • 默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求

  • 如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Allow-Methods来指明实际请求所允许使用的 HTTP 方法

// 只允许 POST、GET、DELETE、HEAD 请求方法
res.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, HEAD");
// 允许所有的HTTP请求方法
res.setHeader("Access-Control-Allow-Methods", "*");

7.7.8 CORS 请求的分类

  • 客户端在请求 CORS 接口时,根据请求方式和请求头的不同,可以将 CORS 的请求分为两大类

    • 简单请求

    • 预检请求

  • 同时满足以下两大条件的请求,就属于简单请求

    • 请求方式:GET、POST、HEAD 三者之一

    • HTTP 头部信息不超过以下几种字段:无自定义头部字段AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(值仅限于text/plainmultipart/form-dataapplication/x-www-form-urlencoded三者之一)

  • 符合以下任何一个条件的请求,都需要进行预检请求

    • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型

    • 请求头中包含自定义头部字段

    • 向服务器发送了application/json格式的数据

  • 在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求成为预检请求

  • 服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据

  • 简单请求和预检请求的区别

    • 简单请求:客户端与服务器之间只发生一次请求

    • 预检请求:客户端与服务器之间发送两次请求,OPTION 预检请求成功后,才会发起真正的请求

7.7.9 JSONP 接口

  • 概念:浏览器端通过<script>标签的src属性请求服务器上的数据,同时服务器返回一个函数的调用。这种请求数据的方式叫做JSONP

  • 特点

    • JSONP不属于真正的Ajax请求,因为它没有使用XMLHttpRequest这个对象

    • JSONP仅支持GET请求,不支持POSTPUTDELETE等请求

  • 创建JSONP接口

    • 如果项目中已经配置了CORS跨域资源共享,为了防止冲突,必须在配置CORS中间件之前声明JSONP的接口,否则JSONP接口会被处理成开启了CORS的接口
// 优先创建JSONP接口【这个接口不会被处理成CORS接口】
app.get("/api/jsonp", (req, res) => {});
// 再配置CORS中间件【后续的所有接口都会被处理为CORS接口】
app.use(cors());
// 这是一个开启了CORS的接口
app.get("/api/get", (req, res) => {});
  • 实现 JSONP 接口的步骤

    • 获取客户端发送过来的回调函数的名字

    • 得到要通过 JSONP 形式发送给客户端的数据

    • 根据前两步得到的数据,拼接出一个函数调用的字符串

    • 把上一步拼接得到的字符串响应给客户端的<script>标签进行解析执行

app.get("/api/jsonp", (req, res) => {
	// 获取客户端发送过来的回调函数的名字
	const funcName = req.query.callback;
	// 得到要通过 JSONP 形式发送给客户端的数据
	const data = { name: "张三", age: 18 };
	// 根据前两步得到的数据,拼接出一个函数调用的字符串
	const str = `${funcName}(${JSON.stringify(data)})`;
	// 把上一步拼接得到的字符串响应给客户端的`<script>`标签进行解析执行
	res.send(str);
});
  • 由于 axios 没有内置 jsonp,此处使用 jquery 发送 ajax 请求
$("#jsonp").on("click", () => {
	$.ajax({
	method: "GET",
	url: "http://127.0.0.1/api/jsonp",
	dataType: "jsonp",
	success: res => {
	console.log(res);
	},
	});
});

7.8 在项目中操作数据库

7.8.1 安装并连接数据库

  • 安装第三方模块:npm i mysql

  • 配置 mysql 模块,连接到 MySQL 数据库

// 导入
const mysql = require("mysql");
// 建立与MySQL数据库的连接
const db = mysql.createPool({
	host: "127.0.0.1", // 数据库的ip地址
	user: "root", // 登录数据库的账号
	password: "root", // 登录数据库的密码
	database: "test", // 指定要操作哪个数据库
});
  • 执行 SQL 语句,测试 mysql 模块是否正常工作
db.query("SELECT 1", (err, results) => {
	if (err) return console.log(err.message);
	// 只要能打印出[ RowDataPacket { '1': 1 } ],就证明数据库连接正常
	console.log(results);
});

7.8.2 查询数据

  • 如果执行的是select查询语句,则执行的结果是数组
// 查询users表中的所有用户数据
db.query("SELECT * FROM users", (err, results) => {
	// 查询失败
	if (err) return console.log(err.message);
	// 查询成功
	console.log(results);
});

7.8.3 插入数据

  • 如果执行的是insert into插入语句,则results是一个对象

  • 可以通过affectedRows属性来判断是否插入数据成功

// 要插入的数据
const user = { username: "zhangsan", password: "123456" };
// 待执行的sql语句,其中?表示占位符
const sqlStr = "INSERT INTO users (username, password) VALUES (?, ?)";
// 使用数组形式,依次为?占位符指定具体的值
db.query(sqlStr, [user.username, user.password], (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("插入数据成功");
	}
});
  • 向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过以下方式快速插入数据
// 要插入的数据
const user = { username: "Tom", password: "123456" };
// 待执行的sql语句,其中?表示占位符
const sqlStr = "INSERT INTO users SET ?";
// 直接将数据对象当作占位符的值
db.query(sqlStr, user, (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("插入数据成功");
	}
});

7.8.4 更新数据

  • 执行update语句后,执行结果也是一个对象,可以通过affectedRows判断是否更新成功
// 要更新的数据
const user = { id: 2, username: "lisi", password: "654321" };
// 待执行的sql语句
const sqlStr = "UPDATE users SET username=?, password=? WHERE id=?";
// 使用数组依次为占位符指定具体的值
db.query(sqlStr, [user.username, user.password, user.id], (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("更新数据成功");
	}
});
  • 更新表数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过以下方式快速更新数据
// 要更新的数据
const user = { id: 2, username: "lisi", password: "654321" };
// 待执行的sql语句
const sqlStr = "UPDATE users SET ? WHERE id=?";
// 使用数组依次为占位符指定具体的值
db.query(sqlStr, [user, user.id], (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("更新数据成功");
	}
});

7.8.5 删除数据

  • 在删除数据时,推荐根据id这样的唯一标识来删除对应的数据

  • 执行delete语句之后,结果也是一个对象,也有affectedRows属性

// 要执行的sql语句
const sqlStr = "DELETE FROM users WHERE id=?";
// 注:如果sql语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
// 如果只有一个占位符,则可以省略数组
db.query(sqlStr, 7, (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("删除数据成功");
	}
});
  • 标记删除

    • 使用delete语句会真正的把数据从表中删除,为了防止误删,推荐使用标记删除的形式来模拟删除的动作

    • 所谓标记删除,就是在表中设置类似于status这样的状态字段,来标记当前这条数据是否被删除

    • 当用户执行了删除的动作时,不是执行delete,而是update,将这条数据对应的status字段标记为删除即可

db.query("UPDATE users SET status=1 WHERE id=?", 6, (err, results) => {
	if (err) return console.log(err.message);
	if (results.affectedRows === 1) {
	console.log("删除数据成功");
	}
});

8. 前后端的身份认证

8.1 Web 开发模式

  • 目前主流的 Web 开发模式有两种

    • 基于服务器渲染的传统 Web 开发模式

    • 基于前后端分离的新型 Web 开发模式

8.1.1 服务端渲染

  • 服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接动态生成的,因此,客户端不需要使用 Ajax 额外请求页面的数据
app.get("/index.html", (req, res) => {
	// 要渲染的数据
	const user = { name: "zs", age: 20 };
	// 服务器通过字符串的拼接,动态生成HTML内容
	const html = `<h1>姓名:${user.name},年龄:${user.age}</h1>`;
	// 把生成好的页面内容响应给客户端,因此,客户端拿到的是带有真实数据的HTML页面
	res.send(html);
});
  • 优点

    • 前端耗时少:因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可,尤其是移动端,更省电

    • 有利于 SEO:因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO

  • 缺点

    • 占用服务器端资源:即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力

    • 不利于前后端分离,开发效率低:使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发

8.1.2 前后端分离

  • 后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式

  • 优点

    • 开发体验好:前端专注于 UI 页面的开发,后端专注于 api 的开发,且前端有更多的选择性

    • 用户体验好:Ajax 技术的广泛应用,极大提高了用户的体验,可以轻松实现页面的局部刷新

    • 减轻了服务器端的渲染压力:因为页面最终是在每个用户的浏览器中生成的

  • 缺点

    • 不利于 SEO:因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫无法爬取页面的有效信息

    • 解决:利用 Vue、React 等前端框架的 SSR(server side render)技术

  • SEO

    • Search Engine Optimizatio(搜索引擎优化),简单来说,就是透过一系列的技术和策略,让你的网站更容易被搜寻引擎(如 Google、Bing)收录,并且在搜寻结果中排名靠前。

8.1.3 如何选择?

  • 不谈业务场景而盲目选择使用何种开发模式都是耍流氓

  • 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,此时使用服务器端渲染

  • 类似后台管理项目,交互性比较强,不需要考虑 SEO,则可以使用前后端分离的开发模式

  • 具体使用何种开发模式并不是绝对的,为了同时兼顾首页的渲染速度和前后端分离的开发效率,一些网站采用了首屏服务器端渲染 + 其他页面前后端分离的开发模式

8.2 身份认证

8.2.1 简介

  • 身份认证:又称“身份验证”、“鉴权”,是通过一定的手段完成对用户身份的确认

  • 日常生活中的身份认证随处可见,如:高铁的验票乘车、手机的密码或指纹解锁等

  • 在 Web 开发中,也涉及到用户身份的认证,如:各大网站的手机验证码登录、邮箱密码登录、二维码登录等

  • 不同开发模式下的身份认证

    • 服务端渲染推荐使用Session 认证机制

    • 前后端分离推荐使用JWT 认证机制

8.2.2 Session 认证机制

  • http 协议的无状态性

    • 客户端的每次 http 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 http 请求的状态
  • 如何突破 http 无状态的限制

    • 对于超市来说,为了方便收银员在结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡

    • 现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做Cookie

  • Cookie是存储在用户浏览器中的一段不超过 4kb 的字符串,它由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性组成

  • 不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的Cookie一同发送到服务器

  • Cookie的极大特性

    • 自动发送

    • 域名独立

    • 过期时限

    • 4kb 限制

  • 客户端第一次请求服务器时,服务器通过响应头的形式向客户端发送一个身份认证的Cookie,客户端会自动将Cookie保存在浏览器中

  • 随后,当客户端浏览器每次请求服务器时,浏览器会自动将身份认证相关的Cookie通过请求头的形式发送给服务器,服务器即可验明客户端的身份

  • 由于Cookie 是存储在浏览器中的,而且浏览器也提供了读写Cookie的 API,因此Cookie很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据通过Cookie的形式发送给浏览器

  • 注:千万不要使用Cookie存储重要且隐私的数据,比如用户的身份信息、密码等

  • 为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡后,可以在收银机上进行刷卡认证,只有收银机确认存在的会员卡才能被正常使用

  • 这种“会员卡 + 刷卡认证”的设计理念,就是Session认证机制的精髓

8.2.3 Session 的工作原理

8.2.4 在 Express 中使用 Session 认证

  • 安装express-session中间件
npm i express-session
  • 注册 session 中间件
// 导入
const session = require("express-session");
// 配置session中间件
app.use(
	session({
	secret: "keyboard cat", // secret属性的值可以为任意字符串
	resave: false, // 固定写法
	saveUninitialized: true, // 固定写法
	})
);
  • 向 session 中存数据

    • express-session中间件配置成功后,即可通过req.session来访问和使用session对象,从而存储用户的关键信息
// 登录的接口
app.post("/api/login", (req, res) => {
	// 判断用户提交的登录信息是否正确
	if (req.body.username !== "admin" || req.body.password !== "000000") {
	return res.send({ status: 1, msg: "登录失败!" });
	}
	req.session.user = req.body; // 将用户的信息存储到Session中
	req.session.isLogin = true; // 将用户的登录状态存储到session中
	res.send({ status: 0, msg: "登录成功!" });
});
  • 从 session 中取数据

    • 直接从req.session对象上获取之前存储的数据
// 获取用户名的接口
app.get("/api/username", (req, res) => {
	// 判断用户是否登录
	if (!req.session.user.isLogin) {
	return res.send({ status: 1, msg: "fail" });
	}
	res.send({ status: 0, msg: "success", username: req.session.user.username });
});
  • 清空 session

    • 调用req.session.destory()即可清空服务器保存的session信息
// 退出登录的接口
app.post("/api/logout", (req, res) => {
	// 清空当前客户端对应的session信息
	req.session.destory();
	res.send({
	status: 0,
	msg: "退出登录成功",
	});
});

8.2.5 jwt

  • Session 认证机制需要配合 Cookie 才能实现,由于 Cookie 默认不支持跨域访问,所以当涉及到前端跨域请求后端接口时,需要做很多额外的配置,才能实现跨域 Session 认证

  • 注:

    • 当前端请求后端接口不存在跨域问题时,推荐使用 Session 身份认证机制

    • 当前端需要跨域请求后端接口时,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制

  • JWT(JSON Web Token)是目前最流行的跨域认证解决方案

8.2.6 jwt 工作原理

  • 用户的信息通过 Token 字符串的形式保存在客户端浏览器中,服务器通过还原 Token 字符串的形式来认证用户的身份

8.2.7 jwt 的组成部分

  • jwt 通常由三部分组成,分别是:Header(头部)、Playload(有效荷载)、Signature(签名)

  • 三者之间使用.分隔

Header.Playload.Signature
  • 示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  • 其中

    • Playload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串

    • HeaderSignature是安全性相关的部分,只是为了保证Token的安全性

  • 使用方式

    • 客户端收到服务器返回的 jwt 之后,通常会将它存储在localStoragesessionStorage

    • 此后,客户端每次与服务器通信,都要带上这个 jwt 字符串,从而进行身份认证

    • 推荐的做法是把 jwt 放在 http 请求头的Authorization字段中

    Authorization: Bearer <token>
    

8.2.8 在 Express 中使用 jwt

  • 安装
npm i jsonwebtoken express-jwt
  • 其中

    • jsonwebtoken用于生成 jwt 字符串

    • express-jwt用来将 jwt 字符串解析还原成 JSON 对象

  • 导入

const jwt require('jsonwebtoken')
const { expressjwt } = require('express-jwt')
  • 定义secret密钥

    • 为了保证 jwt 字符串的安全性,防止 jwt 字符串在网络传输过程中被别人破解,需要定义一个用于加密和解密的 secret 密钥

    • 当生成 jwt 字符串时,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 jwt 字符串

    • 当把 jwt 字符串解析还原成 JSON 对象时,需要使用 secret 密钥进行解密

    // secret密钥的本质是一个字符串,任意,越复杂越好
    const secretKey = "hello world";
    
  • 在登录成功后生成 jwt 字符串

    • 调用jsonwebtoken提供的sign(),将用户信息加密成 jwt 字符串响应给客户端
app.post("/api/login", (req, res) => {
	if (req.body.username !== "admin" || req.body.password !== "000000") {
	return res.send({ status: 1, msg: "登录失败!" });
	}
	// 用户登录成功之后生成jwt字符串,通过token属性响应给客户端
	res.send({
	status: 200,
	message: "登录成功!",
	// 调用jwt.sign()生成jwt字符串
	// 三个参数分别是:用户信息、加密密钥、配置对象,可以配置当前token的有效期
	token: jwt.sign({ username: req.body.username }, secretKey, { expiresIn: "30s" }),
	});
});
  • 将 jwt 字符串还原为 JSON 对象

    • 客户端每次在访问那些有权限接口时,都需要主动通过请求头中的Authorization字段,将Token字符串发送到服务器进行身份认证

    • 此时服务器可以通过express-jwt这个中间件,自动将客户端发送过来的Token解析还原成 JSON 对象

    • 注:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息挂载到req.auth属性上

// expressJWT({secret: secretKey})用来解析Token
// .unless({path: [/^\/api\//]})用来指定哪些接口不需要访问权限
app.use(expressjwt({ secret: secretKey, algorithms: ["HS256"] }).unless({ path: [/^\/api\//] }));
  • 使用req.auth获取用户信息

    • express-jwt这个中间件配置成功后,即可在那些有权限的接口中使用req.auth对象,来访问从 jwt 字符串中解出来的用户信息了
app.get("/admin/getinfo", (req, res) => {
	console.log(req.auth);
	res.send({
	status: 200,
	message: "获取用户信息成功!",
	data: req.auth,
	});
});
  • 捕获解析 jwt 失败后产生的错误

    • 当使用express-jwt解析Token时,如果客户端发送过来的Token过期或不合法,会产生一个解析失败的错误,影响项目的正常运行

    • 可以通过 Express 的错误中间件捕获这个错误并进行相关的处理

app.use((err, req, res, next) => {
	// token解析失败导致的错误
	if (err.name === "UnauthorizedError") {
	return res.send({
	status: 401,
	message: "无效的token",
	});
	}
	// 其他原因导致的错误
	res.send({ status: 500, message: "未知错误" });
});
作者:iRuriCatt原文地址:https://www.cnblogs.com/iRuriCatt/p/18631580

%s 个评论

要回复文章请先登录注册