Node.js 学习笔记

Node.js 学习

1 Node.js 概述

1.1 什么是 Node.js ?

Node.js发布于2009年5月,由Ryan Dahl开发,是一个基于Chrome V8引擎的JavaScript运行环境,使用了一个事件驱动、非阻塞式 I/O 模型,让 JavaScript 运行在服务端的开发平台,它让JavaScript成为与PHPPythonPerlRuby等服务端语言平起平坐的脚本语言

官方网址:Node.js

注意:

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

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

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

1.2 Node.js 可以做什么?

Node.js 作为一个 Javascript 的运行环境,仅仅提供了基础的功能和 API。但是靠这些基础功能以及一些强大的工具和框架,它就像是大前端时代的 “大宝剑”,前端程序员的行业竞争力将会越来越强。

  1. 基于 Express 框架,可以快速构建 Web 应用。
  2. 基于 Electron 框架,可以构建跨平台的桌面应用。
  3. 基于 Restify 框架,可以快速构建 API 接口项目。
  4. 读写和操作数据库、创建实用的命令行工具辅助前端开发等等。

1.3 Node.js 安装及配置

传送门:node.js 安装教程 (Windows zip 版)

1.4 在 node.js 环境中执行 JavaScript 代码

第一步:打开终端。

第二步:输入 “node 要执行 js 文件的路径”。

2 fs 文件系统模块

fs 模块就是 Node.js 官方提供的、用来操作文件的模块。

在 JavaScript 中使用,先导入:

const fs = require('fs')

2.1 fs.readFile()

使用 fs.readFile() 方法,可以读取指定文件的内容。

// 语法格式
fs.readFile(path[, options], callback)
// path: 表示文件的路径
// options:可选,表示以什么编码格式来读取文件
// callback:文件读取完成后,通过回调函数拿到读取的结果

// 示例:
const fs = require('fs')
// err:失败的结果  dataStr:成功的结果
fs.readFile('./test.txt', 'utf8', function(err, dataStr) {
    // 读取文件成功 err 值为 null,dataStr 值为 文件内容
    // 读取文件失败 err 值为 错误对象,dataStr 值为 undefined
    if(err) {
        return console.log('读取失败!' + err.message)
    }
    console.log('读取成功!' + dataStr)
})

2.2 fs.writeFile()

使用 fs.writeFile() 方法,可以向指定文件写入内容。

// 语法格式
fs.writeFile(path, data[, options], callback)
// path: 表示文件的路径
// data:表示要写入的内容
// options:可选,表示以什么编码格式来写入文件
// callback:文件写入完成后的回调函数

// 示例:
const fs = require('fs')
// err:失败的结果
fs.writeFile('./test.txt', 'hello world!', 'utf8', function(err) {
    // 写入文件成功 err 值为 null
    // 写入文件失败 err 值为 错误对象
    if(err) {
        return console.log('写入失败!' + err.message)
    }
    console.log('写入成功!')
})

注意:fs.writeFile() 方法只能用来创建文件,不能用来创建路径;重复写入同一个文件,新内容会覆盖旧内容。

2.3 路径动态拼接的问题

在使用 fs 模块操作文件时,如果提供的操作路径是以 ./ 或 …/ 开头的相对路径时,很容易出现路径动态拼接错误的问题。

解决方法:

const fs = require('fs')
// 使用 __dirname,表示当前文件所处的路径
fs.readFile(__dirname + '/test.txt', 'utf8', function(err, dataStr) {
    if(err) {
        return console.log('读取失败!' + err.message)
    }
    console.log('读取成功!' + dataStr)
})

3 path 路径模块

path 模块时 Node.js 官方提供的、用来处理路径的模块。

在 JavaScript 中使用,先导入:

const path = require('path')

3.1 path.join()

path.join() 方法,可以把多个路径片段拼接为完整的路径字符串。

// 语法格式
path.join([...paths])

// 示例
const path = require('path')
// ../ 会抵消前面一层路径
const str = path.join('/a', '/b/c', '../', './e')
console.log(str) // 输出:\a\b\e

3.2 path.basename()

path.basename() 方法,可以获取路径中的最后一部分,可以通过这个方法获取路径中的文件名。

// 语法格式
path.basename(path[, ext])
// path:表示路径字符串
// ext:可选,表示文件扩展名
// 返回结果为路径中的最后一部分

// 示例
const path = require('path')
const filePath = '/a/index.html' // 定义文件存放路径
const fileName = path.basename(filePath)
console.log(fileName) // 输出:index.html

const fileName1 = path.basename(filePath, '.html')
console.log(fileName1) // 输出:index

3.3 path.extname()

path.extname() 方法可以获取路径中的扩展名部分。

// 语法格式
path.extname(path)
// path:表示路径字符串
// 返回值为文件扩展名

// 示例
const path = require('path')
const filePath = '/a/index.html' // 定义文件存放路径
const fileExt = path.extname(filePath)
console.log(fileExt) // 输出:.h

4 http 模块

4.1 什么是 http 模块?

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

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

http 模块是 Node.js 官方提供的、用来创建 web 服务器的模块。通过 http 模块提供的 http.createServer() 方法,就能把一台普通的电脑变为一台 web 服务器,对外提供 web 资源服务。

同样,想使用就先调用:

const http = require('http')

4.2 服务器相关概念

4.2.1 IP 地址

IP 地址是一个唯一地址,用于标识互联网或本地网络上的设备。

IP 地址是一串由句点分隔的数字。IP 地址表示为一组四个数字,比如 192.158.1.38 就是一个例子。该组合中的每个数字都可以在 0 到 255 的范围内。因此,完整的 IP 寻址范围从 0.0.0.0 到 255.255.255.255。

4.2.2 域名和域名服务器

尽管 IP 地址能够唯一地标记网络上的计算机,但 IP 地址是一长串数字,不方便记忆,于是人们又发明了另一套字符型地址方案,即所谓的域名(Domain Name)地址

IP 地址和域名是一一对应的关系,这份对应关系存放在一种叫做域名服务器(DNS,Domain Name System)的电脑中。使用者只需通过好记的域名访问对应的服务器即可,对应的转换工作由域名服务器实现。因此,域名服务器就是提供 IP 地址域名之间的转换服务的服务器。

4.2.3 端口号

所谓的端口,就好像是门牌号一样,客户端可以通过ip地址找到对应的服务器端,但是服务器端是有很多端口的,每个应用程序对应一个端口号,通过类似门牌号的端口号,客户端才能真正的访问到该服务器。为了对端口进行区分,将每个端口进行了编号,这就是端口号。

4.3 创建最基本的 web 服务器

代码如下:

// 1.导入 http 模块
const http = require('http')
// 2.创建 web 服务器实例
const server = http.createServer()
// 3.为服务器绑定 request 事件,监听客户端的请求
server.on('request', function (request, response) {
  // 3.1 request
  // request 是请求对象,它包含了客户端相关的数据和属性:
  // request.url 是客户端请求的 URL 地址
  let str = `url is ${request.url}, method is ${request.method}`

  // 3.2 response
  // response 是响应对象,它包含了服务器相关的数据和属性:
  // response.end() 向客户端发送指定的内容,并结束这次请求的处理过程

  // 3.3 解决中文乱码,设置响应头 'Content-Type' 的值为 'text/html; charset=utf-8'
  response.setHeader('Content-Type', 'text/html; charset=utf-8')
  response.end(str)
})
// 4.启动服务器
server.listen(80, function () {
  console.log('Server running at http://127.0.0.1')
})

4.4 根据不同的 url 地址响应不同的 html 内容

// 1.导入 http 模块
const http = require('http')
// 2.创建 web 服务器实例
const server = http.createServer()
// 3.为服务器绑定 request 事件,监听客户端的请求
server.on('request', (req, res) => {
  // 3.1 获取请求的 url 地址
  let url = req.url
  // 3.2 设置默认响应内容
  let tis = '

404 Not found!

'
// 3.3 判断 url 地址 if (url === '/' || url === '/index.html') { tis = '这是首页' } else if (url === '/login.html') { tis = '这是登录页' } // 3.4 解决中文乱码 res.setHeader('Content-Type', 'text/html; charset=utf-8') // 3.5 响应数据 res.end(tis) }) // 4.启动服务器 server.listen(80, function () { console.log('Server running at http://127.0.0.1') })

5 模块化

5.1 什么是模块化?

模块化是指解决一个复杂问题时,自顶向下逐层把系统划分成若干模块的过程。对于整个系统来说,模块是可组合、分解和更换的单元

编程领域中的模块化,就是遵守固定的规则,把一个大文件拆分成独立并相互依赖的多个小模块。

好处:

  • 提高了代码的复用性
  • 提高了代码的可维护性
  • 可以实现按需加载

5.2 Node.js 中模块的分类

根据模块的来源不同,可以分为:

  1. 内置模块:是由 Node.js 官方提供的,例如 fs、path、http等。
  2. 自定义模块:用户自己创建的 js 文件。
  3. 第三方模块:是由第三方开发出来的模块,并非是内置模块与自定义模块,使用前需要下载。

5.3 加载模块

可以使用 require() 加载模块:

//1.加载内置模块
const fs = require('fs')
//2.加载用户自定义模块
const myJs = require('./myJs.js')
// 可以不加后缀名
// 如:const myJs = require('./myJs')
//3.加载第三方模块
const 第三方模块名 = require('第三方模块名')

5.4 Node.js 中的模块作用域

函数作用域类似,在自定义模块中定义的变量方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域

好处:能够防止全局变量污染的问题。

5.5 向外共享模块作用域中的成员

5.5.1 module 对象

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

Module {
  id: '.',
  path: 'C:\\Users\\86175\\Desktop\\test\\nodeTest',
  exports: {},
  filename: 'C:\\Users\\86175\\Desktop\\test\\nodeTest\\myJs.js',
  loaded: false,
  children: [],
  paths: [
    'C:\\Users\\86175\\Desktop\\test\\nodeTest\\node_modules',
    'C:\\Users\\86175\\Desktop\\test\\node_modules',
    'C:\\Users\\86175\\Desktop\\node_modules',
    'C:\\Users\\86175\\node_modules',
    'C:\\Users\\node_modules',
    'C:\\node_modules'
  ]
}

5.5.2 module.exports 对象

在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用。

外界用 require() 方法导入自定义模块时,得到的就是 module.exports 所指的对象。

实例:

// myJs.js 文件
let name = '张三'

// 在 module.exports 对象上挂载属性
module.exports.name = name
module.exports.sex = '男'

// 在 module.exports 对象上挂载方法
module.exports.say = function () {
  console.log('hello')
}

测试:

// test.js 文件
const myJs = require('./myJs')
console.log(myJs)
// 输出:{ name: '张三', sex: '男', say: [Function (anonymous)] }
// 用户自定义模块如果没有挂载属性或方法,默认输出 {}

注意:使用 require() 方法导入模块时,导入的结果,永远以 module.exports 指向的对象为准。

实例:

// myJs.js 文件
let name = '张三'

// 在 module.exports 对象上挂载属性
module.exports.name = name
modumodule.exports.sex = '男'

// 在 module.exports 对象上挂载方法
module.exports.say = function () {
  console.log('hello')
}

// 让 module.exports 指向一个新对象
module.exports = {
  nextName: '李四',
  eat() {
    console.log('吃饭')
  }
}

测试:

// test.js 文件
const myJs = require('./myJs')
console.log(myJs)
// 输出:{ nextName: '李四', eat: [Function: eat] }

5.5.3 exports 对象

为了简化向外共享成员的代码,Node 提供了 exports 对象。默认情况下,exports 和 modumodule.exports 指向同一个对象。

实例:

// myJs.js 文件
let name = '张三'

exports.name = name
exports.sex = '男'
exports.say = function () {
  console.log('hello')
}

测试:

// test.js 文件
const myJs = require('./myJs')
console.log(myJs)
// 输出:{ name: '张三', sex: '男', say: [Function (anonymous)] }

注意:最终共享的结果,还是以 module.exports 指向的对象为准。

5.6 Node.js 中的模块化规范

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

CommonJS 规定:

  1. 每个模块内部,module 变量代表当前模块。
  2. module 变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。
  3. 加载某个模块,其实是加载该模块的 module.exports 属性。require() 方法用于加载模块

6 npm 与包

6.1 包

  1. 什么是包?

Node.js 中的第三方模块又叫做

  1. 包的来源?

不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用。

注意:Node.js 中的包都是免费且开源的,不需要付费即可免费下载使用。

  1. 为什么需要包?

由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时候,效率很低。

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

内置模块之间的关系,类似于 jQuery浏览器内置 API 之间的关系。

  1. 在哪下包?

从 npm 网站搜索自己想要的包。

从 registry 服务器上下在自己需要的包。

  1. 如何下包?

npm, Inc.公司提供了一个包管理工具,我们可以使用这个包管理工具,从 registry 服务器把需要的包下载到本地使用。

这个包管理工具的名字叫做 Node Package Manager(简称 npm 包管理工具),这个工具会随着 Node.js 的安装一起被安装到本地。

可以通过在终端输入 npm -v 来查看版本号。

6.2 在项目中安装包

命令如下:

npm install 包的完整名称
// 简写
npm i 包的完整名称
// 安装指定版本的包,如果不指定,默认安装最新版本的包
npm install 包的完整名称@版本号
npm i 包的完整名称@版本号

// 安装指定的包,并记录到 devDependencies 点中,如果某些包只会在项目开发阶段被用到,建议如下命令安装包
npm install 包名 --save-dev
npm i 包名 -D // 简写

// 如果要卸载包,可以执行如下命令
npm uninstall 包的完整名称

注意:如果直接执行npm i或者npm install命令,npm 包管理工具会从 package.json (一般第一次安装包后会自动生成到项目根目录,也可以通过npm init -y手动生成)中的 dependencies 节点中读取记录的所有依赖包的名称与版本号,将它们一次性下载到项目中。

演示:

Moment JavaScript 日期处理类库下载及使用。

// 下载:
// 在当前项目下,打开终端,输入
npm i moment

// 简单使用:
// 导入 moment 包
const moment = require('moment')
// 格式化时间
let date = moment().format('YYYY-MM-DD HH:mm:ss a')
console.log(date) // 2022-05-04 17:31:45 pm

详细使用看官方文档Moment

6.3 包的语义化版本规范

报的版本号是以 “点分十进制” 的形式进行定义的,总共有三位数字,例如 2.22.2

其中:第一位表示大版本,第二位表示功能版本,第三位表示Bug 修复版本

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

6.4 包的分类

  1. 项目包

安装在 node_modules 目录中的包,都是项目包。

项目包又分为两类:

  • 开发依赖包(被记录到 devDependencies 节点中的包,只在开发阶段会用到)
  • 核心依赖包(被记录到 dependencies 节点中的包,在开发阶段和项目上线后都会用到)
npm i 包名 -D // 开发依赖包
npm i 包名    // 核心依赖包
  1. 全局包

在执行npm install命令时,如果提供了-g参数,则会把包安装为全局包。

命令如下:

npm -i 包名 -g  // 安装
npm uninstall 包名 -g  // 卸载

注意:全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下。

7 模块的加载机制

7.1 优先从缓存中加载

**模块在第一次加载后会被缓存。**这也就意味着多次调用 require() 不会导致模块的代码被多次执行。

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

7.2 内置模块的加载机制

内置模块由 Node.js 官方提供的模块,内置模块的加载优先级最高。

例如,require(‘fs’) 始终返回内置的 fs 模块,即使在 node_modules 目录下有名字相同的包也叫做 fs。

7.3 自定义模块的加载机制

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

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

  1. 按照确切的文件名进行加载
  2. 补全.js扩展名进行加载
  3. 补全.json扩展名进行加载
  4. 补全.node扩展名进行加载
  5. 加载失败,终端报错

7.4 第三方模块的加载机制

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

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

7.5 目录作为模块

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

  1. 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口。
  2. 如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
  3. 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error:Cannot find module ‘xxx’。

8 Express

8.1 简介

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

通俗理解:Express 的作用和 Node.js 内置的 http 模块类似,是专们用来创建 Web 服务器的。

本质:就是一个 npm 上的第三方包,提供了快速创建 Web 服务器的便捷方法。

官网:Express

8.2 基本使用

8.2.1 安装

npm i [email protected]

8.2.2 创建基本的 Web 服务器

// 1.导入 express
const express = require('express')

// 2.创建 web 服务器
const app = express()

// 3.启动 web 服务器
app.listen(80, () => {
  console.log('express server running at http://127.0.0.1')
})

8.2.3 监听 GET 和 POST 请求

/**监听 GET 和 POST 请求
 * 参数1: 客户端请求的 URL 地址
 * 参数2: 处理函数
 * 可以通过 res.send() 方法把处理好的数据发送给客户端
 */
// 监听 GET 请求
app.get('请求的 URL 地址', function (req, res) {
  /* 处理函数 */
})
// 监听 POST 请求
app.post('请求的 URL 地址', function (req, res) {
  /* 处理函数 */
})

8.2.4 获取 URL 中携带的查询参数

app.get('/', function (req, res) {
  // req.query 默认是一个空对象
  // 可以通过 req.query 对象访问到客户端发送到服务器的参数
  // 例如: 查询字符串 ?id=1&name=zs 可以通过 req.query.id 和 req.query.name获取
  console.log(req.query) // { id: '1', name: 'zs' }
})

8.2.5 获取 URL 中的动态参数

// URL 地址中,可以通过 :参数名 的形式,匹配动态参数值
// 冒号后面的参数名可以随意
app.get('/:id', function (req, res) {
  // req.params 默认是一个空对象
  // 里面存放着通过 : 动态匹配到的参数值
  console.log(req.params)  // { id: '1' }
})

8.3 托管静态资源

8.3.1 express.static()

为了提供诸如图像、CSS 文件和 JavaScript 文件之类的静态文件,请使用 Express 中的 express.static 内置中间件函数。

函数语法:

express.static(root, [options])

例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:

app.use(express.static('public'))

现在,你就可以访问 public 目录中的所有文件了:

http://localhost:3000/images/kitten.jpg
http://localhost:3000/css/style.css
http://localhost:3000/js/app.js
http://localhost:3000/images/bg.png
http://localhost:3000/hello.html

注意:Express 在静态目录查找文件,因此,存放静态文件的目录名不会出现在 URL 中。

8.3.2 托管多个静态资源目录

如果要使用多个静态资源目录,请多次调用 express.static 中间件函数:

app.use(express.static('public'))
app.use(express.static('files'))

访问静态资源文件时,express.static 中间件函数会根据目录的添加顺序查找所需的文件。

8.3.3 挂载路径前缀

如果希望托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下的方式:

app.use('/static', express.static('public'))

现在,你就可以通过带有 /static 前缀地址来访问 public 目录中的文件了。

http://localhost:3000/static/images/kitten.jpg
http://localhost:3000/static/css/style.css
http://localhost:3000/static/js/app.js
http://localhost:3000/static/images/bg.png
http://localhost:3000/static/hello.html

然而,您提供给 express.static 函数的路径相对于从中启动节点进程的目录。如果从其他目录运行 Express 应用,则使用要提供的目录的绝对路径会更安全:

const path = require('path')
app.use('/static', express.static(path.join(__dirname, 'public')))

8.4 nodemon

8.4.1 什么是 nodemon?

nodemon 能够监听项目文件的变动,当代码被修改后,我们不需要频繁手动关闭项目再开启,nodemon 会自动帮我们重启项目,极大的方便了开发和调试。

8.4.2 安装 nodemon

在终端中输入如下命令,就可将 nodemon 安装为全局可用工具。

npm install -g nodemon

8.4.3 使用 nodemon

使用node app.js启动项目,缺点是代码被修改后,需要手动重启项目。

使用nodemon app.js启动项目,好处是代码被修改后,会被 nodemon 监听到,从而实现自动重启项目的效果。

9 Express 路由

9.1 概念

  1. 什么是路由?

从广义上来讲,路由就是映射关系

  1. Express 中的路由

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

Express 中的路由分为 3 部分,分别是请求类型请求的 URL 地址处理函数,如下:

app.method(path, handler)
  1. 路由的匹配过程

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

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

9.2 路由的使用

9.2.1 简单使用

const express = require('express')
// 创建 Web 服务器
const app = express()

// 挂载路由
app.get('/', (req, res) => {
  res.send('hello get')
})
app.post('/', (req, res) => {
  res.send('hello post')
})

// 启动服务器
app.listen(80, () => { console.log('Express server running at http://127.0.0.1') })

9.2.2 模块化路由

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

将路由抽离为单独的模块的步骤如下:

  1. 创建路由模块对应的 .js 文件。
  2. 调用express.Router()函数创建路由对象。
  3. 向路由对象上挂载具体的路由。
  4. 使用module.exports向外共享路由对象。
  5. 使用app.use()函数注册路由模块。

9.2.3 创建路由模块

// 1.导入 express
const express = require('express')
// 2.创建路由对象
const router = express.Router()
// 3.挂载具体路由
router.get('/user', (req, res) => {
  res.send('hello router get')
})
router.post('/user', (req, res) => {
  res.send('hello router post')
})
// 4.向外导出路由
module.exports = router

9.2.4 注册路由模块

const express = require('express')
// 创建 Web 服务器
const app = express()

// 1.导入自定义路由模块
const router = require('./router')

// 2.注册路由模块
app.use(router)

// 启动服务器
app.listen(80, () => { console.log('Express server running at http://127.0.0.1') })

9.2.5 为路由模块添加前缀

const express = require('express')
const router = require('./router')
// 创建 Web 服务器
const app = express()

// 1.注册路由模块,并添加前缀
app.use('/api', router)

// 启动服务器
app.listen(80, () => { console.log('Express server running at http://127.0.0.1') })

10 Express 中间件

10.1 什么是中间件?

中间件(Middleware),特指业务流程的中间处理环节

10.2 Express 中间件的处理流程

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

Node.js 学习笔记_第1张图片

10.3 Express 中间件的格式

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

Node.js 学习笔记_第2张图片

10.4 next 函数的作用

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

Node.js 学习笔记_第3张图片

10.5 Express 中间件简单使用

10.5.1 定义中间件函数

const express = require('express')

const app = express()

// 定义一个最简单的中间件函数
const mw = function (req, res, next) {
  console.log('这是一个中间件函数')
  // 把流转关系转交给下一个中间件或路由
  next()
}

app.listen(80, () => {
  console.log('http://127.0.0.1')
})

10.5.2 全局生效的中间件

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

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

const express = require('express')

const app = express()

// 定义一个最简单的中间件函数
const mw = function (req, res, next) {
  console.log('这是一个中间件函数')
  // 把流转关系转交给下一个中间件或路由
  next()
}

// 将 mw 注册为全局生效的中间件
app.use(mw)

app.listen(80, () => {
  console.log('http://127.0.0.1')
})

10.5.3 定义多个全局中间件

可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行调用,如下:

// 定义多个全局生效的中间件
app.use(function (req, res, next) {
  console.log('one')
  next()
})

app.use(function (req, res, next) {
  console.log('two')
  next()
})

app.get('/', (req, res) => {
  res.send('hello')
})

10.5.4 定义中间件的简化形式

const express = require('express')

const app = express()

// 定义全局生效的中间件
app.use(function (req, res, next) {
  console.log('这是一个中间件函数')
  // 把流转关系转交给下一个中间件或路由
  next()
})

app.listen(80, () => {
  console.log('http://127.0.0.1')
})

10.5.5 中间件的作用

多个中间件之间,共享一份 req 和 res。基于这样的特性,我们可以在上游的中间件中,统一为 req 和 res 对象添加自定义的属性和方法,供下游的中间件或路由进行使用。

10.5.6 局部生效的中间件

不使用 app.use() 定义的中间件,叫做局部生效的中间件,如下:

const mw = function (req, res, next) {
  console.log('局部生效中间件')
  next()
}

app.get('/', mw, (req, res) => {
  res.send('mw')
})

10.5.7 同时使用多个局部生效的中间件

app.get('/', mw, mw1, mw2, (req, res) => {
  res.send('mw')
})
app.get('/', [mw, mw1, mw2], (req, res) => {
  res.send('mw')
})

10.5.8 使用中间件的注意事项

  1. 一定要在路由之前注册中间件。
  2. 客户端发送过来的请求,可以连续调用多个中间件进行处理。
  3. 执行完中间件的业务代码之后,不要忘记调用 next() 函数
  4. 为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码。
  5. 连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象。

10.6 中间件的分类

  1. 应用级别的中间件

通过 app.use() 或 app.get() 或 app.post(),绑定到 app 实例上的中间件,叫做应用级别的中间件。

  1. 路由级别的中间件

绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有任何区别。应用级别中间件时绑定到 app 实例上,路由级别中间件绑定到 router 实例上。

  1. 错误级别的中间件

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

格式:错处级别中间件 function 处理函数中,必须有4个形参,形参顺序从前到后,分别是(err, req, res, next)

app.get('/', (req, res) => {
  throw new Error('制造一个错误')
})

app.use(function (err, req, res, next) {
  console.log(err.message)
  res.send(err.message)
})

​ 注意:错误级别的中间件,必须注册在所有路由之后。

  1. Express 内置的中间件
    • express.static 快速托管静态资源的内置中间件,例如:图片、HTML文件等(无兼容性)。
    • express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)。
    • express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)。
  2. 第三方的中间件

非 Express 官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率。

11 使用 Express 写接口

11.1 简单使用

编写 apiRouter.js 文件

// 导入 express
const express = require('express')
// 创建 router 实例对象
const router = express.Router()

router.get('/', (req, res) => {
  let query = req.query
  res.send({
    status: 200,
    msg: 'GET请求数据成功',
    data: query
  })
})

router.post('/', (req, res) => {
  let body = req.body
  res.send({
    status: 200,
    msg: 'POST请求数据成功',
    data: body
  })
})

module.exports = router

编写 express.js 文件

// 导入 express
const express = require('express')
// 创建服务器实例
const app = express()

const router = require('./apiRouter')

app.use(express.json())
app.use(express.urlencoded({ extended: false }))

app.use('/router', router)

//启动服务器
app.listen(80, () => {
  console.log('http://127.0.0.1')
})

11.2 CORS 跨域资源共享

11.2.1 接口的跨域问题

解决跨域的方案主要有两种:

① CORS(主流的解决方案,推荐)

② JSONP(有缺点,只支持 GET 请求)

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

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

步骤如下:

安装中间件

npm i cors

导入中间件

const cors = require('cors')

注册中间件为全局可用

app.use(cors())

11.2.3 什么是 CORS?

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端JavaScript代码获取跨域请求的响应。

同源安全策略默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。

11.2.4 CORS 的注意事项

  • CORS 主要在服务器端进行配置。客户端浏览器无需做任何额外的配置,即可请求开启了 CROS 的接口。
  • CORS 在浏览器中有兼容性。只支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务器接口(如:IE10+、Chrome4+、FireFox3.5+)

11.2.5 响应首部字段

  1. Access-Control-Allow-Origin

响应首部中可以携带一个 Access-Control-Allow-Origin字段,其语法如下:

Access-Control-Allow-Origin:  | *

其中,origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。

  1. Access-Control-Allow-Headers

响应首部 Access-Control-Allow-Headers 用于preflight request(预检请求)中,列出了将会在正式请求的Access-Control-Request-Headers字段中出现的首部信息。

默认情况下,CORS 仅支持客户端向服务器发送如下的9个请求头,AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(只限于解析后的值为 application/x-www-form-urlencoded、``multipart/form-data text/plain 三种 MIME 类型(不包括参数)),它们始终是被支持的,不需要在这个首部特意列出。

如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers对额外的请求头进行声明,否则这次请求会失败!

// 多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers', 'X-Custom-Header, Upgrade-Insecure-Requests')
  1. Access-Control-Allow-Methods

默认情况下,CORS 仅支持客户端发起 GET、POST 和 HEAD 请求。

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

res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE')
// 允许所有 HTTP 请求方法
res.setHeader('Access-Control-Allow-Methods', '*')

11.2.6 CROS 请求的分类

客户端在请求 CORS 接口时,根据请求方式请求头的不同,可以将 CORS 的请求分为两大类,分别是简单请求和预检请求。

  1. 简单请求

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

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

② HTTP 头部信息不超过这几种字段:无自定义头部字段AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type(只限于解析后的值为 application/x-www-form-urlencoded、``multipart/form-data text/plain 三种 MIME 类型(不包括参数))。

  1. 预检请求

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

① 请求方式为:GET、POST、HEAD 三者之外的请求类型。

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

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

在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。

  1. 两者之间的区别

简单请求:客户端和服务器之间只会发生一次请求。

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

12 在项目中使用 mysql

12.1 安装 mysql 模块

mysql 模块是托管于 npm 上的第三方模块。它提供了在 Node.js 项目中连接和操作 mysql 数据库的能力。

在项目中输入如下命令,即可将 mysql 安装为项目的依赖包:

npm i mysql

12.2 配置 mysql 模块

// 1.导入 mysql 模块
const mysql = require('mysql')
// 2.建立于 mysql 数据库的连接
const conn = mysql.createPool({
  host: '127.0.0.1',  // 数据库 ip 地址
  user: 'root',       // 用户名
  password: 'root',   // 密码
  database: 'test'    // 要操作的数据库名
})

12.3 查询操作

// 查询 user 表中的所有数据
// 定义 sql
let allSql = 'select * from user'
// 执行查询
conn.query(allSql, (err, results) => {
  // 查询失败
  if (err) return console.log(err.message)
  // 查询成功
  console.log(results)
})

// 输出:
[
  RowDataPacket { id: 1, username: '张三', password: '123' },
  RowDataPacket { id: 2, username: '李四', password: '222' },
  RowDataPacket { id: 3, username: '王五', password: '444' }
]

12.4 插入操作

// 要插入的数据
let user = { username: '赵四', password: '666' }
// 定义 sql, ? 号表示占位符
let addSql = 'insert into user values(null, ?, ?)'
// 执行插入操作
conn.query(addSql, [user.username, user.password], (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('插入数据成功')
})

12.5 插入操作便捷方式

在向表中插入数据时,如果数据对象的每个属性数据表的字段一一对应,则可以通过如下方式快速插入数据:

// 要插入的数据
let user = { username: '赵四', password: '666' }
// 定义 sql, ? 号表示占位符
let addSql = 'insert into user set ?'
// 执行插入操作
conn.query(addSql, user, (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('插入数据成功')
})

12.6 修改操作

// 修改用户信息
let user = { id: 1, username: '赵四', password: '666' }
// 定义 sql, ? 号表示占位符
let updateSql = 'update user set username=?, password=? where id=?'
// 执行插入操作
conn.query(updateSql, [user.username, user.password, user.id], (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('修改数据成功')
})

12.7 修改操作便捷方式

修改数据时,如果数据对象的每个属性数据表的字段一一对应,则可以通过如下方式快速修改数据:

// 修改用户信息
let user = { id: 1, username: '赵四', password: '666' }
// 定义 sql, ? 号表示占位符
let updateSql = 'update user set ? where id=?'
// 执行插入操作
conn.query(updateSql, [user, user.id], (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('修改数据成功')
})

12.8 删除操作

// 删除用户信息
// 定义 sql
let delSql = 'delete from user where id=?'
// 执行 sql
conn.query(delSql, 1, (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('删除数据成功')
})

12.9 标记删除

使用 delete 语句,会把真正的数据从表中删除掉。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。

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

当用户执行了删除的动作时,我们并没有执行 delete 语句把数据删除掉,而是执行了 update 语句,将这条数据对应的 status 字段标记为删除即可。

// 定义 sql, ? 号表示占位符
let updateSql = 'update user set status=? where id=?'
// 执行插入操作
conn.query(updateSql, [1, 5], (err, results) => {
  if (err) return console.log(err.message)
  // affectedRows 表示影响的行数
  if (results.affectedRows === 1) console.log('标记删除成功')
})

13 前后端的身份认证

13.1 web 开发模式

  1. 服务器端渲染的 web 开发模式

服务器端渲染的概念:服务器端发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。

优点

  • 前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
  • 有利于 SEO。因为服务器端相应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。

缺点

  • 占用服务器端资源。即服务器完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
  • 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
  1. 前后端分离的 web 开发模式

前后端分离的概念:前后端分离的 web 开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 web 开发模式,就是后端只负责提供 api 接口,前端使用 Ajax 调用接口的开发模式。

优点

  • 开发体验好。前端专注于 UI 页面的开发,后端专注于 api 的开发,且前端有更好的选择性。
  • 用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
  • 减轻了服务器的渲染压力。因为页面最终是在每个用户的浏览器中生成的。

缺点

不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫无法爬取页面的有效信息。(可以利用 Vue、React 等前端框架的 SSR(server side render)技术能够很好的解决 SEO 问题)

13.2 身份认证

  1. 什么是身份认证?

身份认证(Authentication)又称“身份认证”、“鉴权”,是指通过一定的手段,完成对用户身份的确认。

  1. 为什么需要身份认证?

目的就是为了确认当前所声称为某种身份的用户,确实是所声称的用户。

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

对于服务器渲染前后端分离这两种开发模式来说,分别有着两种不同的身份认证方案:

  • 服务器渲染使用 Session 认证机制
  • 前后端分离使用 JWT 认证机制

13.3 Session 认证机制

13.3.1 HTTP协议的无状态性

HTTP协议的无状态性,指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接关系,服务器不会主动保留每次HTTP请求的状态

13.3.2 如何突破HTTP无状态限制?

为了解决 HTTP 无状态的限制,我们可以使用 Cookie

13.3.3 什么是 Cooke?

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

Node.js 学习笔记_第4张图片

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

Cookie 的几大特性:

  • 自动发送
  • 域名独立
  • 过期时限
  • 4KB 限制

13.3.4 Cookie 在身份认证中的作用

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。 之后,当客户端浏览器每次请求服务器的时候,浏览器自动将身份认证相关的 Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。

Node.js 学习笔记_第5张图片

13.3.5 Cookie 不具有安全性

由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。 因此不建议服务器将重要的隐私数据(比如:用户的身份信息、密码等),通过 Cookie 的形式发送给浏览器。

Node.js 学习笔记_第6张图片

注意:不要使用 Cookie 存储重要且隐私的数据!

13.3.6 提高身份认证的安全性

服务器可通过认证Cookie的形式去提高身份认证的安全性。比如:为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证 。只有收银机确认存在的会员卡,才能被正常使用。

Node.js 学习笔记_第7张图片

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

13.3.7 Session 的工作原理

Node.js 学习笔记_第8张图片

13.4 在 Express 中使用 Session 认证

13.4.1 安装 express-session 中间件

只需在项目的终端中运行如下命令。

npm i express-session

13.4.2 配置 express-session 中间件

通过 app.use() 来注册 session 中间件。

// 导入 session 中间件
const session = require('express-session')
// 配置 session 中间件
app.use(session({
  secret: 'key',          // 值可为任意字符串
  resave: false,          // 固定写法
  saveUninitialized: true // 固定写法
}))

13.4.3 向 Session 中存数据

通过 req.session 来访问和使用 session 对象,从而存储用户的关键信息。

// 登录的 API 接口
app.post('/user/login', (req, res) => {
    // 判断用户提交的登录信息是否正确
    if (req.body.username !== 'admin' || req.body.password !== 'admin') {
        return res.send({ status: 400, msg: '登录失败' })
    }
    // 将登录成功后的用户信息,保存到 Session 中
    // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性
    // user、islogin是自己定义的存储属性
    req.session.user = req.body // 用户的信息
    req.session.islogin = true // 用户的登录状态

    res.send({ status: 200, msg: '登录成功' })
})

13.4.4 从 Session 中取数据

可以直接从 req.session 对象上获取之前存储的数据。

// 获取用户姓名的接口
app.get('/user/username', (req, res) => {
    // 从 Session 中获取用户的名称,响应给客户端
    if (!req.session.islogin) {
        return res.send({ status: 400, msg: 'fail' })
    }
    res.send({
        status: 200,
        msg: 'success',
        username: req.session.user.username,  //获取
    })
})

13.4.5 清空 Session

调用 req.session.destroy() 函数,即可清空服务器保存的 session 信息。

// 退出登录的接口
app.post('/user/logout', (req, res) => {
    // 清空 Session 信息
    req.session.destroy()
    res.send({
        status: 200,
        msg: '退出登录成功',
    })
})

13.5 JWT 认证机制

13.5.1 了解 Session 认证的局限性

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

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

13.5.2 什么是 JWT?

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

13.5.3 JWT 的工作原理

Node.js 学习笔记_第9张图片

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

13.5.4 JWT 的组成部分

JWT 通常由三部分组成,分别是 Header(头部)、Payload(有效荷载)、Signature(签名)。三者之间使用英文的 “.” 分隔,格式如下:

Header.Payload.Signature

其中:

  • Payload 部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。
  • HeaderSignature 是安全性相关的部分,只是为了保证 Token 的安全性。

Node.js 学习笔记_第10张图片

13.5.5 JWT 的使用方式

客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStoragesessionStorage中。此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是把 JWT 放在 HTTP 请求头的 Authorization 字段中,格式如下:

Authorization: Bearer 

13.6 在 Express 中使用 JWT

13.6.1 安装 JWT 相关的包

运行如下命令,安装如下两个 JWT 相关的包jsonwebtoken用于生成 JWT 字符串,express-jwt 用于将 JWT 字符串解析还原成 JSON 对象。

npm i jsonwebtoken express-jwt

13.6.2 导入 JWT 相关的包

// 安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')

注意:如果 express-jwt 是 7.x.x 以上的版本,必须使用如下引入方式,或者降低版本。

const { expressjwt } = require('express-jwt')

13.6.3 定义 secret 密钥

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

  • 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息进行加密,最终得到加密好的 JWT 字符串。
  • 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥进行解密。
// 定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = 'hello ^_^'

13.6.4 生成 JWT 字符串

// 登录接口
app.post('/user/login', function (req, res) {
  // 将 req.body 请求体中的数据,转存为 userinfo 常量
  const userinfo = req.body
  // 登录失败
  if (userinfo.username !== 'admin' || userinfo.password !== 'admin') {
    return res.send({
      status: 400,
      message: '登录失败!',
    })
  }
  // 登录成功
  // 在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
  // 参数1:用户的信息对象
  // 参数2:加密的秘钥
  // 参数3:配置对象,可以配置当前 token 的有效期
  // 注意:千万不要把密码加密到 token 字符中
  const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: '0.5h' })
  res.send({
    status: 200,
    message: '登录成功!',
    token: tokenStr, // 要发送给客户端的 token 字符串
  })
})

13.6.5 将 JWT 字符串还原为 JSON 对象

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

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

// 注册将 JWT 字符串解析还原成 JSON 对象的中间件
// unless 用来指定哪些接口不需要访问权限
// 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上
app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/user\//] }))

注意:如果 express-jwt 是 7.x.x 以上的版本,必须使用如下配置。其中algorithms参数是必需的,以防止在提供第三方库作为‎‎机密‎‎时发生潜在的降级攻击。可选参数有对称和非对称(即HS256/RS256),不要混合使用对称和非对称(即HS256/RS256)算法‎。‎

app.use(expressjwt({ secret: secretKey, algorithms: ['HS256'] }).unless({ path: [/^\/user\//] }))

HS256 和 RS256 两者之间的区别

JWT 签名算法中,一般有两个选择,一个采用 HS256,另外一个就是采用 RS256。签名实际上是一个加密的过程,生成一段标识(也是 JWT 的一部分)作为接收方验证信息是否被篡改的依据。

RS256(采用 SHA-256 的 RSA 签名)是一种非对称算法, 它使用公共/私钥对: 标识提供方采用私钥生成签名,JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护, 因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据 URL)。

HS256(带有 SHA-256 的 HMAC)是一种对称算法,双方之间仅共享一个 密钥。由于使用相同的密钥生成签名和验证签名,因此必须注意确保密钥不被泄密。

在开发应用的时候启用 JWT,使用 RS256 更加安全,你可以控制谁能使用什么类型的密钥。另外,如果你无法控制客户端,无法做到密钥的完全保密,RS256 会是个更佳的选择,JWT 的使用方只需要知道公钥。

由于公钥通常可以从元数据 URL 节点获得,因此可以对客户端进行进行编程以自动检索公钥。如果采用这种方式,从服务器上直接下载公钥信息,可以有效的减少配置信息。

13.6.6 使用 req.auth 获取用户信息

express-jwt 这个中间件配置成功之后,即可在那些有权限的接口中,使用 req.auth 对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:

// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
  // 使用 req.auth 获取用户信息,并使用 data 属性将用户信息发送给客户端
  // console.log(req.auth)
  res.send({
    status: 200,
    message: '获取用户信息成功!',
    data: req.auth, // 要发送给客户端的用户信息
  })
})

13.6.7 捕获解析 JWT 失败后产生的错误

当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行。我们可以通过 Express 的错误中间件,捕获这个错误并进行相关的处理,示例代码如下:

// 使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误
app.use((err, req, res, next) => {
  // 这次错误是由 token 解析失败导致的
  if (err.name === 'UnauthorizedError') {
    return res.send({
      status: 401,
      message: '无效的token',
    })
  }
  res.send({
    status: 500,
    message: '未知的错误',
  })
})

你可能感兴趣的:(node.js,学习,前端)