Node学习笔记

文章目录

初识Node.js与内置模块

初识Node.js

什么是Node.js

  • Node.js是一种基于Chrome V8引擎JavaScript运行时环境,用于在服务器端运行JavaScript代码

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

  • 注意:Node.js中无法调用DOMBOM浏览器内置API

13 1const arr = [0, 1, 2, [3, 4, [5, 6, [7, 8]]]]2​3// 提取深度为默认值:14console.log(arr.flat()); // 输出:[0, 1, 2, 3, 4, [5, 6, [7, 8]]]5​6// 提取深度为默认值:37console.log(arr.flat(3)); // 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8]8​9// 提取深度为默认值:3010console.log(arr.flat(30)); // 输出:[0, 1, 2, 3, 4, 5, 6, 7, 8]11​12// 不会改变原数组的值13console.log(arr) // 输出:[0, 1, 2, [3, 4, [5, 6, [7, 8]]]]js

Node.js可以做什么

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

学习过程

  1. Node.js内置API模块(fs、path、http等)
  2. 第三方API模块(express、MySQL等)

区分LTS版本和Current版本的不同

  1. LTS为长期稳定版,对于追求稳定性的企业项目来说,推荐安装LTS版本的Node.js

  2. Current版本为尝鲜版,对于热衷于尝试新特性的用户来说,推荐安装Current版本的Node.js。但是,Current版本中可能存在隐藏的BUG或安全漏洞,因此不推荐在企业项目中国使用Current版本的Node.js

初学者安装LTS版本既可以了

如何在Node.js环境中执行JavaScript代码

在vscode中打开终端,输入:node Js文件位置

如:

image-20230921164009301

在终端输入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必选参数,字符串,表示以什么编码格式来读取文件,默认是utf8
  • callback必选参数,回调函数,文件读取完成后的回调函数
    • err
      读取文件成功时,值为 null,所以这时的err是没有message
      读取文件失败时,值为 错误对象
      当读取文件失败时,打印 err.message,会在终端输出失败的信息
    • dataStr
      读取文件成功时,值为 读取的文件
      读取文件失败时,值为 undefined
向指定的文件中写入内容

用法fs.writeFile(path, data [, options], callback(err))

  • path必选参数,字符串,表示文件的路径
  • data必选参数,表示要写入的内容
  • options可选参数,字符串,表示以什么编码格式来写入文件,默认是utf8
  • callback必选参数,回调函数,文件写入完成后的回调函数
    • err
      写入文件成功时,值为 null
      写入文件失败时,值为 错误对象
      当写入文件失败时,打印 err.message,会在终端输出失败的信息

注意

  1. 当输入的路径没有文件,则会创建一个文件,然后进行写入内容
    如:

    // 有一路径:a\b\c,但是 c 文件夹中没有 d.txt ,
    fs.writeFile('/a/b/c/d.txt','123',err()=>{})
    // 在执行写入代码时,因为没有 d.txt,所以先在 a\b\c 文件夹中创建了一个新的 d.txt 文件,后将内容写入这个新创建的文件中
    
  2. 新写入的内容会覆盖原来的内容

fs模块-路径动态拼接问题

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

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

如:

// 在 'JS' 文件中,有这样一个读取文件的函数,这个 'JS' 文件在  ‘C:\我的学习文件\Node学习\Node代码’
fs.readFile('./hellow.txt', 'utf8', (err, dataStr) => {})
// 则在本个 'JS' 文件所在的文件夹中开启的终端执行这个 'JS' 文件,会读取
// 'C:\我的学习文件\Node学习\Node代码\hellow.txt'   这个绝对路径的文件

image-20230923164220567

代码访问路径:C:\我的学习文件\Node学习\Node代码\hellow.txt

如何解决

  1. 直接使用需要读取的文件的完整路径,但是不利于维护
    (不建议使用的方法)

  2. 使用__dirname(前面时两个下划线),它代表文件所在的位置

    console.log(__dirname)
    // 在终端输出结果如下
    

    image-20230923171115713

    完整用法

    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
// 结果如下图:

image-20230923191433615

获取文件的扩展名

用法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端口可以被忽略

80
223
653
97
客户发起请求\nhttp://127.0.0.1
web server1
web server2
web server3
web server4

一个web服务中包含了多个文件(类型可相同),如:

  1. HTML文件
  2. CSS文件
  3. JavaScript文件
  4. 图片文件
  5. 数据库文件
  6. 配置文件
  7. 服务器脚本文件

创建web服务器

分为4步:

  1. 导入http模块

    const http = require('http')
    
  2. 创建web服务器示例

    调用createServer()方法,即可快速创建一个web服务器示例

    const server = http.creatServer()
    
  3. 为服务器示例绑定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('我爱吃坤蛋')
    })
    
  4. 启动服务器

使用服务器示例listen(端口号, 服务器启动成功后调用的回调函数)方法

server.listen(110, () => {
   console.log("http server running at 'http://127.0.0.1:110'")
})
   # 

模块化

模块化的基础

什么是模块化

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

  • 简单来说,就是遵守固定的规则,把一个大文件拆成独立并互相依赖的多个小模块
    优点:
    1. 提高了代码的复用性
    2. 提高了代码的可维护性
    3. 可以实现按需加载

模块化规范

模块化规范就是对代码进行模块化的拆分与结合时,需要遵守的那些规则

如:

  • 使用什么样的语法格式来引用模块
  • 在模块中使用什么样的语法规范向外暴露程艳

好处:大家都遵守同样的模块化规范写代码,降低了沟通的成本

Node.js中的模块

Node.js中模块的分类

Node.js中根据模块来源的不同,将模块分为了3大类,分别是:

  1. 内置模块:由Node.js官方提供的,例如:fspathhttp
  2. 自定义模块:用户创建的每个js文件,都是自定义模块
  3. 第三方模块:由第三方开发出来的模块,并非官方提供的内置模块,也不是用户创建的自定义模块,使用前需要先下载

加载模块

使用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对象,它里面存储了和当前模块有关的信息

如:

image-20230926161337166

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)

终端输出结果如下

image-20230926163450535

注意:由于module.exports单词写起来比较复杂,Node提供了exports对象,默认情况下,exportsmodule.exports指向同一个对象。最终共享的结果,但是读取到的对象还是以module.exports指向的对象为准

// exports 和 module.exports 指向同一个对象
console.log(exports === module.exports)
// true
exports和module.exports的使用误区

重点:使用require()时,得到的永远是module.exports指向的对象
他们两个本来都是指向一个对象,但是,对其中一个进行了对象重赋值(即exports = {})后,两个就不指向同一个对象

  1. exports.username = 'cxk'
    module.exports = {
       age: '2.5 years'
    }
    // require() 后,对象为:{age:'2.5 years'}
    
  2. module.exprots.username = 'jntm'
    exports = {
       gender: '未知'
    }
    // require() 后,对象为:{username:'jntm'}
    
  3. exports.name = 'cxk'
    module.exports.like = 'basketball'
    // require() 后,对象为:{name:'cxk',like:'basketball'}
    
  4. exports  = {
       name:'cxk'
    }
    module.exports = exports
    module.exports.age = '2.5'
    // require() 后,对象为:{name:'cxk',age:'2.5'}
    

Node.js中的模块化规范

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

CommonJS规定:

  1. 每个模块内部,module变量代表当前模块
  2. module变量是一个对象,他的exports属性(即module.exports)是对外的接口
  3. 加载某个模块,其实是加载该模块的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

注意

  1. 上述命令只能在英文的目录下成功运行,所以,项目文件夹的名称一定要使用英文命名,不要使用中文,不能出现空格
  2. 运行npm install命令安装包的时候,npm包管理工具会自动把报的名称和版本号记录到package.json
  3. 对于目前来说,安装第一个包时,会自动创建一个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

规范的包结构

  1. 每个包独占一个文件夹
  2. 包的顶级目录下必须包含package.json这个包管理配置文件
    image-20230930160845343
  3. package.json中必须包含name、version、main这三个属性,分别代表包的名字、版本号、入口文件。
    入口文件require('moment')导入时,以什么文件进行导入的

发布包

登录:在需要发布的包的父文件夹中,打开终端,输入npm login进行登录

发布:在需要发布的包的文件夹中,打开终端,输入npm publish

注意:需要发布的包不能和其他人已经发布的包的名字雷同

删除包

在终端输入npm unpublish 包名 --force,即可删除72小时以内发布的包

被删除的包在24小时内不允许重新发布

模块的加载机制

优先从缓存中加载

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

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

内置模块加载机制

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

自定义模块的加载机制

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

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

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

第三方的加载机制

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

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

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

  1. C:\Users\itheima\project\**node_modules\**tools
  2. C:\Users\itheima\**node_modules\**tools
  3. C:\Users\**node_modules\**tools
  4. C:\**node_modules\**tools

目录作为模块

当把**包的目录(文件夹名)**作为标识符,传递给require()进行加载的时候,有三种加载方式:

  1. 被加载的目录下查找一个叫做package.json的文件,并寻找main属性作为require()加载的入口
  2. 如果目录里面没有package.json文件,或者main入口不存在或无法解析,则Node.js将会试图加载目录下的index.js文件
  3. 如果以上两步都失败了,则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文件夹同级下,向内查询,如:
image-20231212001851681

需要托管pages文件夹,则需要从路径”综合案例“文件夹向下寻找:

app.use(express.static('综合案例/session案例/pages'))
托管多个资源目录

如果要托管多个静态资源目录,请多次调用express.static()函数:

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

访问静态文件时,express.static()函数会根据目录的添加顺序来查找所需文件

如:(获取public文件夹中的文件的优先级高于其他文件夹)

  • 如果publicfiles中都有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)上,推荐将路由抽离为单独的模块

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

  1. 创建路由对应的 .js 文件
  2. 调用express.Router()函数创建路由对象
  3. 路由对象上挂载具体的路由
  4. 使用module.exports向外共享路由对象
  5. 使用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的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理

请求
响应
客户端
中间件1
中间件2
中间件N
处理完毕,\n响应这次请求

express中间件的格式

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

app.get('/', function(req, res, next) { next() } )

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

其中的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() } )

中间件的作用

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

定义多个全局中间件

  1. 方法一

    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
    // 执行结果都相同,如下图:
    

    image-20231004171336232

  2. 方法二

    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/ 时,执行结果如下图:
    image-20231004180922242
  • 当访问 http://localhost:133/server 时,执行结果如下图:
    image-20231004181159471
加入多个局部中间件

有2中方法:

  1. app.get('/', mf1, mf2, (req, res) => {} )
    
  2. app.get('/', [mf1, mf2], (req, res) =>{} )
    

中间件的使用注意事项

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

中间件的分类

为了方便大家理解和记忆中间件的使用,express官方把常见的中间件用法,分成了5大类,分别是:

  1. 应用级别的中间件
  2. 路由级别的中间件
  3. 错误级别的中间件
  4. express内置的中间件
  5. 第三方的中间件
应用级别的中间件

通过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个常用的中间件:

  1. express.static:快速托管静态资源的内置中间件
  2. express.json解析JSON格式的请求体数据(有兼容性,尽在4.16.0+版本中可用)
  3. 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官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件。

使用步骤

  1. 运行 npm install 中间件名 安装中间件
  2. 运行require()导入中间件
  3. 调用app.use()注册并使用中间件

自定义中间件

手动模拟一个类似于express.urlencoded这样的中间件,来解析POST提交到服务器器的表单数据

实现步骤

  1. 定义中间件
  2. 监听req的data事件
    获取客户端发送到服务器的数据,如果数据量比较大无法一次性发送完毕,则客户端会把数据进行分割后,分批发送到服务器,所以data时间内可能会触发多次,每触发一次data事件时,获取到数据只是完整数据的一部分,需要手动对接收到的数据进行拼接
  3. 监听req的end事件
    当请求体数据接收完毕之后,会自动触发req的end事件,所以可以在end事件中,拿到并处理完整的请求体的数据
  4. 使用querystring模块解析请求体数据
    Node.js内置了一个querystring模块,专门用来处理查询字符串,通过这个模块提供的parse()函数,可以轻松把查询的字符串解析成对象的格式
  5. 将解析出来的数据对象挂载为req.body
    上下游的中间件路由之间,共享一份reqres,因此,我么可以将解析出来的数据,挂载为req的自定义属性req.body,供下游使用
  6. 将自定义中间件封装为模块

使用Express是写接口

实现步骤:

  1. 创建基本的服务器
  2. 创建API路由模块
  3. 编写GET接口
  4. 编写POST接口
  5. 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跨域跨域资源共享

接口的跨域问题

上面编写的接口与,在一个很严重的问题:不支持跨域请求

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

  1. CORS(主流的解决方案,推荐使用)
  2. JSONP(有缺陷的未解决方案,只支持GET请求)
使用CORS中间件解决跨域问题

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

使用步骤分为如下3步:

  1. 运行 npm i cors安装中间件
  2. 使用const cors = require('cors')导入中间件
  3. 在路由之前调用app.use(cors())注册中间件
什么是CORS

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

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

image-20231208220656188

CORS的注意事项
  1. ORS主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了CORS的接口。
  2. 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 请求。

如果客户端希望通过 PUTDELETE 等方式请求服务器的资源,则需要在服务器端,通过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的请求分为两大类,分别是:

  1. 简单请求
    同时满足以下两大条件的请求,就属于简单请求:
    • 请求方式: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)
  2. 预检请求
    只要符合以下任何一个条件的请求,都需要进行预检请求:
    • 请求方式为 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大部分组成。

image-20231209125530218

①整个Excel叫做工作薄
②users和books是工作表
③users工作表中有3行数据
④每行数据由6列信息组成
⑤每列信息都有对应的数据类型

传统型数据库的数据组织结构

在传统型数据库中,数据的组织结构分为数据库(database)数据表(table)数据行(row)、**字段(field)**这4大部分组成。

数据库类似于Excel的工作薄
数据表类似于Excel的工作表
数据行类似于Excel的每一行数据
字段类似于 Excel 的
⑤每个字段都有对应的数据类型

实际开发中库、表、行、字段的关系
  1. 在实际项目开发中,一般情况下,每个项目都对应独立的数据库
  2. 不同的数据,要存储到数据库的不同表中,例如:用户数据存储到 users 表中,图书数据存储到 books表中。
  3. 每个表中具体存储哪些信息,由字段来决定,例如:我们可以为 users 表设计 id、user、name、password 这3个字段。
  4. 表中的行,代表每一条具体的数据。

安装并配置MySQL

了解需要安装哪些MySQL相关的软件

工具

Navicat可视化的MySQL管理工具

终端管理MySQL

  1. 默认情况下,在终端是无法直接执行mysql命令的,会无法识别,这时需要配置环境变量
  • 接着右键点击我的电脑img
  • 再点击img
  • 点击高级系统设置img
  • 点击环境变量img
  • 在第一步中选择Path,选择编辑
    将MySQL Server 8.0文件夹下的bin文件夹添加到里面
    我的是:C:\Program Files\MySQL\MySQL Server 8.0\bin
  1. 在终端中输入

    mysql -u用户名 -p密码
    
    # 如:
    mysql -uroot -padmin123
    

    其中,root为用户名,admin123为密码

  2. 终端中查看用户信息

    # 执行SQL查询用户信息
    select host,user,plugin,authentication_string from mysql.user;
    
  3. 如果在后面,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;

    image-20231211110641013

MySQL的基本使用

使用 Navicat 连接数据库

  1. 点击连接
  2. 选择MySQL数据库

image-20231210155941302

完成以上的步骤后,出现新建连接:

image-20231210160338737
  • 连接名:随便取
  • 用户名:使用下载MySQL数据库时创建的用户名(默认为root)
  • 密码:创建用户名时的密码

其他选项没有出现问题就可以不用更改

创建数据库

  1. 在数据库列表中,右键选择新建组

image-20231210161335286

  1. 然后右键新建的组,然后新建连接,选择MySQL数据库:[连接数据库](#使用 Navicat 连接数据库)

image-20231210161542597

  1. 双击打开连接
image-20231210161840445
  1. 右键连接,创建数据库

    • 数据库名:不要使用中文,也不要有空格
    • 字符集:选择utf8mb4
    • 排列规则:可以不填写
    image-20231210162124350

创建数据表

基本操作
  1. 打开创建的数据库,右键其中的“表”,选择新建表
  2. 设计表的字段
    image-20231210163127814
  3. Ctrl+S保存,并给表命名
配置表的字段

image-20231210172203538

在这里,根据字段的类型,可以配置不同的东西

同样可以在“索引”进行一些配置

image-20231210172449250

  1. 输入索引名:
    image-20231210173104145

  2. 选择要添加的字段(可多选),并选择排序顺序(无所谓,二选一
    image-20231210173255958

  3. 添加索引类型:

    1. Normal(普通索引):

      • 普通索引是最常见的索引类型。
      • 它用于加速查询操作,提高查询性能
      • 允许重复值,可以在一个字段上创建多个普通索引。
    2. Unique(唯一索引):

      • 唯一索引要求索引列的值是唯一的,不允许重复值
      • 它用于保证数据的唯一性约束。
      • 唯一索引可以加速唯一性检查和查找操作。
    3. Spatial(空间索引):

      • 空间索引用于处理空间数据,如地理位置信息、二维图形等。
      • 它支持空间范围查询和最近邻查询等操作。
      • 空间索引使用特定的空间算法来加速空间数据的检索。
    4. Fulltext(全文索引):

      • 全文索引用于支持全文搜索功能。
      • 它可以在文本数据中快速搜索关键词,支持模糊搜索、分词和关键词匹配等操作。
      • 全文索引适用于文本处理和搜索引擎等应用场景。

image-20231210173520698

  1. 添加索引方法:

    1. B树索引(B-Tree Index):
    • B树索引是一种常见的平衡树结构索引,适用于范围查询和精确查找
    • B树索引适用于传统的关系型数据库系统,如MySQL、Oracle等。
    • B树索引的特点是支持高效的插入和删除操作,能够自动平衡和调整树结构。
    1. 哈希索引(Hash Index):
    • 哈希索引使用哈希表的结构来快速查找数据,适用于等值查找
    • 哈希索引通常用于内存数据库或具有特定查询模式的场景。
    • 哈希索引的特点是查找速度快,但不支持范围查询和排序操作。

    image-20231210173700999

使用SQL管理数据库

什么是SQL

SQL(英文全称:StructuredQueryLanguage)是结构化查询语言,专门用来访问和处理数据库的编程语言。能够让我们以编程的形式,操作数据库里面的数据

三个关键点:

  1. SQL是一门数据库编程语言
  2. 使用SQL语言编写出来的代码,叫做SQL语句
  3. SQL语言只能在关系型数据库中使用(例如MySQL、Oracle、SQLServer)。非关系型数据库(例如Mongodb)不支持SQL语言

SQL能做什么

  • 从数据库中查询数据
  • 向数据库中插入新的数据
  • 更新数据库中的数据
  • 从数据库删除数据
  • 可以创建新数据库
  • 可在数据库中创建新表
  • 可在数据库中创建存储过程、视图

SQL的学习目标

重点掌握如何使用SQL从数据表中:

  • 查询数据(select)
  • 插入数据(insert into)
  • 更新数据(update)
  • 删除数据(delete)

额外需要掌握的4种SQL语法:

  • andornot运算符
  • 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子句用于限定选择的标准,在SELECTUPDATEDELETE语句中,亦可使用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;
image-20231211084538718

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

  1. 安装操作MySQL数据库的第三方模块(mysql)
  2. 通过mysql模块连接到MySQL数据库
  3. 通过mysql模块执行SQL语句

image-20231211092041954

安装与配置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 包,对用户密码进行加密,其优点:

  • 加密后的密码,无法被你想破解。
  • 同一明文密码经过加密,得到的加密结果不相同,如:张三、李四的密码相同,但是经过加密后,得到的加密结果是不想同的。

使用步骤:

  1. 安装
  2. 导入
  3. hashSync()函数得到加密字符串
安装

在终端中,输入如下代码安装:

npm i bcryptjs
导入
const bcrypt = require('bcryptjs')
加密

使用.hashSync(明文密码, salt 的轮数)

轮数越多,则生成的哈希值越复杂,也越难以被破解,通常建议轮数设置在 10 到 12 之间

const encryptPasswords = bcrypt.hashSync(userinfo.password, 10)

优化表单数据验证

表单验证的原则:前端验证为辅,后端验证为主,后端永远不要相信前端提交过来的任何内容

在实际开发中,前后端都需要对表单的数据进行合法性的验证,而且,后端做为数据合法性验证的最后一个关口,在拦截非法数据方面,起到了至关重要的作用。

单纯的使用if....else...的形式对数据合法性进行验证,效率低下、出错率高、维护性差。因此,推荐使用第三方数据验证模块,来降低出错率、提高验证的效率与可维护性,让后续程序员把更多的精力放在核心业务逻辑的处理上,

  1. 安装@hapi/joi包,为表单中携带的每个数据项,定义验证规则:

    npm i @hapi/joi
    
  2. 安装@escook/express-joi中间件,来实现自动对表单数据进行验证的功能:

    npm i @escook/express-joi
    
  3. 新建用户信息验证规则模块: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开发模式有两种,分别是:

  1. 基于服务端渲染的传统Web开发模式
  2. 基于前后端分离的新型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用户发放会员卡。

image-20231211212326657

上面例子中的会员卡,就是身份认证的标识,在Web开发中的专业术语叫做Cookie,这个Cookie储存在客户端的,每次访问服务器的时候,会从客户端寻找并校验Cookie

什么是Cookie

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

image-20231211213140700

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

Cookie的几大特性:

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

Cookie在身份认证中的作用

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

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

image-20231211213840510

请求头中的Cookie

image-20231211214347636

Cookie不具有安全性

由于Cookie是存储在浏览器中的,而且浏览器也提供了读写CookieAPI,因此**Cookie和容易被伪造**,不具有安全性。

因此,不建议服务器将重要的隐私数据,通过Cookie的形式发送给浏览器。

image-20231211215119027

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

提高身份认证的安全性

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

image-20231211215710295

会员卡:相当于Cookie验证

刷卡认证:相当于服务器验证

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

Session的工作原理

image-20231211221952200

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的工作原理

image-20231212212553431

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

JWT的组成部分

JWT通常头三部分组成,分别是Header(头部)、Payload(有效荷载)、Signature(签名)

三者之间使用英文的"."分割,格式如下:

Header.Payload.Signature

下面是JWT字符串的示例:

JWT的三个部分分别代表的含义

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

JWT的使用方式

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

此后,客户端每次与服务器通信,都要带上这个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 // 要发送给客户端的用户信息
  })
})

测试

image-20231218153653231

image-20231218153529520

捕获解析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真正的用户信息,它是用户信息经过加密之后生成的字符串
  • HeaderSignature:是安全性相关的部分,只是为了保证Token的安全性

JWT的使用方式

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

此后,客户端每次与服务器通信,都要带上这个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: '未知的错误',
  })
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值