• Node.js:万字总结黑马教程,学懂node.js看这一篇就够了


    一、初识 Nodejs

    Node.js 中文网

    Node.js® is a JavaScript runtime built on Chrome’s V8 JavaScript engine
    Node.js® 是一个基于 Chrome V8 引擎 的 JavaScript 运行时环境

    • 基于 Express 框架 (opens new window),可以快速构建 Web 应用
    • 基于 Electron 框架 (opens new window),可以构建跨平台的桌面应用
    • 基于 restify 框架 (opens new window),可以快速构建 API 接口项目
    • 读写和操作数据库、创建实用的命令行工具辅助前端开发、etc…

    二、fs 文件系统模块

    2.1 fs.readFile 读取文件

    语法:

    fs.readFile(path[, options], callback)
    
    • 1
    • path:文件路径
    • options:配置选项,若是字符串则指定编码格式
      1. encoding:编码格式(utf-8)
      2. flag:打开方式
    • callback:回调函数
      1. err:错误信息
      2. data:读取的数据,如果未指定编码格式则返回一个 Buffer

    2.2 fs.writeFile 写入文件

    语法:

    fs.writeFile(file, data[, options], callback)
    
    • 1
    • file:文件路径
    • data:写入内容
    • options:配置选项,包含 encoding, mode, flag;若是字符串则指定编码格式
    • callback:回调函数

    2.3 读写文件案例

    原数据:

    在这里插入图片描述

    目标处理结果:

    在这里插入图片描述

    //读取文件
    fs.readFile('./1.txt', 'utf-8', (err, dataStr) => {
    	//如果读取失败,则返回err,默认值为null,故可直接用于if判断
       if (err) {
       	//直接return,不再输出后续代码
           return console.log(err)
       }
       //dataStr为成功读取到的数据
       console.log('读取成功' + dataStr)
       
    //处理数据    
       //对dataStr做空格换行分隔,赋到Old数组上,并创建空的New数组
       const arrOld = dataStr.split('\r\n') 
       const arrNew = []
       
       //对Old数组的每一项,将'='替换为':',并push到新数组中
       arrOld.forEach(item => {
           arrNew.push(item.replace('=', ':'))
       })
       
       //以回车分隔将数组串联成一字符串
       const newStr= arrNew.join('\r\n')
       // console.log(typeof(newStr)) 可以看到输出类型为字符串
       // console.log(newStr)          输出串联后的字符串
       
    //写入文件
       fs.writeFile('./1.txt',newStr , (err) => {
           //写入失败返回的err默认为null,因此可作为判断条件
           if (err) {
               //err.message是错误信息
               return console.log(err.message);
           }
           console.log('写入成功');
       });
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    2.4 路径动态拼接问题 __dirname

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

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

    • 解决方案:在使用 fs 模块操作文件时,直接提供完整的路径,从而防止路径动态拼接的问题
      __dirname 获取文件所处的绝对路径

    fs.readFile(__dirname + '/files/1.txt', 'utf8', function(err, data) {
      ...
    })
    
    • 1
    • 2
    • 3
    • 原理:无论在哪个文件位置执行,绑定的路径都以读取的js文件位置为基准,在这个位置的基础上去访问它的上下级文件自然是对的

    三、path 路径模块

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

    3.1 路径拼接 path.join()

     path.join('','')
    
    • 1
    const path = require('path')
    const fs = require('fs')
    
    // 注意 ../ 会抵消前面的路径
    // ./ 会被忽略
    const pathStr = path.join('/a', '/b/c', '../../', './d', 'e')
    console.log(pathStr) 		// \a\d\e
    
    fs.readFile(path.join(__dirname, './files/1.txt'), 'utf8', function (err, dataStr) {
       if (err) {
       		return console.log(err.message)
       }
    	console.log(dataStr)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.2 获取路径中文件名 path.basename()

    使用 path.basename() 方法,可以获取路径中的最后一部分,常通过该方法获取路径中的文件名

    path.basename(path[, ext])
    
    • 1
    • path: 文件路径
    • ext: 文件扩展名
    const path = require('path')
    
    // 定义文件的存放路径
    const fpath = '/a/b/c/index.html'
    
    const fullName = path.basename(fpath)
    console.log(fullName) 				// index.html
    
    const nameWithoutExt = path.basename(fpath, '.html')
    console.log(nameWithoutExt) // index
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3.3 获取路径中文件扩展名 path.extname()

    const path = require('path')
    
    const fpath = '/a/b/c/index.html'
    
    const fext = path.extname(fpath)
    console.log(fext)								 // .html
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    四、http 模块

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

    4.1 创建基本 Web 服务器

    const http = require('http')
    
    // 创建 web 服务器实例
    const server = http.createServer()
    
    // 为服务器实例绑定 request 事件,监听客户端的请求
    server.on('request', function (req, res) {
       const url = req.url
       const method = req.method
       const str = `Your request url is ${url}, and request method is ${method}`
       console.log(str)
    
       // 设置 Content-Type 响应头,解决中文乱码的问题
       res.setHeader('Content-Type', 'text/html; charset=utf-8')
       // 向客户端响应内容
       res.end(str)
    })
    
    //注:要是报错可能是端口被mysql或其他软件占用,改个端口即可
    server.listen(8080, function () {
       console.log('server running at http://127.0.0.1:8080')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    4.2 实现简单路由效果

    const http = require('http')
    const server = http.createServer()
    
    server.on('request', (req, res) => {
       const url = req.url
       // 设置默认的响应内容为 404 Not found
       let content = '

    404 Not found!

    '
    // 判断用户请求的是否为 / 或 /index.html 首页 if (url === '/' || url === '/index.html') { content = '

    首页

    '
    } // 判断用户请求的是否为 /about.html 关于页面 else if (url === '/about.html') { content = '

    关于页面

    '
    } res.setHeader('Content-Type', 'text/html; charset=utf-8') res.end(content) }) server.listen(80, () => { console.log('server running at http://127.0.0.1') })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    五、模块化

    5.1 模块化概念

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

    模块化作用:

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

    模块化规范是对代码进行模块化拆分和组合时需要遵守的规则,如使用何种语法格式引用模块和向外暴露成员

    5.2 Node.js 中模块的分类

    • 内置模块
    const fs = require('fs')
    
    • 1
    • 用户自定义模块
    const custon = require('./custom')
    
    • 1
    • 第三方模块
    const moment = require('monent')
    
    • 1

    5.3 Node.js 中的模块作用域

    模块作用域:和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,无法被外界访问,这种模块级别的访问限制叫做模块作用域
    模块作用域作用:防止全局变量污染

    5.4 module对象 / exports对象

    • 自定义模块中都有一个 module 对象,存储了和当前模块有关的信息
    • 在自定义模块中,可以使用 module.exports 对象,将模块内的成员共享出去,供外界使用
    • 导入自定义模块时,得到的就是 module.exports 指向的对象,且默认为空对象,需要向对象进行挂载
    //自定义模块.js
    const age = 20
    //向module.exports对象上挂载 属性 / 方法 / 变量 
    module.exports.username = 'zs'
    module.exports.sayHi = function(){
        console.log('hi')
    }
    module.exports.age = age
    
    //使用模块.js
    const m = require('./自定义模块')
    console.log(m)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    输出:

    在这里插入图片描述

    • 默认情况下,exports 和 module.exports 指向同一个对象。
    • 最终共享的结果,以 module.exports 指向的对象为准
    //自定义模块.js
    const age = 20
    //向exports对象上挂载 属性 / 方法 / 变量 
    exports.username = 'zs'
    exports.sayHi = function(){
       console.log('hi')
    }
    module.exports.age = age
    
    //以module.exports最终指向为准(相当于开辟了一个新对象)
    module.exports = {
       nickName: '做一只猫',
       sayHello(){
           console.log('hello')
       }
    }
    
    //即便这时候再改exports指向不影响
    exports = {
       nickName: '做一只猫',
       sayHello(){
           console.log('hello')
       }
    }
    
    //使用模块.js
    const m = require('./自定义模块')
    console.log(m)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述

    5.5 CommonJS 模块化规范

    • 每个模块内部,module 变量代表当前模块
    • module 变量是一个对象,module.exports 是对外的接口
    • 加载某个模块即加载该模块的 module.exports 属性

    5.6 模块加载机制

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

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

    自定义模块加载

    • 加载自定义模块时,路径要以 ./ 或 …/ 开头,否则会作为内置模块或第三方模块加载(会报错)

    • 导入自定义模块时,若省略文件扩展名,则 Node.js 会按顺序尝试加载文件:

    1. 按确切的文件名加载
    2. 补全 .js 扩展名加载
    3. 补全 .json 扩展名加载
    4. 补全 .node 扩展名加载
    5. 报错

    第三方模块加载

    • 若导入第三方模块, Node.js 会从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块
    • 如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录
    • 例如,假设在 C:\Users\bruce\project\foo.js 文件里调用了 require(‘tools’),则 Node.js 会按以下顺序查找:
    1. C:\Users\bruce\project\node_modules\tools
    2. C:\Users\bruce\node_modules\tools
    3. C:\Users\node_modules\tools
    4. C:\node_modules\tools

    目录作为模块加载
    当把目录作为模块标识符进行加载的时候,有三种加载方式:

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

    例:
    1 新建test文件夹,在文件夹中放 a.js , index.js 和 package.json
    在这里插入图片描述

    //a.js中
    console.log('调用了a.js')
    
    //index.js中
    console.log('调用了index.js')
    
    //package.json中
    {
        "main": "./a.js"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3 新建1.js 并调用

    //try.js
    require('./test')
    
    • 1
    • 2

    在这里插入图片描述
    4 删去package.json 再次调用1.js
    在这里插入图片描述
    5 删去index.js 再次调用1.js
    在这里插入图片描述

    六、包

    6.1 包的定义

    • Node.js 中的第三方模块又叫做包
    • 不同于 Node.js 中的内置模块与自定义模块,包是由第三方个人或团队开发出来的,免费供所有人使用
    • 由于 Node.js 的内置模块仅提供了一些底层的 API,导致在基于内置模块进行项目开发的时,效率很低
    • 包是基于内置模块封装出来的,提供了更高级、更方便的 API,极大的提高了开发效率
    • 包和内置模块之间的关系,类似于 jQuery 和 浏览器内置 API 之间的关系

    6.2 包的使用

    • 包查找地址: https://www.npmjs.com/
    • 包下载地址:https://registry.npmjs.org/
    • 用npm下载包,下载node的同时也将npm下载了下来,可以在终端中用npm -v查看npm的版本
    • 可在官网看对应包的使用说明
    //在终端中(vs终端也可)@版本号可不加
    npm install 完整的包名@版本号
    //简写:
    npm i 完整的包名
    
    //例:用moment实例化时间
    //终端中
    npm i moment
    //代码区中
    const moment = require('moment')
    const dt = moment().format('YYYY-MM-DD HH:mm:ss')
    console.log(dt)
    //这个包停止维护了,但还可以使用
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    6.3 包管理配置文件

    作用: 团队开发中第三方的体积过大,因为联网即可下载第三方包,所以不需要将包也上传到项目中,而只用上传核心文件

    创建packge.json

    • npm init -y
    • 命令只能在英文且无空格的目录下运行
    • 运行npm install命令安装包时,npm包管理工具会自动将包的名称和版本号,记录到package.json 的dependencies节点中
    "dependencies": {
        "moment": "^2.29.4"
    }
    
    • 1
    • 2
    • 3

    一次性安装所有需要的包

    npm install		//能安装dependencies节点中所有的包
    
    • 1

    卸载包

    npm uninstall 要卸载的包名
    
    • 1

    deDependencies节点

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

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

    • 可使用如下的命令,将包记录到 devDependencies 节点中:

    npm i 包名 -D
    //完整写法如下
    npm install 包名 --save-dev
    
    • 1
    • 2
    • 3
    • 可以到官网看install安装说明,有–save-dev的就可以用这个方法丢devDependencies 节点里

    解决下载速度慢的问题

    为了更方便的切换下包的镜像源,我们可以安装 nrm 这个小工具,利用 nrm 提供的终端命令,可以快速查看和切换下包的镜像源

    //将nrm安装为全局可用的工具
    npm i nrm -g
    //查看所有可用的镜像源
    nrm ls
    //将下包的镜像源切换为taobao 镜像
    nrm use taobao
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.4 包的分类

    1. 项目包
    那些被安装到项目的 node_modules 目录中的包,都是项目包。

    项目包又分为两类,分别是:

    • 开发依赖包(被记录到 devDependencies 节点中的包,只在开发期间会用到)
    • 核心依赖包(被记录到 dependencies 节点中的包,在开发期间和项目上线之后都会用到)
    npm i 包名 -D 	//开发依赖包(会被记录到devDependencies节点下)
    npm i 包名			//核心依赖包(会被记录到dependencies节点下)
    
    • 1
    • 2

    2.全局包

    • 在执行 npm install 命令时,如果提供了 -g 参数,则会把包安装为全局包
    • 全局包会被安装到 C:\Users\用户目录\AppData\Roaming\npm\node_modules 目录下(新版本在安装目录下 或者你之前安装时配的路径中)
    npm i 包名 -g					//全局安装指定的包
    npm uninstall 包名 -g		//卸载全局安装的包
    
    • 1
    • 2
    • 注意:
      只有工具性质的包,才有全局安装的必要性,因为它们提供了好用的终端命令
      判断某个包是否需要全局安装后才能使用,可参考官方提供的使用说明

    3.i5ting_toc
    i5ting_toc 是一个可以把 md 文档转为 html 页面的小工具,使用步骤如下:

    //将 i5ting_toc 安装为全局包
    npm install -g i5ting_toc
    //调用 i5ting_toc 实现md转html的功能
    i5ting_toc -f 要转换的md文件路径 -o
    
    • 1
    • 2
    • 3
    • 4

    6.5 规范的包结构

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

    • 包必须以单独的目录而存在
    • 包的顶级目录下要必须包含 package.json 这个包管理配置文件
    • package.json 中必须包含 name,version,main 这三个属性,分别代表包的名字、版本号、包的入口。

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

    七、Express

    7.1 基本定义与使用

    Express中文网

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

    • Express 是用于快速创建服务器的第三方模块。

    安装 Express:

    npm install express
    //黑马课程学习建议使用@4.17.1
    
    • 1
    • 2
    1. 创建服务器,监听客户端get、post请求,并把内容响应给客户端
    const express = require('express')
    
    // 创建 web 服务器
    const app = express()
    
    // 监听客户端的 GET 和 POST 请求,并向客户端响应具体的内容
    // 注意这里是向客户端发送相应了!
    app.get('/user', (req, res) => {
    	//向客户端发送 JSON 对象
        res.send({ name: 'zs', age: 20, gender: '男' })
    })
    app.post('/user', (req, res) => {
    	//向客户端发送文本内容
        res.send('请求成功')
    })
    
    //启动web服务器
    app.listen(80, () => {
        console.log('express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    1. 获取 URL 中携带的查询参数和URL中的动态参数
    const express = require('express')
    
    // 创建 web 服务器
    const app = express()
    
    //查询参数
    app.get('/', (req, res) => {
        // 通过 req.query 可以获取到客户端发送过来的查询参数
        // req.query默认是一个空对象
        // 客户端使用 http://127.0.0.1?name=zs&age=20这种查询字符串形式,发送到服务器的参数
        // 可通过req.query对象访问到,如:req.query.name
        console.log(req.query)
        res.send(req.query)
    })
    
    // 这里的 :id 是一个动态的参数 id可以是其他任意合法的值
    app.get('/user/:ids/:username', (req, res) => {
        // req.params 是动态匹配到的 URL 参数,默认是一个空对象
        console.log(req.params)
        res.send(req.params)
    })
    
    //启动web服务器
    app.listen(80, () => {
        console.log('express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    7.2 托管静态资源

    1. .use(express.static())

    express 提供了一个非常好用的函数,叫做 express.static(),通过它,我们可以非常方便地创建一个静态资源服务器,例如,通过如下代码就可以将 public 目录下的图片、CSS 文件、JavaScript 文件对外开放访问了:

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

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

    http://localhost:3000/images/bg.jpg
    http://localhost:3000/css/style.css
    http://localhost:3000/js/login.js
    
    • 1
    • 2
    • 3

    注意: Express 在指定的静态目录中查找文件,并对外提供资源的访问路径,因此,存放静态文件的目录名不会出现在 URL 中,即public不会出现在上述http://...的URL中

    2. 托管多个静态资源目录

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

    app.use(express.static('public'))
    app.use(express.static('files'))
    
    • 1
    • 2

    当public和files中都有index.js,会优先访问public中的

    3. 挂载路径前缀

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

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

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

    http://localhost:3000/public/images/kitten.jpg
    http://localhost:3000/public/css/style.css
    http://localhost:3000/public/js/app.js
    
    • 1
    • 2
    • 3

    7.3 安装nodemon

    作用:

    • 在编写调试 Node.js 项目的时候,如果修改了项目的代码,则需要频繁的手动 close 掉,然后再重新启动,非常繁琐
    • 现在,我们可以使用 nodemon(https://www.npmjs.com/package/nodemon) 这个工具,它能够监听项目文件的变动,当代码被修改后,nodemon 会自动帮我们重启项目,极大方便了开发和调试

    安装nodemon

    在终端中,运行如下命令,即可将 nodemon 安装为全局可用的工具:

    npm install -g nodemon
    
    • 1

    使用 nodemon
    将 node 命令替换为 nodemon 命令,使用 nodemon app.js 来启动项目。使得代码被修改之后,会被 nodemon 监听到,从而实现自动重启项目的效果。

    node 1.js
    nodemon 1.js
    //若无法使用nodemon,管理员权限打开终端输入:set-ExecutionPolicy RemoteSigned 之后按Y
    
    • 1
    • 2
    • 3

    7.4 Express路由

    • 每当一个请求到达服务器之后,需要先经过路由的匹配,只有匹配成功之后,才会调用对应的处理函数
    • 在匹配时,会按照路由的顺序进行匹配,如果请求类型和请求的 URL 同时匹配成功,则 Express 会将这次请求,转交给对应的 function 函数进行处理
    • 理解:打电话给运营商客服,按下1到0的键对应不同服务,这种按键和服务绑定的映射关系,就叫路由

    路由匹配的注意点:

    1. 按照定义的先后顺序进行匹配
    2. 请求类型和请求的URL同时匹配成功,才会调用对应的处理函数

    创建路由模块:

    // router.js
    // 导入express
    const express = require('express')
    
    // 创建路由对象
    const router = express.Router()
    
    // 挂载具体路由
    // /api是统一挂载的访问前缀
    router.get('/api/user/list', (req, res) => {
      res.send('Get user list.')
    })
    router.post('/api/user/add', (req, res) => {
      res.send('Add new user.')
    })
    
    // 向外导出路由对象
    module.exports = router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    注册路由模块:

    const express = require('express')
    const router = require('./router')
    
    const app = express()
    
    // 注册路由模块,添加访问前缀
    //app.use(expresss.static('./files'))
    //app.use()的作用是注册全局中间件 expresss.static和router都是中间件
    // '/api', 是统一挂载的访问前缀
    app.use('/api', router)
    
    app.listen(80, () => {
      console.log('http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    7.5 Express 中间件

    • 中间件是指业务流程的中间处理环节
    • 当一个请求到达 Express 的服务器之后,可以连续调用多个中间件,从而对这次请求进行预处理
    • Express 的中间件,本质上就是一个 function 处理函数,Express 中间件的格式如下:
    //包含 req, res, next 三个参数,next() 参数把流转关系交给下一个中间件或路由
    const mw = function(req, res, next){
    	next()
    }
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    中间件注意事项:

    1. 在注册路由之前注册中间件(错误级别中间件除外)

    2. 中间件可连续调用多个

    3. 执行完中间件的业务代码之后,不要忘记调用 next() 函数

    4. 为了防止代码逻辑混乱,调用 next() 函数后不要再写额外的代码

    5. 连续调用多个中间件时,多个中间件之间,共享 req 和 res 对象

    7.5.1 全局中间件

    通过 app.use() 定义的中间件为全局中间件

    const express = require('express')
    const app = express()
    
    //1 常规写法
    const mw = function(req, res, next){
    	console.log('这是一个中间件')
    	next()
    }
    app.use(mw)
    
    //2 简化形式
    app.use((req, res, next) => {
      console.log('这是一个中间件')
      next()
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. 多个中间件之间,共享同一份 req 和 res。基于这样的特性,我们可以在上游的中间件中,统一为 req 或 res 对象添加自定义的属性或方法,供下游的中间件或路由进行使用
      在这里插入图片描述
    app.use((req, res, next) => {
      req.startTime = Date.now()
      next()
    })
    app.get('/',(req, res) =>{
    	res.send('hi' + startTime)
    })
    app.get('/',(req, res) =>{
    	res.send('hello' + startTime)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    1. 可以使用 app.use() 连续定义多个全局中间件。客户端请求到达服务器之后,会按照中间件定义的先后顺序依次进行
    //定义第一个全局中间件
    app.use((req, res, next) => {
      console.log('调用了第1个全局中间件')
      next()
    })
    // 定义第二个全局中间件
    app.use((req, res, next) => {
      console.log('调用了第2个全局中间件')
      next()
    })
    
    app.get('/user', (req, res) => {
      res.send('User page.')
    })
    
    app.listen(80, () => {
      console.log('http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    7.5.2 局部中间件

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

    const express = require('express')
    const app = express()
    
    // 定义中间件函数
    const mw1 = (req, res, next) => {
      console.log('调用了第一个局部生效的中间件')
      next()
    }
    
    const mw2 = (req, res, next) => {
      console.log('调用了第二个局部生效的中间件')
      next()
    }
    //1. 调用一个中间件
    app.get('/one', mw1, (req, res) => res.send('one page.'))
    
    // 2. 调用多个中间件的两种方式
    app.get('/hello', mw2, mw1, (req, res) => res.send('hello page.'))
    app.get('/about', [mw1, mw2], (req, res) => res.send('about page.'))
    
    app.get('/user', (req, res) => res.send('User page.'))
    
    app.listen(80, function () {
      console.log('Express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    7.5.3 中间件的分类

    1.应用级别的中间件

    通过 app.use() 或 app.get() 或 app.post() ,绑定到 app 实例上的中间件

    2.路由级别的中间件

    • 绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件
    • 用法和应用级别中间件没有区别,应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上
    const app = express()
    const router = express.Router()
    
    router.use(function (req, res, next) {
        console.log(1)
        next()
    })
    
    app.use('/', router)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.错误级别的中间件

    • 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
    • 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是 (err, req, res, next)
    • 错误级别的中间件必须注册在所有路由之后,否则无法捕获路由抛出的错误
    const express = require('express')
    const app = express()
    
    app.get('/', (req, res) => {
        throw new Error('服务器内部发生了错误!')
        res.send('Home page.')
    })
    
    // 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
    app.use((err, req, res, next) => {
        console.log('发生了错误!' + err.message)
        res.send('Error:' + err.message)
    })
    
    app.listen(80, function () {
        console.log('Express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    4.Express 内置中间件

    自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:

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

    演示express.json 和 express.urlencoded

    // 导入 express 模块
    const express = require('express')
    // 创建 express 的服务器实例
    const app = express()
    
    // 注意:除了错误级别的中间件,其他的中间件,必须在路由之前进行配置
    // 通过 express.json() 这个中间件,解析表单中的 JSON 格式的数据
    app.use(express.json())
    // 通过 express.urlencoded() 这个中间件,来解析 表单中的 url-encoded 格式的数据
    app.use(express.urlencoded({ extended: false }))
    
    app.post('/user', (req, res) => {
      // 在服务器,可以使用 req.body 这个属性,来接收客户端发送过来的请求体数据
      // 默认情况下,如果不配置解析表单数据的中间件,则 req.body 默认等于 undefined
      console.log(req.body)
      res.send('ok')
    })
    
    app.post('/book', (req, res) => {
      // 在服务器端,可以通过 req,body 来获取 JSON 格式的表单数据和 url-encoded 格式的数据
      console.log(req.body)
      res.send('ok')
    })
    
    // 调用 app.listen 方法,指定端口号并启动web服务器
    app.listen(80, function () {
      console.log('Express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    5.第三方中间件

    7.5.4* 自定义中间件

    演示body-parser,仅作为演示作用,该中间件和之前express.json 和 express.urlencoded的用法没区别

    // 导入 express 模块
    const express = require('express')
    // 创建 express 的服务器实例
    const app = express()
    
    // 1. 导入解析表单数据的中间件 body-parser
    const parser = require('body-parser')
    // 2. 使用 app.use() 注册中间件
    app.use(parser.urlencoded({ extended: false }))
    // app.use(express.urlencoded({ extended: false }))
    
    app.post('/user', (req, res) => {
      // 如果没有配置任何解析表单数据的中间件,则 req.body 默认等于 undefined
      console.log(req.body)
      res.send('ok')
    })
    
    // 调用 app.listen 方法,指定端口号并启动web服务器
    app.listen(80, function () {
      console.log('Express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    5.第三方中间件
    注意:querystring 模块已被弃用,可改用JSON.parse

    // 导入 express 模块
    const express = require('express')
    // 创建 express 的服务器实例
    const app = express()
    // 导入 Node.js 内置的 querystring 模块
    const qs = require('querystring')
    
    // 这是解析表单数据的中间件
    app.use((req, res, next) => {
      // 定义中间件具体的业务逻辑
      // 1. 定义一个 str 字符串,专门用来存储客户端发送过来的请求体数据
      let str = ''
      // 2. 监听 req 的 data 事件
      req.on('data', (chunk) => {
        str += chunk
      })
      // 3. 监听 req 的 end 事件
      req.on('end', () => {
        // 在 str 中存放的是完整的请求体数据
        // console.log(str)
        // TODO: 把字符串格式的请求体数据,解析成对象格式
        const body = qs.parse(str)
        req.body = body
        next()
      })
    })
    
    app.post('/user', (req, res) => {
      res.send(req.body)
    })
    
    // 调用 app.listen 方法,指定端口号并启动web服务器
    app.listen(80, function () {
      console.log('Express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    7.5.5 编写GET、POST接口

    编写接口

    //exp.js
    const express = require('express')
    const router = express.Router()
    
    // 在这里挂载对应的路由
    router.get('/get', (req, res) => {
      // 通过 req.query 获取客户端通过查询字符串,发送到服务器的数据
      const query = req.query
      // 调用 res.send() 方法,向客户端响应处理的结果
      res.send({
        status: 0, // 0 表示处理成功,1 表示处理失败
        msg: 'GET 请求成功!', // 状态的描述
        data: query, // 需要响应给客户端的数据
      })
    })
    
    // 定义 POST 接口
    router.post('/post', (req, res) => {
      // 通过 req.body 获取请求体中包含的 url-encoded 格式的数据
      const body = req.body
      // 调用 res.send() 方法,向客户端响应结果
      res.send({
        status: 0,
        msg: 'POST 请求成功!',
        data: body,
      })
    })
    
    // 定义 DELETE 接口(用于验证后面发送两次请求,即OPTION 请求和正常请求)
    router.delete('/delete', (req, res) => {
      res.send({
        status: 0,
        msg: 'DELETE请求成功',
      })
    })
    module.exports = router
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    使用接口

    //use.js
    // 导入 express
    const express = require('express')
    // 创建服务器实例
    const app = express()
    
    // 导入路由模块
    const router = require('./exp.js')
    // 把路由模块,注册到 app 上
    app.use('/api', router)
    
    // 启动服务器
    app.listen(80, () => {
      console.log('express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    7.5.6 CORS 跨域资源共享

    CORS 中间件解决跨域

    • 安装中间件:npm install cors
    • 导入中间件:const cors = require(‘cors’)
    • 配置中间件:app.use(cors())

    CORS

    • CORS(Cross-Origin Resource Sharing,跨域资源共享)解决跨域,是通过 HTTP 响应头决定浏览器是否阻止前端 JS 代码跨域获取资源
    • 浏览器的同源安全策略默认会阻止网页“跨域”获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可解除浏览器端的跨域访问限制
    • CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口
    • CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。
    // 导入 express
    const express = require('express')
    // 创建服务器实例
    const app = express()
    
    // 配置解析表单数据的中间件
    app.use(express.urlencoded({ extended: false }))
    
    // 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
    const cors = require('cors')
    app.use(cors())
    
    // 导入路由模块
    const router = require('./exp.js')
    // 把路由模块,注册到 app 上
    app.use('/api', router)
    
    // 启动服务器
    app.listen(80, () => {
      console.log('express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    CORS 常见响应头

    • Access-Control-Allow-Origin:制定了允许访问资源的外域 URL
    //允许百度访问
    res.setHeader('Access-Control-Allow-Origin', 'http://www.baidu.com')
    //允许所有URL访问
    res.setHeader('Access-Control-Allow-Origin', '*')
    
    • 1
    • 2
    • 3
    • 4
    • Access-Control-Allow-Headers
    1. 默认情况下,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 三者之一)
    2. 如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')
    
    • 1
    • Access-Control-Allow-Methods
      默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
    res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
    res.setHEader('Access-Control-Allow-Methods', '*')
    
    • 1
    • 2

    CORS 请求分类

    简单请求

    • 请求方式:GET、POST、HEAD 三者之一
    • HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-formurlencoded、multipart/form-data、text/plain)

    预检请求

    • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
    • 请求头中包含自定义头部字段
    • 向服务器发送了 application/json 格式的数据
    • 在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据

    *编写JSON接口

    // 导入 express
    const express = require('express')
    // 创建服务器实例
    const app = express()
    
    // 配置解析表单数据的中间件
    app.use(express.urlencoded({ extended: false }))
    
    // 必须在配置 cors 中间件之前,配置 JSONP 的接口
    app.get('/api/jsonp', (req, res) => {
      // TODO: 定义 JSONP 接口具体的实现过程
      // 1. 得到函数的名称
      const funcName = req.query.callback
      // 2. 定义要发送到客户端的数据对象
      const data = { name: 'zs', age: 22 }
      // 3. 拼接出一个函数的调用
      const scriptStr = `${funcName}(${JSON.stringify(data)})`
      // 4. 把拼接的字符串,响应给客户端
      res.send(scriptStr)
    })
    
    // 一定要在路由之前,配置 cors 这个中间件,从而解决接口跨域的问题
    const cors = require('cors')
    app.use(cors())
    
    // 导入路由模块
    const router = require('./16.apiRouter')
    // 把路由模块,注册到 app 上
    app.use('/api', router)
    
    // 启动服务器
    app.listen(80, () => {
      console.log('express server running at http://127.0.0.1')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    八、数据库和身份认证

    8.1 Node 操作 mysql

    配置 mysql 模块

    • 安装 mysql 模块
    npm install mysql
    
    • 1
    • 建立连接
    const mysql = require('mysql')
    
    const db = mysql.createPool({
        host: '127.0.0.1',
        user: 'root',
        password: 'root',
        database: 'test',
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 测试是否正常工作
    db.query('select 1', (err, results) => {
       if (err) return console.log(err.message)
       console.log(results)
    })
    
    • 1
    • 2
    • 3
    • 4

    8.2 操作 mysql 数据库

    • 查询数据
    db.query('select * from users', (err, results) => {
      	if (err) return console.log(err.message)
        console.log(results)
    })
    
    • 1
    • 2
    • 3
    • 4
    • 插入数据
    const user = {username: '做一只猫', password:'111111'}
    
    // ? 表示占位符
    const sql = 'insert into users values(?, ?)'
    
    // 使用数组的形式为占位符指定具体的值
    db.query(sql, [user.username, user.password], (err, results) => {
        if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('插入成功')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 插入数据简便方法
      向表中新增数据时,如果数据对象的每个属性和数据表的字段一一对应,则可以通过如下方式快速插入数据:
    const user = { username: '做一只猫', password: '111111' }
    const sql = 'insert into users set ?'
    db.query(sql, user, (err, results) => {
        if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('插入成功')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 更新数据
    const user = { username: '做一只猫', password: '111111' }
    const sql = 'update users set username=?, password=? where id=?'
    db.query(sql, [user.username, user.password, user.id], (err, results) => {
      	if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('更新数据成功')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 更新数据简便方法
    const user = {id: 4, username: '做一只猫', password: '111111' }
    const sql = 'update users set ? where id=?'
    db.query(sql, [user, user.id], (err, results) => {
      	if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('更新数据成功')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 删除数据
      删除数据时,推荐根据id这样的唯一标识来删除对应的数据
    const sql = 'delete from users where id=?'
    //4代表user.id
    //若SQL语句中有多个占位符,则必须使用数组为每个占位符指定具体的值
    //若SQL语句中只有一个占位符,则可以省略
    db.query(sql, 4, (err, results) => {
      	if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('删除数据成功')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 使用 delete 语句会真正删除数据,保险起见,使用标记删除的形式,模拟删除的动作。即在表中设置状态status字段,标记当前的数据是否被删除
    db.query('update users set status=1 where id=?', 7, (err, results) => {
      	if (err) return console.log(err.message)
        if (results.affectedRows === 1) console.log('删除数据成功')
    })
    
    • 1
    • 2
    • 3
    • 4

    8.3 Web 开发模式

    服务端渲染

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

    app.get('/index.html', (req, res) => {
      const user = { name: 'Bruce', age: 29 }
      const html = `

    username:${user.name}, age:${user.age}

    `
    res.send(html) })
    • 1
    • 2
    • 3
    • 4
    • 5
    • 优点:
    1. 前端耗时短。浏览器只需直接渲染页面,无需额外请求数据。
    2. 有利于 SEO。服务器响应的是完整的 HTML 页面内容,有利于爬虫爬取信息。
    • 缺点:
    1. 占用服务器资源。服务器需要完成页面内容的拼接,若请求比较多,会对服务器造成一定访问压力。
    2. 不利于前后端分离,开发效率低。

    前后端分离

    前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。后端只负责提供 API 接口,前端使用 Ajax 调用接口。

    • 优点:
    1. 开发体验好。前端专业页面开发,后端专注接口开发。
    2. 用户体验好。页面局部刷新,无需重新请求页面。
    3. 减轻服务器的渲染压力。页面最终在浏览器里生成。
    • 缺点:

      不利于 SEO。完整的 HTML 页面在浏览器拼接完成,因此爬虫无法爬取页面的有效信息。

    Vue、React 等框架的 SSR(server side render)技术能解决 SEO 问题。

    如何选择

    • 企业级网站,主要功能是展示,没有复杂交互,且需要良好的 SEO,可考虑服务端渲染
    • 后台管理项目,交互性强,无需考虑 SEO,可使用前后端分离
    • 为同时兼顾首页渲染速度和前后端分离开发效率,可采用首屏服务器端渲染 + 其他页面前后端分离的开发模式

    8.4 身份认证

    • 身份认证又称“身份验证”、“鉴权”,是指通过一定的手段,完成对用户的确认
    • 如各大网站的手机验证码登录、账号密码验证等
    • 服务端渲染推荐使用Session认证机制
    • 前后端分离推荐使用JWT认证机制

    8.4.1 Session 认证机制

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

    Session 工作原理
    在这里插入图片描述

    会员卡例子详解:

    1. HTTP 协议的无状态性

    • 了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提
    • HTTP 协议的无状态性,指的是客户端的每次 HTTP 请求都是独立的,连续多个请求之间没有直接的关系,服务器不会主动保留每次 HTTP 请求的状态

    在这里插入图片描述
    2. 如何突破 HTTP 无状态的限制

    对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡
    在这里插入图片描述
    注意: 现实生活中的会员卡身份认证方式,在 Web 开发中的专业术语叫做 Cookie。

    3. 关于 Cookie

    • Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串
    • 它由一个名称(Name)、一个值(Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性组成
      -不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器
    • Cookie的几大特性:
    1. 自动发送
    2. 域名独立
    3. 过期时限
    4. 4KB 限制

    4. Cookie 在身份认证中的作用

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

    5. Cookie 不具有安全性

    • 由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性
    • 因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器
    • 注意区分伪造跟盗取的不同
      在这里插入图片描述
      注意: 千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

    6. 提高身份认证的安全性

    在这里插入图片描述
    这种 “会员卡 + 刷卡认证” 的设计理念,就是 Session 认证机制的精髓。

    8.4.2 Express 中使用 Session 认证

    安装 express-session 中间件

    npm install express-session
    
    • 1

    配置中间件

    const session = require('express-session')
    app.use(
    	session({
    		secret: '做一只猫', 		// secret 属性的值为任意字符串
    		resave: false,			//固定写法
    		saveUninitalized: true	//固定写法
      })
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    向 session 中存数据
    中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息

    app.post('/api/login', (req, res) => {
    	//判断用户提交的登录信息是否正确
    	if(req.body.username !== 'admin' || req.body.password !== '000000'){
    		return res.send({status: 1, msg: '登录失败'})
    	}
    	req.session.user = req.body	//将用户的信息存储到S
    	req.session.isLogin = true
    
     	res.send({ status: 0, msg: 'login done' })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从 session 取数据

    app.get('/api/username', (req, res) => {
    	//判断用户是否登录
     	if (!req.session.isLogin) {
        	return res.send({ status: 1, msg: 'fail' })
      	}
      	res.send({ status: 0, msg: 'success', username: req.session.user.username })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    清空 session

    app.post('/api/logout', (req, res) => {
     	// 清空当前客户端的session信息
    	req.session.destroy()
    	res.send({ status: 0, msg: '退出登录成功' })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5

    8.4.3 JWT 认证机制

    • Session 认证机制需要配合 Cookie 才能实现。
    • 由于 Cookie 默认不支持跨域访问,所以,当涉及到前端跨域请求后端接口的时候,需要做很多额外的配置,才能实现跨域 Session 认证
    • JWT(英文全称:JSON Web Token) 是目前最流行的跨域认证解决方案

    注意:

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

    JWT 的工作原理
    用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份
    在这里插入图片描述

    JWT 组成部分:
    Header、Payload、Signature

    • Payload 是真正的用户信息,加密后的字符串
    • Header 和 Signature 是安全性相关部分,保证 Token 安全性
    • 三者使用 . 分隔
    Header.Payload.Signature
    
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTcsInVzZXJuYW1lIjoiQnJ1Y2UiLCJwYXNzd29yZCI6IiIsIm5pY2tuYW1lIjoiaGVsbG8iLCJlbWFpbCI6InNjdXRAcXEuY29tIiwidXNlcl9waWMiOiIiLCJpYXQiOjE2NDE4NjU3MzEsImV4cCI6MTY0MTkwMTczMX0.bmqzAkNSZgD8IZxRGGyVlVwGl7EGMtWitvjGD-a5U5c
    
    • 1
    • 2
    • 3

    JWT 使用方式:

    • 客户端会把 JWT 存储在 localStorage 或 sessionStorage 中
    • 此后客户端与服务端通信需要携带 JWT 进行身份认证,将 JWT 存在 HTTP 请求头 Authorization 字段中
    • 加上 Bearer 前缀
    Authorization: Bearer <token>
    
    • 1

    8.4.4 Express 使用 JWT

    安装

    • jsonwebtoken 用于将用户的信息生成 JWT 字符串
    • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
    //安装多个包中间用空格隔开
    npm install jsonwebtoken express-jwt
    
    • 1
    • 2

    定义 secret 密钥

    • 为保证 JWT 字符串的安全性,防止其在网络传输过程中被破解,需定义用于加密和解密的 secret 密钥
    1. 生成 JWT 字符串时,使用密钥加密信息,得到加密好的 JWT 字符串
    2. 把 JWT 字符串解析还原成 JSON 对象时,使用密钥解密
    const jwt = require('jsonwebtoken')
    const expressJWT = require('express-jwt')
    
    // 密钥可以为任意字符串
    const secretKey = '做一只猫No.1'
    
    • 1
    • 2
    • 3
    • 4
    • 5

    生成 JWT 字符串

    app.post('/api/login', (req, res) => {
      ...
      res.send({
        status: 200,
        message: '登录成功',
        // jwt.sign() 生成 JWT 字符串
        // 参数:用户信息对象、加密密钥、配置对象-token有效期
        // 尽量不保存敏感信息,因此只有用户名,没有密码
        token: jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '10h'})
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    JWT 字符串还原为 JSON 对象

    • 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证
    • 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
    // unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
    app.use(expressJWT({ 
    	secret: secretKey,
    	//6.0版本之后要加上这个
    	algorithms: [HS256]
    }).unless({ path: [/^\/api\//] }))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    获取用户信息

    • 当 express-jwt 中间件配置成功后,即可在那些有权限的接口中,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息
    app.get('/admin/getinfo', (req, res) => {
      console.log(req.user)
      res.send({
        status: 200,
        message: '获取信息成功',
        //data: req.user
        //6.0版本后改用req.auth
        data: req.auth
        
      })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

    • 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
    • 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
    app.use((err, req, res, next) => {
      if (err.name === 'UnauthorizedError') {
        return res.send({ status: 401, message: 'Invalid token' })
      }
      res.send({ status: 500, message: 'Unknown error' })
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注: 本篇博客不包含时钟案例(P11-P13),时钟web案例(P18),大事件项目(P77-P96),仅包含基础知识内容,供搜索备忘。

  • 相关阅读:
    The 2021 ICPC Asia Nanjing Regional Contest H. Crystalfly
    【C++进阶】:特殊类的设计
    尚硅谷大数据项目《在线教育之实时数仓》笔记001
    redis:内存穿透、内存击穿、内存雪崩以及各数据类型应用场景
    【华为OD机试真题 python】最长连续子序列 【2022 Q4 | 100分】
    前端js手写面试题看这篇就够了
    【CSDN话题挑战赛第2期】​web组态可视化领域分享​
    异或和大小比较类问题——抓住最高位:CF1863F
    html +css 练习的(太极图)
    彻底理解Java并发:Java内存模型
  • 原文地址:https://blog.csdn.net/m0_51487301/article/details/125959702