nodejs

305 阅读18分钟

服务器

服务器架构

  • B/S 架构:Browser(浏览器)/ Server(服务器)
  • C/S 架构:Client(客户端)/ Server(服务器)

网络协议

网络协议,是指计算机伪类能够在网络中进行数据的交换,从而建立的一个规则,标准。

TCP/IP 协议

TCP/IP 协议,称为“网络通信协议”,是指互联网中最基本的协议。全世界所有的计算机都要遵循这个协议。

TCP/IP 协议是一个协议组,在协议下还包含小的协议:

  • HTTP(S)
  • TCP
  • IP
  • DNS
  • ……

状态码

  • 概念:指一次请求在处理中的实时状态。根据状态的不同,用指定3个数字的组合来表示请求的不同状态。

  • 常见状态码

    • 404:表示当前请求对应的资源没找到。资源可以是页面、图片、视频、路由。即Not found
    • 200:表示请求正常处理完毕。一般会表示请求成功
    • 304:指资源无修改,会直接使用缓存。
    • 500:指服务器程序错误。一般是指服务器程序有bug,即代码不对
    • 401:指请求未授权访问。即浏览器没有权限
    • 403:请求被服务器拒绝。比如防盗链,图片限制下载。
  • 概念:指一次请求在处理中的实时状态。根据状态的不同,用指定3个数字的组合来表示请求的不同状态。

  • 常见状态码

    • 404:表示当前请求对应的资源没找到。资源可以是页面、图片、视频、路由。即Not found
    • 200:表示请求正常处理完毕。一般会表示请求成功
    • 304:指资源无修改,会直接使用缓存。
    • 500:指服务器程序错误。一般是指服务器程序有bug,即代码不对
    • 401:指请求未授权访问。即浏览器没有权限
    • 403:请求被服务器拒绝。比如防盗链,图片限制下载。

在浏览器中输入 URL 后发生什么

1.域名解析

“域名”就是我们平常说的网址:如https://www.xiaobinw.cn中的www.xiaobinw.cn就是我自己的域名。

真正访问一个计算机是通过计算机的 IP 地址去访问,但是 IP 地址不容易记忆,所以出现了域名。

“域名解析”指的是通过 DNS 服务器(域名解析服务器)对域名进行解析,找到对应的计算机 IP 地址。###

2.建立 TCP 链接

TCP (传输控制协议)用于保证计算机之间的数据传递的完整和安全性。

三次握手:

TCP 通过三次握手来保证数据的完整和安全,三次握手是为了保证客户端和服务端都处于正常的工作状态。

image-20230130221809665

3.客户端发送请求服务器处理请求

TCP 连接成功后,浏览器就可以利用 HTTP 协议向服务器发送请求了。

服务器接收请求后,开始处理请求,处理完请求后,服务器将处理的结果返回(响应)给客户端

4.关闭 TCP 连接

客户端接收到服务器发送的数据后,需要通过 TCP 协议来断开与服务器的连接。

四次挥手

image-20230130222805199

5.浏览器渲染页面

浏览器接收到服务器响应的数据后,开始对数据进行解析渲染。

Nodejs

Nodejs 简介

Nodejs 是基于 Chrome V8 引擎的 javascript 运行的。

  • javascript 运行时:JS 代码的运行环境
  • Chrome V8 引擎
    • 浏览器内核:主要分为“渲染引擎”和 “JS 引擎”
      • 渲染引擎:负责解析 HTML 和 CSS 代码
      • JS 引擎:赋值解析 JS 代码
    • V8 引擎是谷歌公司开发的一款 JS 引擎,目前公认的解析 JS 代码最快的引擎

Nodejs 让 javascript 代码可以运行在服务端

命令行工具

cd:进入某个文件夹

  • cd 后面可以跟某个路径(建议直接拖)
  • cd 后面还可以跟文件夹名字(该文件夹必须当前路径的子文件)

模块化

每个 JS 文件都是独立的模块,模块之间是没有关联的。默认情况下模块之间不能进行数据交换。

以前“模块化”是后端的一种概念,前端没有模块化,从 ES6 开始前端也引入模块化的概念。

前端模块化

引入 JS

  • 在 HTML 引入 JS:需要添加一个type="module"属性

    • <script src="./index.js" type="module"></script>
      
  • 在 JS 中引入 JS:一个模块中加载另一个模块import '相对路径'

    • import './a.js'
      

暴露和引入

1.1 暴露export default

function foo() {
    console.log('foo')
}
export default { a: 1, foo: foo }

1.2 引入import 变量名 from '相对路径'

import a from './a'
console.log(a.a);
a.foo()

2.1 暴露export

export let a = 1
export function foo {
    console.log('hello foo')
}

2.2 引入import { 变量名 } from '相对路径'

import { a } from './a.js'
// 重命名
import { a as A } './a.js'

后端模块化

CommonJS

ECMAscript 是一种规范,javascript 是这个规范的实现

CommonJS 是一种规范,NodeJS 是这个规范的实现

  • 在 JS 中引入 JS:
require('./a.js')
  • 暴露和引入 JS:

    • 暴露
    module.exports.a = 10
    module.exports = { a:10 }
    
    • 引入
    const a = require('./a.js')
    const { a } = require('./a.js')
    

注意:

  1. 当前使用 require 去加载某个模块时,除了第一次加载会运行文件以外(第一次运行完成后会进行缓存),后续的加载都会从缓存中读取文件内容。换句话说同一个文件被 require 多次,只执行一次。
  2. require 中的 JS 文件后缀可以省略。
  3. require 中的路径如果有模块名,那就说明引入的是第三方下载的模块或者是 NodeJS 原生自带的模块

内置模块

fs

  • 读取文件fs.readFile异步
// 异步
fs.readFile('./test.txt', 'utf-8', (err, data) => {
    if (err) {
        console.log('异步读取失败:', err);
    } else {
        console.log('异步读取成功', data);
    }
})
  • 读取文件fs.readFileSync同步
// 同步
try {
    const result = fs.readFileSync('./test.txt', 'utf-8')
    console.log('同步读取成功', result);
} catch (error) {
    console.log('同步读取失败', error);
}
  • fs.writeFile往文件里写入内容 (会覆盖,会自动创建文件(但不会创建文件夹))
fs.writeFile('./test.txt', 'hello world', (err) => {
    if (err) {
        console.log('写入失败', err);
    } else {
        console.log('写入成功');
    }
})
  • fs.appendFile往文件追加内容
fs.appendFile('./test.txt', '\n这是新加的内容', (err) => {}
  • fs.copyFile复制文件
fs.copyFile('./test.txt', './test2.txt', (err) => {}
  • fs.unlink文件删除
fs.unlink('./test.txt', (err) => {}
  • fs.rename文件重命名
fs.rename('./test.txt','test1.txt',(err)=>{}
  • fs.mkdir创建文件夹(只能创建一个文件夹)
fs.mkdir('./public', (err) => {
    if (err) {
        console.log('创建文件夹失败', err);
    } else {
        console.log('创建文件夹成功');
    }
})
  • fs.rmdir删除文件夹(只能删除空文件夹)
fs.rmdir('./a', (err) => {}
  • fs.readdir读取文件夹内容
fs.readdir('./a', (err, data) => {}
  • fs.access判断文件(文件夹是否存在)
fs.access('./a/b/c', (err) => {}
  • fs.stat查看文件(文件夹状态):判断是文件还是文件夹
fs.stat('./a', (err, stats) => {
    if (err) {
        console.log('查看失败', err);
    } else {
        const isFile = stats.isFile()
        const idDir = stats.isDirectory()
        console.log(isFile, idDir);
    }
})

path

path 获取

  • basename():获取指定路径中最后一部分
const path = require('path')
const myPath = __dirname + '\\a.js'
console.log(path.basename(myPath));
  • dirname():获取指定路径中除了 basename 以外的其他路径
console.log(path.dirname(myPath));
  • extname():获取指定路径对应的扩展名
console.log(path.extname(myPath));

path 组合

  • join():相对路径
console.log(path.join('a','b','c')); // a/b/c
  • resolve():绝对路径
console.log(path.resolve('a','b','c')); // E://xxx/a/b/c

path 拆分

  • parse():从对象返回路径字符串
console.log(path.parse(myPath)); 
/* {
  root: 'E:\\',
  dir: 'E:\\nodejs\\02-nodejs模块',
  base: 'a.js',
  ext: '.js',
  name: 'a'
} */

http

const http = require('http');

// 创建本地服务器来从其接收数据
// request(req):请求对象,包含了请求相关的数据和方法 
// response(res):响应对象,包含响应相关的数据和方法
const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    data: 'Hello World!'
  }));
});

server.listen(3000,()=>{
    console.log('服务器开启成功端口号为:http://localhost:3000/')
});  // http://localhost:3000/

异步

同步异步

  • 同步:在同一时间段内,只能做一件事情
  • 异步:在同一时间段内,可以同时做多件事情

javascript 是单线程语言,从头到尾,从左到右,有错误就停止运行。

Ajax

  • 可以实现与服务器的异步通信
  • 局部刷新页面

异步解决的发展

  • 回调函数
  • Promise(ES6)
  • generator(ES6)
  • async / await(ES7):异步终极解决方案

Promise

const p = new Promise((resolve, reject)=>{
    // 异步代码
    $.ajax({
        url:'/users',
        success(msg){
            // 成功回调
            resolve(msg)
        }
    })
})
// 成功执行
p.then((msg) => {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: '/users',
            data: {
                id: msg.id
            }
        })
    }).then((msg)=>{})
})
// 失败执行
p.catch(()=>{})

async/await

async:用于定义一个异步函数,异步函数的返回值是 Promise 对象

async function foo() { }
foo()

await:一般用于等待一个 Promise 对象,实际上就是等待一个异步处理结果

const p = new Promise((resolve, reject)=>{
    // 异步代码
    resolve({id:1})
})
async function foo() {
    const msg = await p
    console.log(msg) // {id:1}
}
foo()

axios

  1. 发起get请求:
axios.get(请求地址,{params: 参数}).then(function (res) {});
  1. 发起post请求:
axios.post(请求地址,{data: 参数}).then(function (res) {});
axios.post(请求地址, 参数).then(function (res) {});
  1. 综合方法:
axios({
	method: 请求类型,
	url: 请求地址,
	params: get请求发送的数据,
	data: post请求发送的数据,
})
.then(function (res){})
.cache(function (error){})

注意:res不是服务器返回的原始数据,是axios加工之后的数据;res.data是服务器返回的数据;

Express

Express 是一个基于 Nodejs 的服务框架

Express 应用程序生成器

Express 应用生成器,名称:express-generator。他的作用是可以帮助开发者快速创建一个 express 项目

全局安装

执行一下命令,来全局安装 Express 应用生成器:express-generator

npm i express-generator -g

Express 项目创建

  1. 通过应用程序生成器来快速创建 Express 项目
express 项目名称
  1. 进入到项目目录
cd 03-express
  1. 下载所有的依赖包
npm install
  1. 运行服务器
npm start
  1. 访问项目
http://localhost:3000

MongoDB

数据库(database),用于数据的管理。

  • 关系型数据库:MySQL
  • 非关系型数据库:MongoDB

使用方法

  • 命令行操作 MongoDB
  • 可视化图形工具中操作 MongoDB
  • 通过后端代码去操作 MongoDB

image-20230202231619742

image-20230202233328915

1.在命令行中操作 MongoDB

找到 MongoDB 安装包中的 bin 目录下的 mongodbsh.exe

常用命令

  1. 查看当前 MongoDB 服务器中所有的数据库

    show dbs
    
  2. 查看当前执行的数据库

    db
    
  3. 新建/切换数据库

    use 数据库名
    
  4. 查件当前数据库中所有的结合

    show collections
    
  5. 往集合中添加数据(如果集合不存在,会自动创建该集合)

    db.集合名称.insert({name:'小斌',age:'19',sex:'男'})
    
  6. 查看某个集合中所有数据

    db.集合名称.find()
    
  7. 后续命令省略,因为命令行太他妈麻烦了,了解就好需要百度

2.可视化图形操作 MongoDB

简单不用做笔记

3.后端代码操作 MongoDB

mongoose 是 NodeJS 中提供的一个便捷操作 MongoDB 的库

  1. 下载
npm i mongoose --save
  1. 连接 MongoDB
// 连接 MongoDB 将 express 项目与 MongoDB 服务器连接起来
const mongoose = require('mongoose')
// 项目需要连接的数据库地址
const dbURI = 'mongodb://localhost:27017/xiaobin'
// 关闭警告
mongoose.set('strictQuery', true);
// 连接数据库
mongoose.connect(dbURI)
// 当数据库连接成功时触发下面事件
mongoose.connection.on('connected', function () {
  console.log(dbURI + '数据库连接成功');
})
  1. 数据库集合相关配置(models)
// 1.定义数据集合的结构:定义集合中数据有哪些属性,属性的值类型是什么类型
const { Schema, model } = require('mongoose')
const usersSchema = new Schema({
  userName: String,
  password: String
})

// 2.定义数据集合的模型:将 schema 和数据库中的数据关联起来
// model('模型名称','schema名称','数据库中的集合名称')
const usersModel = model('usersModel', usersSchema, 'users')
router.post('/login', async function (req, res, next) {
  // 1.接收前端发送的数据
  //  post:req.body
  //  get: req.query
  const result = await usersModel.find(req.body)
  if (result.length > 0) {
    res.send({
      message: '登录成功',
      status: 1
    })
  }
});

操作数据

  • 查询,查找
// 按条件查询
usersModel.find( {userName:'xiaobin'} )
// 查询所以数据
usersModel.find( )
  • 新增,删除,修改
// 新增
usersModel.create( {userName:'xiaobin'} )
// 删除(一个)
usersModel.deleteOne( {_id: 1'} )
// 删除(多个)
usersModel.deleteMany( {userName:'xiaobin'} )
// 修改
usersModel.updateOne( {_id: 1}, {userName:'xiaobin'} )

注:以上所以方法都是异步方法,且这些方法的返回值都是 Promise 对象,因此需要通过 await 去等待结果。

结构及封装

三层结构

image-20230203012230365

  1. 通过前端将数据发送到表现层
axios({
    // 设置请求类型
    method: 'post',
    // 设置前后端链接接口
    url: '/users/login',
    // 设置前端发送给后端的数据
    data: { }
})
 .then((response) => { })
 .catch((error) => { });
  1. 配置数据库连接(database)
// 连接 MongoDB
const mongoose = require('mongoose')
// 项目需要连接的数据库地址
const dbURI = 'mongodb://localhost:27017/xiaobin'
// 关闭警告
mongoose.set('strictQuery', true);
// 连接数据库
mongoose.connect(dbURI)
// 当数据库连接成功时触发下面事件
mongoose.connection.on('connected', function () {
    console.log(dbURI + '数据库连接成功');
})
  1. 配置 app.js
// 引入路由
var usersRouter = require('./routes/users');
// 引入第三层(持久层)
require('./dao/database')
// 用于配置 Ajax 请求的一级路径
app.use('/users', usersRouter);
  1. 配置第一层表现层(routes)
var express = require('express');
var router = express.Router();
// 二级路径
router.get('/login', function(req, res, next) {
  // req.body: 获取 get 请求数据
  // req.query: 获取 post 请求数据
  res.send( );
});
module.exports = router;
  1. 配置第二层服务层(service)
// 引入第三层
const { daoLogin } = require('../dao/usersDao')
// 登录
async function serviceLogin(user) {
    // 调用第三层
    const data = await daoLogin(user)
}
module.exports = {serviceLogin}
  1. 配置第三层持久层(Dao)
// 引入模型
const { usersModel } = require('./models/usersModel')
// 登录
async function daoLogin(user) {
    return await usersModel.find(user)
}
module.exports = { daoLogin }
  1. 配置模块(models)
// 1.定义数据集合的结构:定义集合中数据有哪些属性,属性的值类型是什么类型
const { Schema, model } = require('mongoose')

const usersSchema = new Schema({
    userName: String,
    password: String
})

// 2.定义数据集合的模型:将 schema 和数据库中的数据关联起来
// model('模型名称','schema名称','数据库中的集合名称')
const usersModel = model('usersModel', usersSchema, 'users')

module.exports = { usersModel }
  1. 端口更改
app.listen(3000, () => {
  console.log('服务器开启成功端口号为:http://localhost:3000');
});

express 方法

  1. 深层嵌套
 .populate({
    path: 'classId', // 第一层关联路径
    populate: {
         path: 'teacherId' // 第二层关联路径
  	 }
})
  1. 请求限制
.limit(请求条数 - 0) // 设置请求条数
  1. 请求跳过
.skip((请求页数 - 1) * 请求条数) // 跳过数据的条数
  1. 关联集合
const studentsSchema = new Schema({
    // 用于关联classes集合
    classId: {
        type: Schema.Types.ObjectId, // id
        // ref 用于设置要关联的集合模型名称
        ref: 'classesModel'
    }
})

查询

  • 全部查询,带数据劫持
// 获取学生数据
async function daoGetStudents({ pageSize(条), currentPage(页) }) {
    // 获取数据的总条数
    const total = await studentsModel.countDocuments()
    // 计算总页数
    const pages = Math.ceil(total / pageSize)
    const students = await studentsModel
        .find()
        // 关联.populate('classId')
        .populate({
            path: 'classId',
            populate: {
                path: 'teacherId'
            }
        })
        .limit(pageSize - 0) // 设置请求条数
        .skip((currentPage - 1) * pageSize) // 跳过数据的条数
    return {
        total, pages, students
    }
}
  • 添加查询不带数据劫持
// 条件渲染
async function searchStudents(params) {
    const students = await studentsModel.find({
        // 键					值,不区分大小写
        [params.selectValue]: { $regex: params.searchValue, $options: 'i' }
    })
        .populate({
            path: 'classId',
            populate: {
                path: 'teacherId'
            }
        })
        return {
            students
        }
}

图片上传

  • 图片上传流程

主要流程

  1. 前端部分
    • 页面上设置一个上传文件的 input标签,并设置onchange事件
    • 通过onchange事件获取要上传的文件数据并通过ajax传给后端
  2. 后端部分
    1. 需要通过npm安装图片处理插件multer
    2. 单独新建一个处理图片的路由文件,即routes/upload.js
    3. 在第二步新建js中引入文件handlerFile.js,用于在nodejs环境下配合multer插件完成图片上传
    4. 将图片上传后的服务器地址传回前端。前端就可以获取上传的图片的地址。可以通过img标签进行展示

handleFile 封装工具

const multer = require('multer')
const fs = require('fs')
const path = require('path')
/** 
 * 文件上传
 * 参数说明:接收一个 options 对象作为参数,该对象包含两个属性
 * - path: 图片上传路径
 * - key:与前端 formData 对象的 fieldname 相匹配(即: formData.append()方法的第一个参数)
 * - size:设置图片大小
 */
function uploadFiles(options = {}) {
    // 1.对参数 options 进行结垢并设置默认值
    let { path = './public/temp', key = 'file', size = 1000 } = options
    // 2.设置 multer 的参数,配置 diskstorage 来控制文件存储的位置及文件名
    let storage = multer.diskStorage({
        // 2.1 确定图片位置
        destination: function (req, res, cb) {
            try {
                fs.accessSync(path)
            } catch (error) {
                fs.mkdirSync(path)
            }
            cb(null, path)
        },
        // 2.2 确定图片存储时的名字(注意:如果使用原名,可能会造成再次上传同一张图片)
        filename: function (req, file, cb) {
            let changedName = new Date().getTime() + '-' + file.originalname
            cb(null, changedName)
        }
    })
    // 3. 配置图片限制
    let limits = {
        // 限制文件大小
        fileSize: 1024 * size,
        // 限制文件数量
        files: 5
    }
    // 4.生成的专门处理上传的一个工具,可以传入 storage。limits等配置
    let upload = multer({ storage, limits })
    // 5.返回多文件上传设置信息(同样可以上传单文件)
    return upload.array(key)
}

/**
 * 文件复制
 * fromPath:源文件路径
 * - toPath:要复制过去的新文件
 * - filename:文件名
 */
function copyFiles(options = {}) {
    let { fromPath = './public.temp/', toPath = './public/images/', filename } = options
    let sourceFile = path.join(fromPath, filename)
    let destPath = path.join(toPath, filename)
    try {
        fs.accessSync(toPath)
    } catch (error) {
        fs.mkdirSync(toPath)
    }
    let readStream = fs.createReadStream(sourceFile)
    let writeStream = fs.createWriteStream(destPath)
    readStream.pipe(writeStream)
}

/**
 * 文件移动
 * - fromPath:源文件路径
 * - toPath:要复制过去的新文件
 * - filename:文件名
 */
function moveFiles(options = {}) {
    let { fromPath = './public/temp/', toPath = './public/images/', filename } = options
    let sourceFile = path.join(fromPath, filename)
    let destPath = path.join(toPath, filename)
    try {
        fs.accessSync(toPath)
    } catch (error) {
        fs.mkdirSync(toPath)
    }
    fs.renameSync(sourceFile, destPath)
}

/**
 * 删除文件
 * - filePath:文件路径 
 */
function removeFiles(filePath = './public/temp') {
    let stats = fs.statSync(filePath)
    if (stats.isFile()) {
        // 删除文件
        fs.unlinkSync(filePath)
    } else if (stats.isDirectory()) {
        let filesArr = fs.readdirSync(filePath)
        filesArr.forEach(file => {
            removeFiles(path.resolve(filePath, file))
        })
    }
    fs.rmdirSync(filePath)
}

module.exports = {
    uploadFiles, copyFiles, moveFiles, removeFiles
}

前端部分

  1. 页面部分:input标签,并设置onchange事件

    <input type="file" id="uploadInput">
    <h1>上传图片展示</h1>
    <img src="" alt="" id="uploadImg">
    
  2. ajax部分:获取图片数据,并包装为formData格式,并通过ajax发送到后端。需要注意的是,为了防止jQuery自动处理,需要传递两个参数来取消jQuery的表单自动处理

    $('body').on('change', '#uploadInput', function () {
        //获取文件数据   
        let file = this.files[0];
        //以表单数据方式将文件发送到后端  
        let fd = new FormData();
        fd.append("file", file);
        //通过ajax发送到后端   
        $.ajax({
            url: '/upload/image',
            success: function (data) {
                console.log(data);
            },
            data: fd,
            //设置参数,防止jQuery自动处理,以便将文件数据原封不动的传给后端处理     
            contentType: false,
            processData: false,
            cache: false,
        });
    })
    

时间插件

  1. 下载
npm i moment
  1. 引入
const moment = require('moment')
  1. 使用格式化时间
time: {
    type: String,
    default: moment().format('YYYY-MM-DD HH:mm')
}

后端部分

  1. 安装multer插件

    npm i multer
    
  2. 新建路由并在app.js中配置

    var express = require('express'); var router = express.Router(); module.exports = router;
    
  3. 设置图片处理的相关代码

    // 引入handlerFile.jsconst
    { uploadFiles } = require('./../util/handleFiles');
    router.post('/image', function (req, res, next) {
        //会针对传入的图片生成一个专门上传的图片函数   
        let path = 'assets';
        //自定义保存图片的文件夹,会直接放在/public下   
        const uploadImages = uploadFiles({
            path: `./public/${path}`,
            //formdata.append()的第一个参数       
            key: 'file',
            //图片的大小限制,1mb以内        
            size: 1024
        });
        //上传函数   
        uploadImages(req, res, (err) => {
            //图片上传失败或成功都会调用的回调函数     
            if (err) {
                //上传失败         
                console.log(err);
                res.send({ message: "上传失败", code: -1 })
            } else {
                //上传成功        
                //将上传成功之后图片的路径发送给前端      
                res.send({
                    message: '上传成功',
                    data: `../${path}/${req.files[0].filename}`, 
                    code: 200
                })
            }
        })
    })
    
  4. 前端根据返回的地址进行展示

身份认证

session 的身份验证流程

  1. 用户输入信息
  2. 服务器验证是否正确,如果正确就创建 session 并存入数据库
  3. 服务端向客户端返回带有 session 的 cookie
  4. 在接下来的每次前端请求中都会带 cookie 服务器吧 sessionID 与数据库中的相匹配
  5. 入伙退出登录,session 会在客户端和服务端都销毁

JWT 的身份验证流程

  1. 用户输入信息
  2. 服务器验证登录信息,如果正确返回一个已签名 token (加密字符串)
  3. 前端将 token 存储在客户端,一般存在 localStorage 中,但也存在 sessionStorage 和 cookie
  4. 接下来每次前端请求都会带上 token
  5. 服务器验证 token 如果 token 有效继续发送求情
  6. 如果退出,token 会在客户端销毁,而这一步和服务器无关

步骤

  1. 安装

    • jsonwebtoken:用于生成 token

    • express-jwt:用于验证 token

npm i jsonwebtoken express-jwt
  1. 发送请求,服务端生成 token
const jwt = require('jsonwebtoken')
const token = jwt.sign(
     { userName }, // 用户信息
     'abc', // 秘钥
     { expiresIn: 60 } // 设置token的有效期
)
  1. 用户登录,客户端保存 token
localStorage.setItem('token(属性名))', response.data.token(属性值)) 
  1. 验证页面,发送 token 请求
// 验证用户是否登录
function isLogin() {
    axios({
        method: 'post',
        url: '/users/isLogin',
        // 请求头
        headers: {
            'Authorization': 'Bearer ' + localStorage.token,
        }
    }).then((response) => {
        console.log(response.data);
    }).catch((error) => {
        if (error.response.status == 401) {
            alert('身份信息已过期,请从新登录')
            location.href = './login.html'
        }
    });
}
isLogin()
  1. app.js 验证 token
var { expressjwt: expressJWT } = require("express-jwt");//解析JWT

app.use(expressJWT({
    secret: 'abc', // 生成 token 时配置的秘钥字符串
    algorithms: ["HS256"], // 设置 jwt 的算法为 HS256
    credentialsRequired: false // 对于没有 token 的请求是否进行解析
}).unless({
    path: ['/users/login', '/users/register', '/users/isAccess'], // 设置不需要验证 token 的路径
}))
  1. 一级路径,验证是否登录
// 是否登录
router.post('/isLogin', async function (req, res, next) {
  // 1. 获取到 token
  const token = req.get('Authorization').split(' ')[1]
  // 2. 解码 token
  const { userName } = jwt.verify(token, 'abc')
  res.send()
}

JWT 与 session 的区别

JWT 与 session 两种认证方法的区别有两个:

  • 一是在于用户状态保存的位置,session 将用户状态保存在服务端,而 JWT 将用户状态保存在客户端

  • 二是 session 依赖于 cookie 而实现,在每次请求是需要带上cookie 取出响应字段与服务器端的警醒对比,而实现身份的认证,而 JWT 仅仅需要在 HTTP 的都不附上 token 由服务器 sheck signature 即可实现,无需担心 cookie 存在的 CORS 问题

RESTful

REST表现层状态装换,实际就是一种接口的命名规范。

  • url(请求路径)用来定义资源名称

  • method(请求类型)用来定义资源动作

请求类型

  • get(获取,查询)

  • pust(新增)

  • put(修改)

  • delete(删除)

请求方式

如果传递的数据中有一个唯一值 _id,我们会将 _id 放到 url 中。

// 前端(删除)
axios({
    method: 'delete',
    url: `/students/${_id}`,
}).then((response) => { }
        
// 后端(删除)
router.delete('/:_id', async function (req, res, next) {
    const data = await servDelStudents(req.params._id)
    res.send(data)
});

image-20230207172859014

image-20230207162143798

跨域

同源策略

  • 一个请求至少三部分组件: 协议 + IP + 端口。

  • 同源策略是浏览器的安全策略,他要求协议 + IP + 端口三者都必须一致,则为同源。

  • 当我们在一个页面中,向服务器发送其他请求时,页面地址和请求地址,协议,IP,端口三者不一致则为跨域

跨域解决方案

  • JSONP
  • CORS
  • 代理服务器

JSONP

// 前端
$.ajax({
    method: 'get',
    url: '/classes',
    dataType: 'jsonp', // 解决跨域
    success(msg){}
})
// 后端
router.get('/', async function (req, res, next) {
    const data = await getClasses()
    res.jsonp(data) // 解决跨域
});

缺点:只能解决 get 请求类型的跨域。

CORS

插件

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

使用步骤分为三步

  • 运行 npm install cors 安装中间件
  • 使用 const cors = require('cors') 导入中间件
  • 在路由之前调用 app.use(cors()) 配置中间件

代码

//设置CORS
app.all('*', function (req, res, next) {
  //当允许携带cookies此处的白名单不能写’*’
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  //允许的请求头
  res.header('Access-Control-Allow-Headers', 'X-Requested-With,Content-Type, Origin,Accept,Authorization');
  //允许的请求方法
  res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS, PUT');
  //允许携带cookies
  res.header('Access-Control-Allow-Credentials', true);
  next();
});

代理服务器

image-20230207184131212

全局安装应用生成工具

npm i express-generator -g

1.创建代理服务器

以下命令,在指定目录中创建项目

express 项目名

2.安装依赖

// 进入项目
cd 项目名
// 安装依赖
npm install

3.安装中间层

代理服务器作为客户端和目标服务器之间的中间层,当客户端将请求发送到代理服务器后,代理服务器需要借助http-proxy-middleware中间件,在把请求代理转发到目标服务器

npm i http-proxy-middleware

4.修改服务器端口

代理服务器和目标服务器的端口不能相同,代理服务器端口在bin/www文件中修改

5.配置请求转发

将代理服务器中的请求转发到目标服务器,两种方式:

  • 在代理服务器中接收到请求后,通过 http-proxy-middleware 中间件直接转发至目标服务器
  • 在代理服务器中接收到请求后,完成一些额外的操作(例如密码加密处理)在通过 request-promise 模块转发至目标服务器
  1. http-proxy-middleware 转发

找到项目应用根目录中的app.js文件,在该文件中引入 http-proxy-middleware

const { createProxyMiddleware } = require('http-proxy-middleware')
const option = {
  // 目标服务器
  target: 'http://localhost:3000',
  // 默认 false 是否需要改变原始主机头为目标 URL
  changeOrigin: true,
  // 重写请求
  pathRewrite: {
    // 所有以 '/api'开头的请求,会重写为 '/'
    '^/api': '/',
  }
}
var app = express();
// 这行必须在express下面 proxy(options)即创建代理
app.use('/api',createProxyMiddleware(option))
  1. request-promise 转发

虽然客户端的大部分请求我们都可以通过代理服务器 http-proxy-middleware 中间件转发至目标服务器,但是类似于登录注册这种特殊请求,我们需要先在代理服务器处理(例如密码加密),然后发送到目标服务器,那么这个时候http-proxy-middleware中间件的自动转发就不可用了

因此这种情况下我们选择另一种模块--request-promise来处理我们的请求转发

安装依赖

npm i request request-promise

什么地方使用就调用

const rp = require('request-promise')
router.post('/login', async (req, res, next) => {
  // 。。。进行密码加密操作
  const data = await rp({
    method: 'post',
    uri: 'http://localhost:3000/users/login',
    body: req.body,
    json: true
  })
  res.send(data)
})