文章目录
初识Node.js与内置模块
初识Node.js
什么是Node.js
Node.js
是一种基于Chrome V8引擎的JavaScript
运行时环境,用于在服务器端运行JavaScript代码Node.js是JavaScript的后端运行环境
注意:Node.js中无法调用
DOM
和BOM
等浏览器内置API
13 1const arr = [0, 1, 2, [3, 4, [5, 6, [7, 8]]]]23// 提取深度为默认值:14console.log(arr.flat()); // 输出:[0, 1, 2, 3, 4, [5, 6, [7, 8]]]56// 提取深度为默认值:37console.log(arr.flat(3)); // 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8]89// 提取深度为默认值:3010console.log(arr.flat(30)); // 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8]1112// 不会改变原数组的值13console.log(arr) // 输出:[0, 1, 2, [3, 4, [5, 6, [7, 8]]]]js
Node.js可以做什么
- 基于
Express
框架,可以快速构建Web应用- 基于
Electron
框架,可以构建跨平台的桌面应用- 基于
restify
框架,可以快速构建API接口项目- 读写和操作数据库、创建使用的命令工具辅助前端开发、etc…
学习过程
- Node.js内置API模块(fs、path、http等)
- 第三方API模块(express、MySQL等)
区分LTS版本和Current版本的不同
LTS
为长期稳定版,对于追求稳定性的企业项目来说,推荐安装LTS版本的Node.js
Current
版本为尝鲜版,对于热衷于尝试新特性的用户来说,推荐安装Current版本的Node.js。但是,Current版本中可能存在隐藏的BUG或安全漏洞,因此不推荐在企业项目中国使用Current版本的Node.js初学者安装LTS版本既可以了
如何在Node.js环境中执行JavaScript代码
在vscode中打开终端,输入:
node Js文件位置
如:
在终端输入
cls
可以清空终端
更换版本
使用nvm插件来更换版本(nvm需要安装)。
在安装了nvm的前提下,在控制台中输入
nvm help
,可以查看nvm的使用在使用nvm更换node版本的时候,npm同样会换
快速启动服务
需要使用nodemon插件,在管理员终端中,输入:
#全局安装 npm i nodemon -g
使用时,只需要在终端中,使用nodemon启动服务即可:
nodemon .\app.js
这时,更改了app.js 的代码并保存之后,会自动重启服务
fs文件系统模块
什么是fs模块
fs模块
是Node.js官方提供的用来操作文件的模块,它提供了一系列方法和属性,用来满足用户对文件操作需求
- 在JavaScript中使用
fs模块
来操作文件前,需要导入fs模块
const fs = require('fs')
使用fs模块
读取指定文件中的内容
用法:
fs.readFile(path, options, callback(err, dataStr))
path
:必选参数,字符串,表示文件的路径options
:必选参数,字符串,表示以什么编码格式来读取文件,默认是utf8callback
:必选参数,回调函数,文件读取完成后的回调函数
- err:
读取文件成功时,值为null
,所以这时的err
是没有message
读取文件失败时,值为错误对象
当读取文件失败时,打印err.message
,会在终端输出失败的信息- dataStr:
读取文件成功时,值为读取的文件
读取文件失败时,值为undefined
向指定的文件中写入内容
用法:
fs.writeFile(path, data [, options], callback(err))
path
:必选参数,字符串,表示文件的路径data
:必选参数,表示要写入的内容options
:可选参数,字符串,表示以什么编码格式来写入文件,默认是utf8callback
:必选参数,回调函数,文件写入完成后的回调函数
- err:
写入文件成功时,值为null
写入文件失败时,值为错误对象
当写入文件失败时,打印err.message
,会在终端输出失败的信息注意:
当输入的路径没有文件,则会创建一个文件,然后进行写入内容
如:// 有一路径:a\b\c,但是 c 文件夹中没有 d.txt , fs.writeFile('/a/b/c/d.txt','123',err()=>{}) // 在执行写入代码时,因为没有 d.txt,所以先在 a\b\c 文件夹中创建了一个新的 d.txt 文件,后将内容写入这个新创建的文件中
新写入的内容会覆盖原来的内容
fs模块-路径动态拼接问题
在使用fs模块操作文件时,如果提供的操作路径是以
./
或../
开头的相对路径时,很容易出现路径动态拼接错误的问题原因:代码在运行的时候,会执行Node命令时所在的目录,动态拼接出被操作的文件的完整路径
如:
// 在 'JS' 文件中,有这样一个读取文件的函数,这个 'JS' 文件在 ‘C:\我的学习文件\Node学习\Node代码’ fs.readFile('./hellow.txt', 'utf8', (err, dataStr) => {}) // 则在本个 'JS' 文件所在的文件夹中开启的终端执行这个 'JS' 文件,会读取 // 'C:\我的学习文件\Node学习\Node代码\hellow.txt' 这个绝对路径的文件
代码访问路径:
C:\我的学习文件\Node学习\Node代码\hellow.txt
如何解决:
直接使用需要读取的文件的完整路径,但是不利于维护
(不建议使用的方法)使用
__dirname
(前面时两个下划线),它代表文件所在的位置console.log(__dirname) // 在终端输出结果如下
完整用法:
fs.readFile(__dirname + '/00-改写读取文件.txt', 'utf-8', () => {})
path路径模块
什么是path路径模块
是
Node.js
官方提供的、用来处理路径的模块,它提供了一系列的方法和属性,用来满足用户对路径的处理和需求
- 在
JavaScript
中使用path路径模块
处理路径前,需要先导入path路径模块
const path = require('path')
使用path路径模块
路径拼接
作用:将多段路径拼接成一段路径,会以字符串的形式返回出来
用法:
path.join([...paths])
paths
:多个路径,用逗号分隔
例子:
// 引入 path 路径模块
const path = require('path')
// 在拼接的路径中,有一个特殊的路径 “../” ,它会从上一个路径中的文件夹里跳出来,跳到 d 文件夹相同路径中,在和 d 的同级路径中,寻找 e 文件。
// 同理,“../../” 则会返回两级路径,“../” 只会返回一级路径。
const pathStr = path.join('a', 'b/c', 'd', '../', 'e')
console.log(pathStr)
// 输出结果如下:
// a\b\c\e
获取路径中的文件名
作用:可以获取路径中指向的文件,会以一个字符串的形式返回
用法:
path.basename(path [, ext])
path
:必选参数,表示一个路径的字符串ext
:可选参数,表示文件以什么结尾的
输出字符串 + ext = 文件完整名
例子:
const path = require('path')
const fpath = path.basename('C:/我的学习文件/Node学习/Node代码/00-改写读取文件.txt', '文件.txt')
console.log('-----------------------')
console.log(fpath)
console.log('-----------------------')
// 指向文件完整名为: 00-改写读取文件.txt
// 结果如下图:
获取文件的扩展名
用法:
path.extname(path)
,扩展名会以一个字符串的形式返回
path
:表示一个路径的字符串
例子:
const path = require('path')
const ext = path.extname('C:/我的学习文件/Node学习/Node代码/00-改写读取文件.txt')
console.log(ext)
// 输出结果如下:
// .txt
http模块
什么是http模块
是Node.js官方提供的用来创建web服务器的模块
- 服务器:在网络节点中,负责对外提供网络资源的电脑
- 客户端:在网络节点中,负责消费资源的电脑
在
JavaScript
中使用http模块
处理路径前,需要先导入http模块
const http = require('http')
服务器相关的概念
IP地址
IP地址
就是互联网上每台计算机的唯一地址,因此IP地址具有唯一性
IP地址的格式
:(a.b.c.d),其中a、b、c、d都是0~255之间的十进制数
如:(192.168.1.1)互联网中每台Web服务器都有自己的IP地址,例如,可以在控制终端中输入
ping www.baidu.com
来查看百度服务器的IP地址
域名和域名服务器
因为
IP
不便于记忆,所以发明了一套字符型的地址方案,即所谓域名地址
IP地址
和域名
是一一对应的关系,这份关系存放在一种叫做域名服务器(DNS(简称), Domain name server)的电脑中。
127.0.0.1
对应的域名是localhost
端口号
端口号就好比每个小区中的门牌号
在一台电脑中,可以运行很多个web服务,每一个web服务就像一个小区中的屋子一样,可以通过门牌号来找到该这个小区的这间屋子,而这个门牌号就是这个web服务的端口号
注意:在实际应用中,有且仅有URL中的80端口可以被忽略
一个web服务中包含了多个文件(类型可相同),如:
- HTML文件
- CSS文件
- JavaScript文件
- 图片文件
- 数据库文件
- 配置文件
- 服务器脚本文件
创建web服务器
分为4步:
-
导入http模块
const http = require('http')
-
创建web服务器示例
调用
createServer()
方法,即可快速创建一个web服务器示例const server = http.creatServer()
-
为服务器示例绑定request事件,监听用户端的请求
使用
服务器示例
的on('事件类型', callback(req, res))
方法,它和DOM中的addEventListener()
作用类似req
:可选参数,是请求对象,里面包含了客户端
相关的属性或数据req.url
:是客户端请求的URL
地址**(只显示端口号后面的部分URL)**req.method
:是客户端method
请求类型
res
:可选参数,响应对象,里面包含了服务器
相关的数据或属性res.end(str)
:向客户端发送指定的内容,并结束这次请求的处理过程,str
为发送的内容
res.setHeader(key, value)
:设置响应头key
:需要设置的响应头value
:需要设置的响应头的值
server.on('request', (req, res) => { console.log('开启服务') })
解决中文乱码问题:
当调用
res.end()
方法,向客户端发送中文内容的时候,会出现乱码问题,此时,需要手动设置内容的编码格式server.on('request', (req, res)=>{ // 为防止中文乱码,需要设置响应头 "Content-Type" 的值为 "text/html; charset=utf-8" res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end('我爱吃坤蛋') })
-
启动服务器
使用
服务器示例
的listen(端口号, 服务器启动成功后调用的回调函数)
方法server.listen(110, () => { console.log("http server running at 'http://127.0.0.1:110'") })
#
模块化
模块化的基础
什么是模块化
指解决复杂问题时,自动向下逐层把系统划分为若干模块的过程,对于整个系统来说,模块是可组合、分解和更换的单元
- 简单来说,就是遵守固定的规则,把一个大文件
拆成
独立并互相依赖的多个小模块
优点:
- 提高了代码的复用性
- 提高了代码的可维护性
- 可以实现按需加载
模块化规范
模块化规范就是对代码进行模块化的拆分与结合时,需要遵守的那些规则
如:
- 使用什么样的语法格式来引用模块
- 在模块中使用什么样的语法规范向外暴露程艳
好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本
Node.js中的模块
Node.js中模块的分类
Node.js中根据模块来源的不同,将模块分为了3大类,分别是:
内置模块
:由Node.js官方提供的,例如:fs
、path
、http
等自定义模块
:用户创建的每个js文件,都是自定义模块第三方模块
:由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载
加载模块
使用
require()
方法,可以加载需要的内置模块、自定义模块、第三方模块
// 1、加载内置的 fs 模块
const fs = require('fs')
// 2、加载用户自动定义模块(后面的 .js 文件扩展名可以省略)
const custom = require('./custom.js')
// 3、加载第三方模块
const moment = require('moment')
注意:使用require()
方法加载其他模块时,会执行被加载模块中的代码
Node.js中的模块作用域
是什么:和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,这种模块级别的访问限制,叫做模块作用域
好处:防止全局变量污染的问题
向外共享模块作用域中的成员
module对象
在每个js自动定义模块中都有一个
module对象
,它里面存储了和当前模块有关的信息如:
module.exports
对象向外共享作用域中的成员外界使用
require()
方法导入自定义模块时,得到的就是module.exports
所指向的对象如:
// A 文档 const username = '蔡坤坤' module.exports.name = username console.log(module.exports)
// B 文档,导入 A 文档 const custom = require('A') console.log(custom)
终端输出结果如下:
注意:由于
module.exports
单词写起来比较复杂,Node提供了exports对象
,默认情况下,exports
和module.exports
指向同一个对象。最终共享的结果,但是读取到的对象还是以module.exports
指向的对象为准// exports 和 module.exports 指向同一个对象 console.log(exports === module.exports) // true
exports和module.exports的使用误区
重点:使用
require()
时,得到的永远是module.exports
指向的对象
他们两个本来都是指向一个对象,但是,对其中一个进行了对象重赋值(即exports = {})后,两个就不指向同一个对象了
exports.username = 'cxk' module.exports = { age: '2.5 years' } // require() 后,对象为:{age:'2.5 years'}
module.exprots.username = 'jntm' exports = { gender: '未知' } // require() 后,对象为:{username:'jntm'}
exports.name = 'cxk' module.exports.like = 'basketball' // require() 后,对象为:{name:'cxk',like:'basketball'}
exports = { name:'cxk' } module.exports = exports module.exports.age = '2.5' // require() 后,对象为:{name:'cxk',age:'2.5'}
Node.js中的模块化规范
Node.js
遵循了CommonJS模块化规范,CommonJS规定了模块的特性和各模块之间如何相互依赖
CommonJS
规定:
- 每个模块内部,module变量代表当前模块
- module变量是一个对象,他的exports属性(即
module.exports
)是对外的接口 - 加载某个模块,其实是加载该模块的
module.exports
属性,require()
方法用于加载模块
npm与包
什么是包
Node.js中的第三方模块又叫做包
包的来源
不同于
Node.js
中内置模块与自定义模块,包是由第第三方个人或团队开发出来的
为什么需要包
由于Node.js的内置模块仅提供了一些底层的API,导致在基于内置模块进行项目开发时,效率很低
包的好处:包是基于内置模块封装出来的,提供了更高级、更方便的API,极大提高了开发效率
如何下载包
包的搜索网站,利用前面的网站来搜索需要的包,
在安装Node.js的时候,同时安装了一个包管理工具,叫Node Package Manager(简称npm),利用这个包管理工具来安装所需要的包
如何用npm安装包:(需要在终端中输入)
npm install 包的完整名称
上述的装包命令,可以简写成如下格式:(将
install
简写成i
)npm i 包的完整名称
注意:在那个文件夹下输入,则包就下在那个文件夹下
上面的方法会默认安装最新的包
如果想安装指定的版本,则在包的后面加上
@指定版本号
(以moment包为例)npm i moment@2.22.2
包的语义化版本规则
包的版本号是以”点分十进制“形式进行定义的,总共有三位数字,如:2.24.0
其中每一个数字所代表的含义如下:
- 第1位数字:大版本(地层更新后,数字 +1)
- 第2位数字:功能版本(更新功能后,数字 +1)
- 第3位数字:Bug修复版本(修完Bug后,数字 +1)
只要前面的版本号增长了,则后面的版本号归零
如:1.12.6 ---> 1.13.0
包管理配置文件
npm规定,在项目根目录中,必须提供一个叫package.json的包管理配置文件,用来记录和项目有关的一些配置信息,如:
- 项目的名称、版本号、描述等
- 项目中都用到了那些包
- 哪些包只在开发期间会用到
- 哪些包在开发和部署时都用到
多人协作问题
在整个项目的体积中,第三方包的体积占比远远大于项目源代码的
问题:第三方包的体积过大,不方便团队成员之间共享项目源代码
解决方法:共享时剔除
node_modules文件夹
如何记录项目中安装了哪些包
在项目根目录中,创建一个叫做
package.json
的配置文件,即可用来记录项目中安装了哪些包,从而方便剔除node_modules
目录之后,在团队成员之间共享项目的源代码所以,在将项目上传到GitHub时,要把node_modules文件夹添加到.gitignore忽略文件中
快速创建package.json
npm包管理工具提供了一个快捷命令,可以在执行命令时所处的目录中,快速创建package.json这个包管理配置文件:
// 在终端中输入 npm init -y
注意:
- 上述命令只能在英文的目录下成功运行,所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格
- 运行
npm install
命令安装包的时候,npm包管理工具会自动把报的名称和版本号记录到package.json
中- 对于目前来说,安装第一个包时,会自动创建一个
package.json
文件
dependencies节点
package.json
文件中,有一个dependencies节点
,专门用来记录你使用npm install
命令安装了哪些包如果某些包在开发和项目上线之后都需要用到,则建议把这些包记录到
dependencies
节点中
一次性安装所有的包
npm install // 或 npm i
// 执行 npm install 命令时,npm 包管理工具会先读取 package.json 中的 dependencies 节点,读取到记录的所有的包的名字和版本号后,npm 包管理工具会把这些包一次性下载到项目中
卸载包
运行npm uninstall
命令来卸载指定的包
npm uninstall moment // 卸载 moment 包
注意:在使用了该命令卸载指定的包后,同时会删除 package.json
中的包的名字和版本号
DevDependencies节点
如果某些包只有在项目开发阶段会用到,在项目上线之后不会用到,则建议把这些包记录到
DevDependencies
节点中// 安装指定的包,并记录到 DevDependencies 节点中 npm install 包名 --save-dev // 简写 npm i 包名 -D
切换npm下包镜像源
下包的镜像源,指的就是下包的服务器地址
// 查看当前的下包镜像源
npm config get registry
// 将下包的镜像源切换为淘宝镜像源(等号两侧不能有空格)
npm config set registry=https://registry.npmmirror.com
规范的包结构
- 每个包独占一个文件夹
- 包的顶级目录下必须包含package.json这个包管理配置文件
- package.json中必须包含name、version、main这三个属性,分别代表包的名字、版本号、入口文件。
入口文件:require('moment')
导入时,以什么文件进行导入的
发布包
登录:在需要发布的包的父文件夹中,打开终端,输入
npm login
进行登录发布:在需要发布的包的文件夹中,打开终端,输入
npm publish
注意:需要发布的包不能和其他人已经发布的包的名字雷同
删除包
在终端输入
npm unpublish 包名 --force
,即可删除72小时以内发布的包被删除的包在24小时内
不允许
重新发布
模块的加载机制
优先从缓存中加载
模块在第一次加载后会被缓存,这也意味着多次调用
require()
不会导致模块的代码被多次执行注意:不论是内置模块、用户自定义模块、还是第三方模块,它们都会优先从缓存这中加载,从而提高模块的加载效率
内置模块加载机制
内置模块的加载优先级最高
自定义模块的加载机制
使用
require()
加载自定义模块时,必须指定以./
或../
开头的路径标识符。在加载自定义模块时,如果没有指定的路径标识符,则node会把他当做内置模块或第三方模块
如果在使用require()
导入自定义模块时,如果省略了文件的扩展名,则Node.js
会按顺序分别尝试加载以下文件:
- 按照确切的文件名进行加载
- 补全**.js**扩展名进行加载
- 补全**.json**扩展名进行加载
- 补全**.node**扩展名进行加载
- 加载失败,终端报错
第三方的加载机制
如果传递给
require()
的模块标识符不是一个内置模块,也没有./
或../
开头,则Node.js
会从当前的模块的父目录开始,尝试从**/node_modules文件**中加载第三方模块。如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
例如,假设在**“C:\Users\itheima\project\foo.js”**文件里调用了require('tools')
,则Node.js会按一下顺序查找:
C:\Users\itheima\project\
**node_modules\**toolsC:\Users\itheima\
**node_modules\**toolsC:\Users\
**node_modules\**toolsC:\
**node_modules\**tools
目录作为模块
当把**包的目录(文件夹名)**作为标识符,传递给
require()
进行加载的时候,有三种加载方式:
- 在被加载的目录下查找一个叫做
package.json
的文件,并寻找main属性
作为require()
加载的入口- 如果目录里面没有
package.json
文件,或者main
入口不存在或无法解析,则Node.js将会试图加载目录下的index.js文件- 如果以上两步都失败了,则Node.js会在终端打印错误信息,报告模块的缺失:Error: Cannot find module ‘xxx’
Express
初识Express
什么是Express
官方解释:Express是基于Node.js平台,快捷、开放、极简的Web开发框架
通俗理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的
Express的本质
:就是一个npm上的第三方包,由Node.js原生http模块封装而成,提供了快速创建Web服务器的边界方法
Express可以做什么
对于前端来说,最常见的两种服务器,分别是:
Web网站服务器
:专门对外提供Web网页资源的服务器API接口服务器
:专门对外提供API接口的服务器
Express的基本使用
安装
在终端输入
npm i express@4.17.1
(以这个版本为例)
创建基本的Web服务器
// 1、导入 express
const express = require('express')
// 2、创建 web 服务器
const app = express()
// 3、调用 app.listen(端口号, 启动成功后的回调函数) 来启动服务器
app.listen(80, () => {
console.log('!^.^!')
})
监听GET、POST请求
GET
:通过
app.get()
方法,可以监听客户端的GET请求,具体方法:
app.get('请求url', function(req, res) {})
req
:请求对象,包含了与请求相关的属性与方法res
:响应对象,包含了响应相关的属性与方法
POST
:通过
app.post()
方法,可以监听客户端的POST请求,具体方法:
app.post('请求url', function(req, res) {})
把内容响应给客户端
res.send('内容')
,将 ‘内容’ 响应给客户端,内容的数据类型不可以是Number
,不然会报错
app.get('/server', (req, res) => {
res.send('成功响应内容给客户端!')
})
获取URL中携带的查询参数
通过
req.query
对象,可以访问到客户端通过查询字符串的形式发送到服务器的参数
req.qurey
默认是一个空对象
如:输入URL为:http://localhost:133/server?name=cxk&age=2.5
app.get('/server', (req, res) => {
console.log(req.query) // {"name":"cxk","age":"2.5"}
})
获取URL中的动态参数
通过
req.params
对象,可以访问到URL中通过:
匹配到的动态参数(:
后面跟的参数可以任意改)
req.params
默认是一个空对象
如:输入ULR为:http://localhost:133/server/1
// 这里的 :id 是动态的参数,必须要写,冒号 : 后面可以任意写
app.get('/server/:id', (req, res) => {
console.log(req.params) // { id: '1' }
})
// 更改URL为 : http://localhost:133/server/2/cxk
app.get('/server/:ids/:name', (req, res) => {
console.log(req.params) // {"ids":"2","name":"ckx"}
})
托管静态资源
托管单个资源目录
express.static()
express提供了一个非常好用的函数,叫做
express.static()
,通过它,我们可以非常方便地创建一个静态资源服务器例如:通过如下代码就可以将public目录下的图片、CSS文件、JavaScript文件对外开放了
app.use(express.static('public')) // app.use 的用法在后面
注意:express在指定的静态目录中查找文件,并对外提供资源的访问路径,因此,存放静态文件的目录名不会出现在URL中
如:
http://localhost:3000/
images/bg.png
http://localhost:3000/
css/style.css
http://localhost:3000/
js/login.js
托管的文件夹需要在和node_modules文件夹同级下,向内查询,如:
需要托管pages文件夹,则需要从路径”综合案例“文件夹向下寻找:
app.use(express.static('综合案例/session案例/pages'))
托管多个资源目录
如果要托管多个静态资源目录,请多次调用express.static()函数:
app.use(express.static('public')) app.use(express.static('files'))
访问静态文件时,express.static()函数会根据目录的添加顺序来查找所需文件
如:(获取public文件夹中的文件的优先级高于其他文件夹)
- 如果public、files中都有
abc.js
,则在输入http://localhost:3000/abc.js
后,访问的是public中的abc.js
- 如果只有files中有
cxk.css
,则在输入http://localhost:3000/cxk.css
后,访问的是files中的cxk.css
挂载路径前缀
如果希望在托管的静态资源访问路径之前,挂载路径前缀,则可以使用如下方法:
app.use('/public', express.static('public'))
现在就可以通过带有**/public前缀地址来访问public目录**的文件了:
http://localhost:3000/public/images/abc.png
http://localhost:3000/public/css/style.css
http://localhost:3000/public/js.app.js
访问服务地址,显示那个页面
在开启了express服务后,访问服务地址,默认显示的其实是托管的静态资源文件夹下的
index.html
文件,如:// 托管静态资源文件夹 app.use(express.static('public')) // 启动服务 app.listen(3456, () => { console.log('服务地址:http://127.0.0.1:3456') })
在访问
http://127.0.0.1:3456
地址时,其实访问的全部地址为http://127.0.0.1:3456/index.html
。
注意:当托管的静态资源文件夹下,没有一个叫index.html
的文件时,访问http://127.0.0.1:3456
是不会显示出什么的
这时,在地址后面,添加该托管的静态资源文件夹下的某个HTML文件,就可以显示出该HTML文件在浏览器中,如:http://127.0.0.1:3456/abc.html
。(abc.html为public文件夹下的一个文件)
Express路由
什么是路由
在express中,路由指的是客户端的请求与服务器处理函数之间的映射关系
express中的路由分3部分组成,
app.method(path, handler)
,分别是:
- method:请求的类型
- path:请求的URL地址
- handler:处理函数
例子:
app.get('/', function(req, res) => {})
路由的匹配过程
当每一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数
在匹配时,会按照路由的先后顺序进行匹配,如果匹配时,
客户端
的请求类型和请求的URL和**服务器
中的请求类型和请求的URL**对应,则express会将这次请求转交给对应的function
函数处理
路由的使用
在express中使用路由最简单的方式,就是把路由挂到创建的Web服务器上,如:
const express require('express') // 创建 Web服务器,命名为 app const app = express() // 挂载路由 app.get('/', (req, res) => { res.send('你好') }) app.post('/', (req, res) => { res.send('hellow') }) // 启动 Web服务器 app.listen(80, () => { console.log('开启监听') })
模块化路由
为了方便对路由进行模块化的管理,express
不建议
将路由直接挂载到 Web服务器(app)上,推荐将路由抽离为单独的模块
将路由抽离为单独模块的步骤如下:
- 创建路由对应的
.js
文件 - 调用
express.Router()
函数创建路由对象 - 向路由对象上挂载具体的路由
- 使用
module.exports
向外共享路由对象 - 使用
app.use()
函数注册路由模块
app.use()
函数的作用,就是用来注册全局中间件
代码实现:
创建的路由模块
const express = require('express')
// 创建路由对象
const router = express.Router()
router.get('/server', (req, res) => {
res.send('GET')
})
router.post('/server', (req, res) => {
res.send('POST')
})
module.exports = router
使用路由模块的js
const express = require('express')
// 导入创建的模块
const router = require('./导入模块.js')
const app = express()
// 注册路由模块
app.use(router)
app.listen(133, () => {} )
为路由模块添加前缀
类似于托管静态资源挂载前缀,路由模块添加前缀的方式同样简单:
假设原本没加前缀时,输入
http://localhost:133/user
就可以访问// 路由模块.js const router = express.Router() router.get('/user', (req, res) => {}) router.post('/user', (req, res) => {}) module.exports = router
// 导入路由模块 const userRouter = require('/路由模块.js') const app = express() // 给路由模块添加前缀 app.use('/server', userRouter) app.listen(133, () => {})
此时需要输入
http://localhost:133/server/user
(中间多了一个**/server**)才可以访问
Express中间件
什么是中间件
中间件(Middleware),特质业务流程的
中间处理环节
例子理解:
在处理污水的时候,一般都要经过三个处理环节
处理污水的这三个中间处理环节,就可以叫做
中间件
express中间件的调用流程
当一个请求到达express的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
express中间件的格式
express的中间件,本质上就是一个function处理函数,express中间件的格式如下:
app.get('/', function(req, res, next) { next() } )
next
:next函数是实现多个中间件连续调用的关键,它表示把流转关系交给下一个中间件或路由其中的
function
就是中间件函数,他的参数必须包含next。而路由处理函数只包含了req和res参数
express中间件的使用
客户端发送的任何请求,到达服务器之后,都会触发的中间件,叫做全局生效的中间件
通过调用
app.use(中间件函数)
,即可定义一个全局生效的中间件
// 创建一个中间件函数
const mf = function (req, res, next) {
console.log('使用了中间件')
// 把流转关系,转交给了下一个中间件或路由
next()
}
// 将 mf 注册为全局生效的中间件
app.use(mf)
app.get('/', (req, res) => {
res.send('USER')
})
app.listen(133, () => {
console.log('监听成功')
})
// 可以不定义中间件函数,直接在app.use()中写入中间件函数就可以实现
app.use( (req, res, next) => { next() } )
中间件的作用
多个中间件之间,共享同一份
req
和res
,基于这样的特性,我们可以在上游的中间件中,统一为req
或res
对象添加自定义的属性或方法,供下游的中间件或路由进行使用
定义多个全局
中间件
-
方法一
const express = require('express') const app = express() function f1(req, res, next) { req.a = 10 next() } function f2(req, res, next) { res.b = 20 next() } function f3(req, res, next) { console.log(req.a, res.b) next() } // 注册全局中间件,中间件的执行会根据注册的顺序来 app.use(f1, f2, f3) app.get('/', (req, res) => { console.log(req.a, res.b) }) app.get('/server', (req,res) => { console.log(req.a, res.b) }) app.listen(133, () => { console.log('成功') }) //不论是访问 http://localhost:133/ 还是 http://localhost:133/server // 执行结果都相同,如下图:
-
方法二
app.use(function (req, res, next) { req.a = 10 next() }) app.use(function (req, res, next) { res.b = 20 next() }) app.use(function (req, res, next) { console.log(req.a, res.b) next() }) // 这样会从上而下执行这三个中间件
局部
中间件
不使用
app.use()
定义的中间件,就叫做局部生效的中间件,它只对加入了局部中间件的路由有效如:
const express = require('express') const app = express() const mf = function (req, res, next) { console.log('这时局部生效的中间件') req.a = '天才' res.b = '就是我' next() } // 加入局部中间件 mf app.get('/', mf, (req, res) => { console.log(req.a, res.b) console.log('局部作用域生效--1') }) // 未加入局部中间件 app.get('/server', (req, res) => { if (req.a && res.b) { console.log(req.a, res.b) console.log('局部作用域生效--2') } else { console.log('未连接局部作用域 mf') } }) app.listen('133', () => { console.log('监听成功:!^.^!') })
- 当访问
http://localhost:133/
时,执行结果如下图:
- 当访问
http://localhost:133/server
时,执行结果如下图:
加入多个局部中间件
有2中方法:
-
app.get('/', mf1, mf2, (req, res) => {} )
-
app.get('/', [mf1, mf2], (req, res) =>{} )
中间件的使用注意事项
- 一定要在使用之前注册中间件(app.use()注册)
- 客户端发送过来的请求,可以连续调用多个中间件进行处理
- 执行完中间件的业务代码之后,不要忘记**调用next()**函数
- 为了防止代码逻辑混乱,调用next()函数后,不要在写额外的代码
- 连续调用多个中间件是,多个中间件之间,共享
req
和res
对象
中间件的分类
为了方便大家理解和记忆中间件的使用,express官方把常见的中间件用法,分成了5大类,分别是:
- 应用级别的中间件
- 路由级别的中间件
- 错误级别的中间件
- express内置的中间件
- 第三方的中间件
应用级别的中间件
通过
app.use()
或app.get()
或app.post()
,绑定到app示例
上的中间件,叫做应用级别的中间件
如:
// 应用级别的中间件(全局中间件)
app.use((req, res, next) => {
next()
})
// 应用级别的中间件(局部中间件)
app.get('/', mf, (req, res) => {})
路由级别的中间件
绑定到
express.Router()
示例上的中间件,叫做路由级别的中间件,它的用法和应用级别中间件
没有任何区别,只不过,应用级别中间件是绑定到app示例上,路由级别中间件绑定到router示例上
如:
const router = express.Router()
// 添加路由级别中间件
router.use((req, res, next) => {
next()
})
app.use('/', router)
错误级别的中间件
作用:专门用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
格式:错误级别中间件的function处理函数中,必有4个形参,分别是
(err, req, res, next)
注意:错误级别中间件必须注册在所有路由之后,否则会报错
如:
app.get('/', (req, res) => {
// 抛出一个错误
thorw new Error('服务器内部发生了错误')
// 当有错误级别中间件的时候,是不会执行这一行代码的
res.send('Home Page')
})
// 错误级别中间件
app.use((err, req, res, next) => {
console.log('发生了错误' + err.message)
res.send('发生了错误' + err.message)
})
// 虽然会报错,但是项目不会崩溃
express内置的中间件
自express的4.16.0版本开始,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-form-urlencoded 格式数据的内置中间件
app.use(express.urlencoded( { extended: false } ))
第三方的中间件
非express官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。
使用步骤:
- 运行
npm install 中间件名
安装中间件- 运行
require()
导入中间件- 调用
app.use()
注册并使用中间件
自定义中间件
手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器器的表单数据
实现步骤:
- 定义中间件
- 监听req的data事件
来获取客户端发送到服务器的数据,如果数据量比较大,无法一次性发送完毕,则客户端会把数据进行分割后,分批发送到服务器,所以data时间内可能会触发多次,每触发一次data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接 - 监听req的end事件
当请求体数据接收完毕之后,会自动触发req的end事件,所以可以在end事件中,拿到并处理完整的请求体的数据 - 使用querystring模块解析请求体数据
Node.js
内置了一个querystring模块
,专门用来处理查询字符串,通过这个模块提供的parse()
函数,可以轻松把查询的字符串解析成对象的格式 - 将解析出来的数据对象挂载为req.body
上下游的中间件
及路由
之间,共享一份req
和res
,因此,我么可以将解析出来的数据,挂载为req的自定义属性req.body
,供下游使用 - 将自定义中间件封装为模块
使用Express是写接口
实现步骤:
- 创建基本的服务器
- 创建API路由模块
- 编写GET接口
- 编写POST接口
- CORS跨域跨域资源共享
创建基本的服务器
// 创建的服务器
const express = require('express')
const app = express()
app.listen(80, function () {
console.log('启动服务成功与:http://localhost:80')
})
创建API路由模块
// 路由模块
const express = require('express')
const Router = express.Router()
module.exports = Router
// 创建的服务器
const express = require('express')
// 导入路由
const Router = require('./24-2-express-写接口-创建路由.js')
const app = express()
// 注册路由
app.use('/api', Router)
app.listen(80, function () {
console.log('启动服务成功与:http://localhost:80')
})
编写GET接口
// 路由模块
const express = require('express')
const Router = express.Router()
// 给路由绑定get请求,当客户端请求方式为get请求,并且请求地址为 "/get"时,会执行后面的回调函数
Router.get('/get',(req, res) => {
// 获取到客户端通过查询字符串,发送到服务器的数据
const query = req.query
// 调用res.send()方法,把数据响应给客户端
res.send({
status:0, // 状态,0表示成功,1表示失败
msg:'GET请求成功', // 状态描述
data:query, // 需要响应给客户端的具体数据
})
})
编写POST接口
apiRouter.post(/post',(req,res)=>{
//1.获取客户端通过请求体,发送到服务器的URL-encoded数据
const body = req.body
//2.调用res.send()方法,把数据响应给客户端
res.send({
status: // 状态,0表示成功,1表示失败
msg:'POST请求成功!', // 状态描述消息
data:body // 需要响应给客户端的具体数据
})
})
注意:如果号获取URL-encoded格式的请求体数据,必须配置中间件:app.use(express.urlencoded({exrended:false}))
const express = require('express')
const app = express()
// 配置解析表单数据终极爱你建
app.use(express.urlencoded({ extended: false }))
const Router = require('./24-2-express-写接口-创建路由.js')
app.use('/api', Router)
app.listen(80, function () {
console.log('启动服务成功与:http://127.0.0.1:80')
})
CORS跨域跨域资源共享
接口的跨域问题
上面编写的接口与,在一个很严重的问题:不支持跨域请求。
解决接口跨域问题的方案主要有两种:
- CORS(主流的解决方案,推荐使用)
- JSONP(有缺陷的未解决方案,只支持GET请求)
使用CORS中间件解决跨域问题
CORS是express的一个第三方中间件,通过安装和配置CORS中间件,可以很方便地解决跨域问题。
使用步骤分为如下3步:
- 运行
npm i cors
安装中间件 - 使用
const cors = require('cors')
导入中间件 - 在路由之前调用
app.use(cors())
注册中间件
什么是CORS
CORS
(Cross-Origin Resource Sharing,跨域资源共享)由一系列HTTP响应头
组成,这些HTTP响应头决定浏览器是否阻止前端JS代码跨域获取资源。
浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了CORS 相关的HTTP响应头,就可以解除浏览器端的跨域访问限制。
CORS的注意事项
- ORS主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。
- ORS在浏览器中有兼容性。只有支持XMLHttpRequestLevel2的浏览器,才能正常访问开启了CORS的服务端接(例如:IE10+、Chrome4+、FireFox3.5+)。
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')
如果指定了Access-Control-Allow-Origin
字段的值为通配符*
,表示允许来自任何域的请求,示例代码如下:
res.setHeader('Access-Control-Allow-Origin','*')
CORS 响应头部-Access-Control-Allow-Headers
默认情况下,CORS仅支持客户端向服务器发送如下的9个请求头:
Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (值仅限于 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一)
如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过Access-Control-Allow-Headers
对额外的请求头进行声明,否则这次请求会失败!
//允许客户端额外向服务器发送Content-Type请求头和X-Custom-Header请求头
// 注意:多个请求头之间使用英文的逗号进行分割
res.setHeader('Access-Control-Allow-Headers','Content-Type,X-Custom-Header')
ORS响应头部-Access-Control-Allow-Methods
默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。
如果客户端希望通过 PUT
、DELETE
等方式请求服务器的资源,则需要在服务器端,通过Access-Control-Alow-Methods
来指明实际请求所允许使用的HTTP方法。
//只允许POST、GET、DELETE、HEAD请求方法
res.setHeader('AcceSs-Control-Allow-Methods','POST,GET,DELETE,HEAD')
//允许所有的HTTP请求方法
res.setHeader('Access-Control-Allow-Methods','*')
CORS请求的分类
客户端在请求CORS接口时,根据请求方式和请求头的不同,可以将CORS的请求分为两大类,分别是:
- 简单请求
同时满足以下两大条件的请求,就属于简单请求:请求方式
:GET、POST、HEAD 三者之一HTTP头部信息
不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type (只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)
- 预检请求
只要符合以下任何一个条件的请求,都需要进行预检请求:- 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
- 请求头中包含自定义头部字段
- 向服务器发送了application/json格式的数据
在浏览器与服务器正式通信之前,浏览器会先发送OPTION请求进行预检,以获知服务器是否允许该实际请求,所以这一次的OPTION请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据。
简单请求和预检请求的区别:
简单请求的特点:客户端与服务器之间只会发生一次请求。
预检请求的特点:客户端与服务器之间会发生两次请求,OPTION预检请求成功之后,才会发起真正的请求。
数据库与身份验证
数据库的基本概念
常见的数据库及分类
市面上的数据库有很多种,最常见的数据库有如下几个:
MySQL数据库
(目前使用最广泛、流行度最高的开源免费数据库;Community + Enterprise)- Oracle数据库(收费)
- SQLServer数据库(收费)
- Mongodb数据库(Community + Enterprise)
其中,MySQL、Oracle、SQLServer属于传统型数据库(又叫做:关系型数据库或SQL数据库),这三者的设计理念相同,用法比较类似。
而Mongodb属于新型数据库(又叫做:非关系型数据库或NoSQL数据库),它在一定程度上弥补了传统型数据库的缺陷。
传统型数据库的数据组织结构
数据的组织结构:指的就是数据以什么样的结构进行存储。
传统型数据库的数据组织结构,与Excel中数据的组织结构比较类似。
因此,我们可以对比着Excel来了解和学习传统型数据库的数据组织结构。
Excel的数据组织结构
每个Excel中,数据的组织结构分别为工作薄、工作表、数据行、列这4大部分组成。
①整个Excel叫做工作薄
②users和books是工作表
③users工作表中有3行数据
④每行数据由6列信息组成
⑤每列信息都有对应的数据类型
传统型数据库的数据组织结构
在传统型数据库中,数据的组织结构分为数据库(database)、数据表(table)、数据行(row)、**字段(field)**这4大部分组成。
①数据库类似于Excel的工作薄
②数据表类似于Excel的工作表
③数据行类似于Excel的每一行数据
④字段类似于 Excel 的列
⑤每个字段都有对应的数据类型
实际开发中库、表、行、字段的关系
- 在实际项目开发中,一般情况下,每个项目都对应独立的数据库。
- 不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books表中。
- 每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、user、name、password 这3个字段。
- 表中的行,代表每一条具体的数据。
安装并配置MySQL
了解需要安装哪些MySQL相关的软件
工具
Navicat
:可视化的MySQL管理工具
终端管理MySQL
- 默认情况下,在终端是无法直接执行
mysql
命令的,会无法识别,这时需要配置环境变量
- 接着右键点击我的电脑
- 再点击
- 点击高级系统设置
- 点击环境变量
- 在第一步中选择Path,选择编辑
将MySQL Server 8.0文件夹下的bin文件夹添加到里面
我的是:C:\Program Files\MySQL\MySQL Server 8.0\bin
-
在终端中输入
mysql -u用户名 -p密码 # 如: mysql -uroot -padmin123
其中,root为用户名,admin123为密码
-
终端中查看用户信息
# 执行SQL查询用户信息 select host,user,plugin,authentication_string from mysql.user;
-
如果在后面,node中使用mysql模块无法连接到MySQL数据库,并报错:
ER_NOT_SUPPORTED_AUTH_MODE: Client does not support authentication protocol requested by server; consider upgrading MySQL client
,意为:客户端不支持服务器请求的身份验证协议;考虑升级MySQL客户端;报错原因
最新的MySQL模块并未完全支持MySQL 8.0的caching_sha2_password
加密方式,而MySQL 8.0中默认仍然是caching_sha2_password
加密方式,因此用户认证不通过了。解决方案
终端中输入:ALTER USER '账户'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';
,
如:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'q791469353';
然后输入:
select host,user,plugin,authentication_string from mysql.user;
MySQL的基本使用
使用 Navicat 连接数据库
- 点击连接
- 选择MySQL数据库
完成以上的步骤后,出现新建连接:

- 连接名:随便取
- 用户名:使用下载MySQL数据库时创建的用户名(默认为root)
- 密码:创建用户名时的密码
其他选项没有出现问题就可以不用更改
创建数据库
- 在数据库列表中,右键选择新建组
- 然后右键新建的组,然后新建连接,选择MySQL数据库:[连接数据库](#使用 Navicat 连接数据库)
- 双击打开连接

-
右键连接,创建数据库
- 数据库名:不要使用中文,也不要有空格
- 字符集:选择
utf8mb4
- 排列规则:可以不填写
创建数据表
基本操作
- 打开创建的数据库,右键其中的“表”,选择新建表
- 设计表的字段
- Ctrl+S保存,并给表命名
配置表的字段
在这里,根据字段的类型,可以配置不同的东西
同样可以在“索引”进行一些配置
-
输入索引名:
-
选择要添加的字段(可多选),并选择排序顺序(无所谓,二选一)
-
添加索引类型:
-
Normal(普通索引):
- 普通索引是最常见的索引类型。
- 它用于加速查询操作,提高查询性能。
- 允许重复值,可以在一个字段上创建多个普通索引。
-
Unique(唯一索引):
- 唯一索引要求索引列的值是唯一的,不允许重复值。
- 它用于保证数据的唯一性约束。
- 唯一索引可以加速唯一性检查和查找操作。
-
Spatial(空间索引):
- 空间索引用于处理空间数据,如地理位置信息、二维图形等。
- 它支持空间范围查询和最近邻查询等操作。
- 空间索引使用特定的空间算法来加速空间数据的检索。
-
Fulltext(全文索引):
- 全文索引用于支持全文搜索功能。
- 它可以在文本数据中快速搜索关键词,支持模糊搜索、分词和关键词匹配等操作。
- 全文索引适用于文本处理和搜索引擎等应用场景。
-
-
添加索引方法:
- B树索引(B-Tree Index):
- B树索引是一种常见的平衡树结构索引,适用于范围查询和精确查找。
- B树索引适用于传统的关系型数据库系统,如MySQL、Oracle等。
- B树索引的特点是支持高效的插入和删除操作,能够自动平衡和调整树结构。
- 哈希索引(Hash Index):
- 哈希索引使用哈希表的结构来快速查找数据,适用于等值查找。
- 哈希索引通常用于内存数据库或具有特定查询模式的场景。
- 哈希索引的特点是查找速度快,但不支持范围查询和排序操作。
使用SQL管理数据库
什么是SQL
SQL(英文全称:StructuredQueryLanguage)是结构化查询语言
,专门用来访问和处理数据库的编程语言。能够让我们以编程的形式,操作数据库里面的数据。
三个关键点:
- SQL是一门数据库编程语言
- 使用SQL语言编写出来的代码,叫做SQL语句
- SQL语言只能在关系型数据库中使用(例如MySQL、Oracle、SQLServer)。非关系型数据库(例如Mongodb)不支持SQL语言
SQL能做什么
- 从数据库中查询数据
- 向数据库中插入新的数据
- 更新数据库中的数据
- 从数据库删除数据
- 可以创建新数据库
- 可在数据库中创建新表
- 可在数据库中创建存储过程、视图
SQL的学习目标
重点掌握如何使用SQL从数据表中:
- 查询数据(
select
) - 插入数据(
insert into
) - 更新数据(
update
) - 删除数据(
delete
)
额外需要掌握的4种SQL语法:
and
、or
、not
运算符where
条件order by
排序count(*)
函数
SQL的AND、OR、NOT运算符
AND、OR可以在WHERE子句中,把两个或多个条件结合起来
NOT条件取反
- AND表示必须同时满足多个条件,相当于
&&
与运算符,例如:a <= 10 && a >= 5
- OR表示只要满足任意一个条件即可,相当于 || 或运算符,例如:
a === 10 || a === 20
- NOT表示条件取反,如:
WHERE NOT id > 10
(筛选id <= 10
的列)
SQL的WHERE
子句
WHERE
子句用于限定选择的标准,在SELECT
、UPDATE
、DELETE
语句中,亦可使用WHERE
子句来限定选择的标准
可在WHERE
子句中使用的运算符
下面的运算符可以在WHERE字居中使用,用来限定选择的标准
运算符 | 描述 |
---|---|
= | 等于 |
<> | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
BETWEEN AND | 在某个范围内 |
LIKE | 搜索某种模式 |
IN | 指定针对某个列的多个可能值 |
注意:在某些版本中的 SQL中,操作符<>
可以写为!=
between and
Select * from emp where sal between 1500 and 3000;
In
Select * from emp where sal in (5000,3000,1500);
查询 SAL 列中等于 5000,3000,1500 的值。
like
Select * from emp where ename like 'M%';
表中 Ename 列中有 M 的值,M 为要查询内容中的模糊信息。
- % 表示多个字值,_ 下划线表示一个字符;
- M% : 为能配符,正则表达式,表示的意思为模糊查询信息为 M 开头的。
- %M% : 表示查询包含M的所有内容。
- %M_ : 表示查询以M在倒数第二位的所有内容。
SQL的ORDER BY
子句
ORDER BY
语句用于根据指定的列
对结果集进行排序
ORDER BY
语句默认按照升序进行排序
ASC
关键字代表升序排序DESC
关键字代表降序排列
例:
-- 对 users 表中的数据,按照 status 字段进行 升序 排列
SELECT * FROM users ORDER BY status ASC;
-- 对 users 表中的数据,按照 status 字段进行 降序 排列
SELECT * FROM users ORDER BY status DESC;
ORDER BY 子句-多重排序
对users表中的数据,先按照status字段进行降序排列,在按照username的字母数顺序进行升序排列
SELECT * FROM users ORDER BY status DESC, username ASC;
SQL的COUNT(*)
函数
COUNT()
函数用于返回查询结果的总数据条数,语法格式如下:SELECT COUNT(*)FROM 表名称
使用 AS 为列
设置别名
:
SELECT COUNT(*) AS 别名 FROM 表名称
例子:
SELECT COUNT(*) AS totle FROM users WHERE status = 0;

SQL的SELECT
语句**(查)**
SELECT
语句用于从表中查询数据。执行的结果被存储在一个结果表中(称为结果集)。语法格式如下:-- 从 FROM 指定的 【表】中,查询出【所有的数据】,* 表示【所有的】 SELECT * FROM 表名称; -- 从 FROM 指定的【表】中,查询出指定【列名称(字段)】的数据。 SELECT 列名称 FROM 表名称;
注意:
-
SQL语句中的关键字对大小写不敏感。SELECT等效于select,FROM等效于from。
-
每一行的结束,需要加上分号
;
,以防报错。 -
字符串需要使用引号
''
或""
-
【表】名,或【列】名可以使用 `` 符号括起来,也可以不使用
SELECT `id` FROM user;
例:
-- user,【表】名
SELECT * FROM user;
-- 可以查询多个列
SELECT id,username FROM user;
SQL的INSERT INTO
语句**(增)**
INSERT INTO
语句用于向数据表中插入新的数据行,语法格式如下:-- 语法解读:向指定的表中,插入如下几列数据,列的值通过 VALUES 指定 -- 注意:列和值要—对应,多个列和多个值之间,使用英文的逗号分隔 -- table_name,【表】名 INSERT INTO table_name(列1,列2,...)VALUES(值1,值2,....);
例:
-- 向 users 表中,插入新数据,username 的值为 tony stark,password 的值为 098123
INSERT INTO users (username, `password`) VALUES ('tony stark', '098123');
SQL的UPDATE
语句**(改)**
Update语句用于修改表中的数据,语法格式如下:
-- 1、语法解读 -- 2、用 SET 指定列对应的新值 -- 3、用 WHERE 指定更新的条件 UPDATE 表名称 SET 列名称 = 新值 WHERE 列名称 = 某值;
例:
-- 把 users 表中 id 为 12 的用户密码,更新为 ctrl,示例如下:
UPDATE users SET `password` = 'ctrl' WHERE id = 12;
-- 把 users 表中 id 为 5 的用户密码、用户状态分别更改为 a1b2c3 ,1
UPDATE users SET passowrd = 'a1b2c3', status = 1 WHERE id = 5;
SQL的DELETE
语句**(删)**
DELETE
语句用于删除表中的行,语法格式如下:-- 语法解读 -- 从指定的表中,根据 WHERE 条件,删除对应的数据行 DELETE FROM 表名称 WHERE 列名称 = 值;
在Express中操作MySQL
- 安装操作MySQL数据库的第三方模块(
mysql
) - 通过
mysql
模块连接到MySQL数据库 - 通过
mysql
模块执行SQL语句
安装与配置mysql模块
安装
mysql模块时托管于npm上的第三方模块,它提供了在Node.js项目中连接MySQL数据库的能力。
想要在项目中使用它,需要先运行如下命令,将mysql安装为项目的依赖包:
npm i mysql
配置mysql模块
在使用mysql模块操作MySQL数据库之前,必须先对mysql模块进行必要的配置,主要的配置步骤如下:
// 1.导入mysql模块
const mysql = require('mysql')
// 2.建立与 MySQL 数据库的连接
const db = mysql.createPool({
host:'127.0.0.1', // 数据库的IP地址
user:'root', // 登录数据库的账号
password:'admin123', // 登录数据库的密码
database:'mydata_1', // 指定要操作事个数据车
})
测试mysql模块能否正常工作
调用db.queryO函数,指定要执行的SQL语句,通过回调函数拿到执行的结果:
// 检测 mysql 模块能否正常工作
db.query('select 1', (err, res) => {
if (err) return console.log('错误' + err.message);
// 只要能打印出 [ RowDataPacket { '1': 1 } ] 的结果,就证明数据库连接正常
console.log(res);
});
使用mysql模块操作MySQL数据库
查询数据
查询users表中所有的数据:
const mysql = require('mysql')
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'admin123',
database: 'mydata_1'
})
// 使用 SQL 语法
db.query('SELECT * FROM users', (err, res) => {
if (err) return console.log(err.message)
console.log(res)
})
插入数据
向users表中插入数据:
const mysql = require('mysql')
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: 'admin123',
database: 'mydata_1'
})
// 插入数据
// 1.要插入到 users 表中的数据对象
const user = { username: 'kunkun', password: 'gege' }
// 2.待执行的 SQL 语句,其中英文 ? ,表示占位符
const sqlStr = 'INSERT INTO users (username, password) VALUE (?,?)'
// 3.使用数组的形式,依次为 ? 占位符指定具体的值
db.query(sqlStr, [user.username, user.password], (err, res) => {
if (err) return console.log('错误' + err.message)
// affectedRows 指执行 SQL 语句后受影响的行数
if (res.affectedRows === 1) console.log('插入成功')
})
插入数据的便捷方式:
向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应
,则可以通过如下方式快速插入数据:
// 1、数据对象
const user = { username: 'kunkun', password: 'gege' }
// 2、待执行的 SQL 语句,其中英文的 ? 表示占位符
const sqlStr = 'INSERT INTO users SET ?'
// 3、直接将数据对象当成占位符的值
db.query(sqlStr, user, (err, res) => {
if (err) return console.log('错误' + err.message)
if (res.affectedRows === 1) console.log('插入成功')
})
更新数据
// 1.要更新的数据对象
const user = { id: 7, username: 'abc', password: '1212'}
// 2.要执行的 SQL 语句
const sqlStr = 'UPDATE users SET username=?, password=? WHERE id=?'
// 3.调用 db.query() 执行 SQL 语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user.username, user.password, user.id], (err, res) => {
if (err) return console.log('错误' + err.message)
if (res.affectedRows === 1) console.log('更新成功')
})
更新数据的边界方式:
更新表数据时,如果数据对象的每个属性和数据表的字段一一对应
,则可以通过如下方式快速更新数据表数据:
// 1.要更新的数据对象
const user = { id: 19, username: 'abc', password: '1212' }
// 2.要执行的 SQL 语句,使用英文 ? 占位符
const sqlStr = 'UPDATE users SET ? WHERE id=?'
// 3.调用 db.query() 执行 SQL 语句的同时,使用数组依次为占位符指定具体的值
db.query(sqlStr, [user, user.id], (err, res) => {
if (err) return console.log('错误' + err.message)
if (res.affectedRows === 1) console.log('更新成功')
})
删除数据
在删除时,推荐根据id这样的唯一表示,来删除对应的数据:
// 1、要执行的 SQL 语句
const sqlStr = 'DELETE FROM users WHERE id = ?'
// 2、调用 db.query() 执行SQL语句的同时,为占位符指定具体的值
// 注意:如果 SQL 语句中有多个占位符,则需要使用数据为每个占位符指定内容的值。如果 SQL 语句中只有一个占位符,则可以省略数组
db.query(sqlStr, 20, (err,res)=>{
if (err) return console.log('错误' + err.message)
if (res.affectedRows === 1) console.log('删除成功')
})
标记删除:
使用 DELETE
语句,会真正的把数据从表中删除。为了保险起见,推荐使用标记删除的形式,来模拟删除的动作。
所谓的标记删除,就是在表中设置类似于status
这样的状态字段
,来标记当前这条数据是否被删除。
当用户执行了删除的动作是,我们并没有执行 DELETE
语句把数据删除,而是执行了UODATE
语句,将这条数据对应的status字段
标记为删除即可。
// 标记删除,使用 UPDATE 语句
const sqlStr = 'UPDATE users SET status=? WHERE id=?'
db.query(sqlStr, [1,3], (err,res)=>{
if (err) return console.log('错误' + err.message)
if (res.affectedRows === 1) console.log('删除成功')
})
对用户密码进行加密
为了保证密码的安全性,不建议在数据库中,以明文的形式保存用户密码,推荐对密码进行加密储存
可以使用 bcryptjs
包,对用户密码进行加密,其优点:
- 加密后的密码,无法被你想破解。
- 同一明文密码经过加密,得到的加密结果不相同,如:张三、李四的密码相同,但是经过加密后,得到的加密结果是不想同的。
使用步骤:
- 安装
- 导入
- hashSync()函数得到加密字符串
安装
在终端中,输入如下代码安装:
npm i bcryptjs
导入
const bcrypt = require('bcryptjs')
加密
使用
.hashSync(明文密码, salt 的轮数)
轮数越多,则生成的哈希值越复杂,也越难以被破解,通常建议轮数设置在 10 到 12 之间
const encryptPasswords = bcrypt.hashSync(userinfo.password, 10)
优化表单数据验证
表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容
在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。
单纯的使用if....else...
的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后续程序员把更多的精力放在核心业务逻辑的处理上,
-
安装
@hapi/joi
包,为表单中携带的每个数据项,定义验证规则:npm i @hapi/joi
-
安装
@escook/express-joi
中间件,来实现自动对表单数据进行验证的功能:npm i @escook/express-joi
-
新建用户信息验证规则模块:user.js,:
// 导入joi const joi = require('@hapi/joi') // string() 必须是字符串 // alphanum() 值只能是包含 a-z,A-Z,θ-9 的字符串 // min(length) 最小长度 // max(length) 最大长度 // required() 值是必填项,不能为undefined // pattern(正则表达式) 值必须符合正则表达式的规则 // 用户名的验证规则 const usernamej = joi.string().alphanum().min(1).max(10).required() // 密码的验证规则 const password = joi.string().pattern(/^[\s]{6,12)S/).required()
前后端的身份验证
Web开发模式
目前主流的Web开发模式有两种,分别是:
- 基于服务端渲染的传统Web开发模式
- 基于前后端分离的新型Web开发模式
服务端渲染
服务端渲染的概念
:服务器发送给客户端的HTML页面,是在服务器通过字符串的拼接,动态生成的。因此,客用户端不需要使用Ajax这样的技术额外请求页面的数据
优点:
前端耗时少
。因为服务器端负责动态生成HTML内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。有益于SEO
。SEO 是搜索引擎优化(Search Engine Optimization)的缩写。它是一种通过优化网站内容和结构,以提高网站在搜索引擎中的排名和可见性的技术和策略。因为服务端响应的是完整的HTML页面内容,所以爬虫更容易爬取获得信息,更有利于SEO
缺点:
占用服务端资源
。即服务器完成HTML页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力不利于前后端分离,开发效率低
。使用服务端渲染,则无法进行分工合作,尤其对前端复杂度高的项目,不利于项目高效开发。
前后端分离
前后端分离的概念
:前后端分离的开发模式,依赖于Ajax技术的广泛应用。简而言之,前后端分离的Web开发模式,就是后端只负责提供API接口,前端使用Ajax调用接口的开发模式。
优点:
开发体验好
。前端专注于UI页面的开发,后端专注于api的开发,且前端有更多的选择性。用户体验好
。Ajax技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。减轻了服务器端的渲染压力
。因为页面最终是在每个用户的浏览器中生成的。
缺点:
不利于SEO
。因为完整的HTML页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方
案:利用Vue、React等前端框架的SSR
(serversiderender)技术能够很好的解决SEO问题!)
如何选择Web开发模式
不谈业务场景而盲目选择使用何种开发模式都是要流氓
- 比如企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的SEO,则这时我们就需要使用服务器端染。
- 而类似后台管理项目,交互性比较强,不需要考虑SEO,那么就可以使用前后端分离的开发模式。
另外,具体使用何种开发模式并不是绝对的,为了同时兼顾了首页的渲染速度和前后端分离的开发效率,
一些网站采用了:首屏服务器端染+其他页面前后端分离的开发模式
身份认证
身份认证
,又称身份验证、鉴权,是指通过一定的手段,完成对用户身份的确认。
身份认证的目的,是为了确认当前所声称为某种身份的用户,确定是所声称的用户。如:你去找快递员取快递,你要真么证明这份快递是你的。
对于服务端泣染和前后端分离这两种开发模式来说,分别有着不同的身份认证方案:
服务端渲染
推荐使用Session认证机制
前后端分离
推荐使用JWT认证机制
Session认证机制
HTTP协议的无状态性
了解HTTP协议的无状态性是进一步学习Session认证机制的必要前提。
HTTP协议的无状态性
,指的是客户端的每次HTTP请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次HTTP请求的状态。
如何突破HTTP无状态的限制
对于超市来说,为了方便收银员在进行结算时给VIP用户打折,超市可以为每个VIP用户发放会员卡。
上面例子中的会员卡,就是身份认证的标识,在Web开发中的专业术语叫做Cookie
,这个Cookie
是储存在客户端的,每次访问服务器的时候,会从客户端寻找并校验Cookie
。
什么是Cookie
Cookie
是存储在用户浏览器中的一段不超过4KB的字符串。它由一个名称(Name)、一个值(Value)和其它几个用于控制Cookie有效期、安全性、使用范围的可选属性
组成。
不同域名下的Cookie各自独立,每当客户端发起请求时,会自动把当前域名下所有**未过期的Cookie
**一同发送到服务器。
Cookie的几大特性:
- 自动发送
- 域名独立
- 过期时限
- 4KB限制
Cookie在身份认证中的作用
客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的Cookie
,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的Cookie,通过请求头的形式发送给服务器,服务器即可验明客户端的身份。
请求头中的Cookie
Cookie不具有安全性
由于Cookie是存储在浏览器中的,而且浏览器也提供了读写Cookie
的API
,因此**Cookie
和容易被伪造**,不具有安全性。
因此,不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器。
注意:千万不要使用Cookie储存重要且隐私的数据!比如用户的身份信息、密码等。
提高身份认证的安全性
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以在收银机上进行刷卡认证。只有收银机确认存在的会员卡,才能被正常使用
会员卡:相当于Cookie验证
刷卡认证:相当于服务器验证
这种“会员卡+刷卡认证”的设计理念,就是Session认证机制的精髓。
Session的工作原理
Express中使用Session
安装express-session中间件
在express项目中,只需要安装express-session中间件,即可在项目中使用Session认证:
npm i express-session
配置express-session中间件
express-session
中间件安装成功后,需要铜鼓欧app.use()
来注册session
中间件:
// 1.导入 session 中间件
const session = require('express-session')
// 2.配置 session 中间件
app.use(session({
secret:'cxk aidalanqiu', // secret 属性的值可以是任意字符串,用来加密 session
resave: false, // 固定写法
saveUninitialized: true, // 固定写法
}))
向session中存数据
当express-session
中间件配置成功后,即可通过req.session
来访问和使用session
对象,从而存储用户的关键信息:
// 登录的 API 接口
app.post('/api/login',(req,res)=>{
res.session.user = req.body; // 创建 user 属性,并赋值
req.session.islogin = true; // 创建 islogin 属性,并赋值
})
从session中取数据
可以直接从req.session
对象上获取之前存储的数据:
app.get('/api/username', (req, res) => {
// 获取 session 信息
res.send({ username: req.session.user })
})
清空session
调用req.session.destory()
函数,即可清空服务器保存的session信息:
app.post('/api/logout', (req, res) => {
// 清空当前客户端对应的 session 信息
req.session.destory()
})
JWT认证机制
了解Session认证的局限性
在实际应用中,由于安全和隔离的考虑,前端和后端的代码通常会被部署在不同的服务器上。这样,前端代码就无法直接访问后端代码,因为它们不在同一个域名或 IP 地址下。
Session
认证机制需要配合Cookie
才能实现,由于Cookie
默认不支持跨域访问。所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域Session
认证
注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制
- 当前端需要跨域请求后端接口的时候,不推荐使用Session身份验证机制,推荐使用JWT认证机制。
什么是JWT
JWT(英文全称:JSON Web Token) 是目前最流行的跨域认证解决方案
JWT的工作原理
总结: 用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务端通过还原Token字符串的形式来认证用户的身份信息
JWT的组成部分
JWT通常头三部分组成,分别是Header
(头部)、Payload
(有效荷载)、Signature
(签名)
三者之间使用英文的".
"分割,格式如下:
Header.Payload.Signature
下面是JWT字符串的示例:
JWT的三个部分分别代表的含义:
Payload
:真正的用户信息,它是用户信息经过加密之后生成的字符串Header
、Signature
:是安全性相关的部分,只是为了保证Token
的安全性
JWT的使用方式
客户端收到服务器返回的JWT之后,通常会将它储存在localStorage
或sessionStorage
中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在HTTP请求头的Authorization字段中,格式如下:
Authorization: Bearer <token> // Bearer 为固定写法
Express中使用JWT
安装JWT相关的包
运行如下命令,安装两个JWT相关的包:
npm i jsonwebtoken express-jwt
两个包的作用:
jsonwebtoken
:用于生成JWT字符串express-jwt
:用于将JWT字符串解析还原成JSON对象
导入JWT相关的包
使用require()的函数,分别导入JWT相关的两个包:
注意:通过 require('express-jwt')
获取到的是一个对象{ UnauthorizedError: [Getter], expressjwt: [Function: expressjwt] }
,因此可以使用解构,将这个对象中的expressjwt函数
取出
// 1.导入用于生成 JWT 字符串的包
const jwt = require('jsonwebtoken')
// 2.导入用于将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包
const { expressjwt: expressJWT } = require('express-jwt')
定义secret秘钥
为了保证 JWT
字符串的安全性,防止 JWT
字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的secret秘钥
:
- 当生成
JWT
字符串的时候,需要使用secret秘钥对用户的信息进行加密,最终得到加密好的JWT
字符串 - 当把
JWT
字符串解还原成 JSON 对象的时候,需要只用secret秘钥进行解密
secret秘钥的本质就是一个字符串
const secretKey = 'cxkaidalanqiu'
登录成功后生成
JWT字符串
调用jsonwebtoken
包提供的sign()
方法,将用户的信息加密成JWT字符串,响应给客户端:
// 登录接口
app.post('/api/login', function (req, res) {
// 用户登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
res.send({
status: 200,
message: '登录成功!',
// 调用 jwt.sign() 生成 JWT 字符串,三个参数分别是:用户信息对象、加密秘钥、配置对象
token: jwt.sign({ username: 'caixukun' }, secretKey, { expiresIn: '30s' })
})
})
将JWT字符串还原
为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization
字段,将 Token 字符串发送到服务器进行身份验证。
此时,服务器可以通过express-jwt
这个中间件,自动将客户端发送过来的Token解析还原成JSON对象
// 使用 app.use() 来注册中间件 expressJWT()
// expressJWT({secret: secretKey, algorithms: ['HS256'] }) 就是用来解析 Token 的中间件
// .unless({path:[/^\/api\//] }) 用来指定那些接口不需要访问权限
app.use(expressJWT({secret: secretKey, algorithms: ['HS256'] }).unless({path:[/^\/api\//] }))
使用req.auth获取用户信息
当express-jwt
这个中间件配置成功后,就可以把解析出来的用户信息,挂载到req.auth
属性上面,即可在那些有权限的接口中,使用req.auth
对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.auth 获取用户信息,并使用 data 属性将用户信息发送给客户端
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: '未知的错误',
})
})
认证
注意:
- 当前端请求后端接口不存在跨域问题的时候,推荐使用Session身份认证机制
- 当前端需要跨域请求后端接口的时候,不推荐使用Session身份验证机制,推荐使用JWT认证机制。
什么是JWT
JWT(英文全称:JSON Web Token) 是目前最流行的跨域认证解决方案
JWT的工作原理
[外链图片转存中…(img-F1T9f52C-1730275368357)]
总结: 用户的信息通过Token字符串的形式,保存在客户端浏览器中,服务端通过还原Token字符串的形式来认证用户的身份信息
JWT的组成部分
JWT通常头三部分组成,分别是Header
(头部)、Payload
(有效荷载)、Signature
(签名)
三者之间使用英文的".
"分割,格式如下:
Header.Payload.Signature
下面是JWT字符串的示例:
JWT的三个部分分别代表的含义:
Payload
:真正的用户信息,它是用户信息经过加密之后生成的字符串Header
、Signature
:是安全性相关的部分,只是为了保证Token
的安全性
JWT的使用方式
客户端收到服务器返回的JWT之后,通常会将它储存在localStorage
或sessionStorage
中。
此后,客户端每次与服务器通信,都要带上这个JWT的字符串,从而进行身份认证。推荐的做法是把JWT放在HTTP请求头的Authorization字段中,格式如下:
Authorization: Bearer <token> // Bearer 为固定写法
Express中使用JWT
安装JWT相关的包
运行如下命令,安装两个JWT相关的包:
npm i jsonwebtoken express-jwt
两个包的作用:
jsonwebtoken
:用于生成JWT字符串express-jwt
:用于将JWT字符串解析还原成JSON对象
导入JWT相关的包
使用require()的函数,分别导入JWT相关的两个包:
注意:通过 require('express-jwt')
获取到的是一个对象{ UnauthorizedError: [Getter], expressjwt: [Function: expressjwt] }
,因此可以使用解构,将这个对象中的expressjwt函数
取出
// 1.导入用于生成 JWT 字符串的包
const jwt = require('jsonwebtoken')
// 2.导入用于将客户端发送过来的 JWT 字符串,解析还原成 JSON 对象的包
const { expressjwt: expressJWT } = require('express-jwt')
定义secret秘钥
为了保证 JWT
字符串的安全性,防止 JWT
字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的secret秘钥
:
- 当生成
JWT
字符串的时候,需要使用secret秘钥对用户的信息进行加密,最终得到加密好的JWT
字符串 - 当把
JWT
字符串解还原成 JSON 对象的时候,需要只用secret秘钥进行解密
secret秘钥的本质就是一个字符串
const secretKey = 'cxkaidalanqiu'
登录成功后生成
JWT字符串
调用jsonwebtoken
包提供的sign()
方法,将用户的信息加密成JWT字符串,响应给客户端:
// 登录接口
app.post('/api/login', function (req, res) {
// 用户登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
res.send({
status: 200,
message: '登录成功!',
// 调用 jwt.sign() 生成 JWT 字符串,三个参数分别是:用户信息对象、加密秘钥、配置对象
token: jwt.sign({ username: 'caixukun' }, secretKey, { expiresIn: '30s' })
})
})
将JWT字符串还原
为JSON对象
客户端每次在访问那些有权限接口的时候,都需要主动通过请求头中的 Authorization
字段,将 Token 字符串发送到服务器进行身份验证。
此时,服务器可以通过express-jwt
这个中间件,自动将客户端发送过来的Token解析还原成JSON对象
// 使用 app.use() 来注册中间件 expressJWT()
// expressJWT({secret: secretKey, algorithms: ['HS256'] }) 就是用来解析 Token 的中间件
// .unless({path:[/^\/api\//] }) 用来指定那些接口不需要访问权限
app.use(expressJWT({secret: secretKey, algorithms: ['HS256'] }).unless({path:[/^\/api\//] }))
使用req.auth获取用户信息
当express-jwt
这个中间件配置成功后,就可以把解析出来的用户信息,挂载到req.auth
属性上面,即可在那些有权限的接口中,使用req.auth
对象,来访问从 JWT 字符串中解析出来的用户信息了,示例代码如下:
// 这是一个有权限的 API 接口
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.auth 获取用户信息,并使用 data 属性将用户信息发送给客户端
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.auth // 要发送给客户端的用户信息
})
})
测试
[外链图片转存中…(img-JIZp475W-1730275368357)]
[外链图片转存中…(img-UceHSg8f-1730275368357)]
捕获解析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: '未知的错误',
})
})