Node.js学习笔记__李立超课程

145 阅读25分钟

Node.js

  • Node.js是一个构建在V8引擎之上的JavaScript运行环境。(可以让JS在浏览器以外的地方运行,比如可以直接在vscode中运行.js文件)
  • 采用了单线程,且通过异步的方式来处理并发的问题。(Java采用的是多线程,服务器成本高)
  • 特点:
    • 单线程(只有一个服务员)、异步(服务多个客户)、非阻塞(不会等待某个操作完成后再执行下一步操作)
    • 统一API
客户端服务器数据库
需要服务,向服务器发起请求收到请求,提供线程为客户端服务,向数据库发起请求,返回给客户端将数据传输到服务器
  • 安装好nodejs之后可以直接在命令行中执行JS代码

  • 使用node运行js文件:

    • 在指定目录下的命令行中输入 node 地址
    • 在vscode中按F5,选择nodejs执行(在调试控制台中)
  • node.js和 JavaScript有什么区别:

    ECMAScript(node有) DOM(node没有) BOM(node没有)

安装工具Nvm

  • 可以在不同的node版本之间进行切换
  • 使用:
    • github.com/coreybutler…
    • nvm version:查看版本
    • nvm node_mirror https://npmmirror.com/mirrors/node/:配置镜像服务器
    • nvm install lts/版本号:安装node
    • nvm use 版本:指定要使用的node版本

模块化

Node.js核心模块和CommonJS模块系统是Node.js特有的功能,而ES模块化系统是JavaScript语言本身的特性。Node.js同时支持这两种模块化系统。

ES模块化

  • 使用ES的模块化,有两种方法:( node默认的模块化标准是CommonJS

    • 使用mjs作为扩展名
    • 修改package.json: "type": "module" ( 当前项目下所有的js文件都默认为es module )
  • 导出

    // 向外部导出内容
    export let a = 10
    export const c = { name: "猪八戒" }
    
    // 设置默认导出, 一个模块中只有一个默认导出
    export default function sum(a, b) {
        return a + b
    }
    
    let d = 20
    export default d 
    
  • 引入

    • 通过ES导入的内容都是常量,不可修改
    • ES模块都是在严格模式下运行的
    • ES模块化,在浏览器中同样支持,但通常都会结合打包工具使用
    // 导入m4模块,es模块不能省略扩展名(官方标准)
    import { a, b, c } from "./m4.mjs"
    
    // 通过as来指定别名
    import { a as hello, b, c } from "./m4.mjs"
    
    // 开发时要尽量避免import * 情况(webpack会把所有内容全部打包,性能变差)
    import * as m4 from "./m4.mjs"
    
    // 导入模块的默认导出, 可以随意命名
    import sum, { a } from "./m4.mjs"
    

CommonJS

node中默认支持的模块化规范。在CommonJS中,一个js文件就是一个模块

Commonjs规范

  • 引入模块

    • 使用require("模块的路径")函数来引入模块
    • 可以通过变量module查看相关模块
    • 引入自定义模块
      • 模块名要以./ 或 ../开头
      • 扩展名可以省略,如果省略.js,则node会自动补全,如果没有对应js文件则去找json文件
      • .cjs:默认是CommonJS模块
      • 引入文件夹作为模块时,默认引入index文件
    • 引入核心模块(node自带的模块)
      • 直接写核心模块的名字即可
      • 也可以在核心模块前添加 node:
    const m1 = require("./m1")
    // 两者等价
    const path = require("path")
    const path = require("node:path")
    
    const {name, age, gender} = require("./m3") // 解构
    
  • 导出模块

    • 定义模块时,模块中的内容默认不能被外部看到,可以通过exports设置向外部暴露的内容
    • 访问exports的方式:
      • exports
      • module.exports
    // 可以通过exports 一个一个的导出值
    exports.a = "孙悟空"
    exports.b = {name:"白骨精"}
    exports.c = function fn(){
        console.log("哈哈")
    }
    
    // 也可以直接通过module.exports同时导出多个值
    module.exports = {
        a: "哈哈",
        b: [1, 3, 5, 7],
        c: () =>{
            console.log(111)
        }
    }
    

原理

  • 所有的CommonJS的模块都会被包装到一个函数中
(function(exports, require, module, __filename, __dirname) {
    // 模块代码会被放到这里
});
console.log(arguments) // 检验是否被包在函数里

console.log(__filename) // __filename表示当前模块的绝对路径
console.log(__dirname) // 当前模块所在目录的路径

核心模块

  • JS创立之初是为了在浏览器上运行,js原生的模块是ES模块化,不需要任何框架(比如nodejs)就可以在浏览器中使用;
  • JS发展,可以使用不同框架在不同场景下运行,nodejs是其中一种,可以使JS在非浏览器的场景之外运行。nodejs提供一种模块化标准是CommonJS。
  • 在ES标准下,全局对象的标准名应该是 globalThis,因为在浏览器中可以使用,所以浏览器的globalThis是window;而对于nodejs,它也属于js,所以也有全局对象,所以给全局对象改名为global,但 global === globalThis
  • 核心模块,是node中自带的模块,可以在node中直接使用(像浏览器中带的BOM、DOM一样)

    有些东西是window中的,但node觉得不错就保留下来了,比如setTimeout等

    JS浏览器(JS)服务器(nodeJS)
    ECMAscript + DOM + BOM自定义模块 + 核心模块
    globalThis = windowglobalThis = global

process

  • 表示当前的node进程,通过该对象可以获取进程的信息,或者对进程做各种操作
  • process是一个全局变量,可以直接使用
  • 属性和方法:
    • process.exit():结束当前进程,终止node
    • process.nextTick(callback[, …args]):将函数插入到 tick队列中
      • tick队列中的代码,会在微任务队列和宏任务队列中任务之前执行(node中独有,浏览器没有)

path

  • 通过path可以用来获取各种路径,使用时需引入
  • path.resolve():用来生成一个绝对路径
    1. 如果直接调用resolve,则返回当前的工作目录
      • 通过不同的方式执行代码时,它的工作目录是有可能发生变化的(f5执行的时候找的是node目录,而在控制台node找的是03_核心模块目录)
    2. 将一个相对路径作为参数,则resolve会自动将其转换为绝对路径
      • 此时根据工作目录的不同,它所产生的绝对路径也不同
    3. 一般会将一个绝对路径作为第一个参数,一个相对路径作为第二个参数,这样它会自动计算出最终的路径
const path = require("node:path") // 引入

const result = path.resolve() //直接返回当前目录的地址
const result = path.resolve("./hello.js") // 返回hellojs的绝对路径

// 在使用路径时,尽量通过path.resolve()来生成路径
const result = path.resolve(__dirname, "./hello.js")

fs : File System

  • 帮助node操作磁盘中的文件,使用时需引入

  • 文件操作也就是所谓的I/O,input output

  • 使用fs有四种方式:同步、异步、Promise的异步、async/await的异步

    const fs = require("node:fs/promises") // 引入
    
    //readFileSync() 同步的读取文件的方法,会阻塞后边代码的执行
    // 当我们通过fs模块读取磁盘中的数据时,读取到的数据总会以Buffer对象的形式返回,Buffer是一个临时用来存储数据的缓冲区
    const buf = fs.readFileSync(path.resolve(__dirname, "./hello.txt"))
    
    // readFile() 异步的读取文件的方法
    fs.readFile(
        path.resolve(__dirname, "./hello.txt"),
        (err, buffer) => 
        {
            if (err) {
                console.log("出错了~")
            } else {
                console.log(buffer.toString())
            }
        }
    )
    
    //Promise版本的fs方法
    fs.readFile(path.resolve(__dirname, "./hello.txt"))
        .then(buffer => {
            console.log(buffer.toString())
        })
        .catch(e => {
            console.log("出错了~")
        })
    
    //通过async和await
    ; (async () => {
        try {
            const buffer = await fs.readFile(path.resolve(__dirname, "./hello.txt"))
            console.log(buffer.toString())
        } catch (e) {        
            console.log("出错了~~")
        }
    })()
    
  • fs的方法:

    • fs.readFile() 读取文件

    • fs.appendFile() 没有就创建,有就把数据添加到已有文件中

      // 复制一个文件
      fs.readFile("C:\\Users\\lilichao\\Desktop\\图片\\jpg\\an.jpg")
          .then(buffer => {
      
              return fs.appendFile(
                  path.resolve(__dirname, "./haha.jpg"),
                  buffer
              )
          })
          .then(() => {
              console.log("操作结束")
          })
      
    • fs.mkdir() 创建目录

      • mkdir可以接收一个 配置对象作为第二个参数,通过该对象可以对方法的功能进行配置
      • recursive 默认值为false,设置true以后,会自动创建不存在的上一级目录
      fs.mkdir(path.resolve(__dirname, "./hello/abc"), { recursive: true })
          .then(r => {
              console.log("操作成功~")
          })
          .catch(err => {
              console.log("创建失败", err)
          })
      
    • fs.rmdir() 删除目录

    • fs.rm() 删除文件

    • fs.rename() 重命名

      fs.rename(
          path.resolve(__dirname, "../an.jpg"),
          path.resolve(__dirname, "./an.jpg")
      ).then(r => {
          console.log("重命名成功")
      })
      
    • fs.copyFile() 复制文件

包管理器

npm

  • node中的包管理局叫做npm(node package manage)
  • npm由以下三个部分组成:
    • npm网站 www.npmjs.com/
    • npm CLI(Command Line Interface 即 命令行)(通过npm的命令行,可以在计算机中操作npm中的各种包(下载和上传等))
    • 仓库(仓库用来存储包以及包相关的各种信息)
  • npm在安装node时已经一起安装,可以在命令行中输入npm -v来查看npm是否安装成功
  • npm镜像:
    • 修改npm仓库地址:npm set registry registry.npmmirror.com
    • 还原到原版仓库:npm config delete registry
    • 查看当前仓库地址:npm config get registry

package.json

包的描述文件, node中通过该文件对项目进行描述,每一个node项目必须有package.json

  • 命令行

    • npm init 初始化项目,创建package.json文件(需要回答问题)
    • npm init -y 初始化项目,创建package.json文件(所有值都采用默认值)
    • npm install 包名 将指定包下载到当前项目中(简写npm i)
      • install时发生了什么?
        1. 将包下载当前项目的node_modules目录下
        2. 会在package.json的dependencies属性中添加一个新属性,"lodash": "^4.17.21"
        3. 会自动添加package-lock.json文件(帮助加速npm下载的,不用管)
    • npm install 自动安装所有依赖(传输文件时不需要传node_module,只要将依赖项在packagejson中写好,直接运行npm i即可)
    • npm install 包名 -g 全局安装(全局安装是将包安装到计算机中,通常都是一些工具)
    • npm uninstall 包名 卸载
  • 包含的字段

    • name(必备):包的名称,可以包含小写字母、_和-

    • version(必备):包的版本,需要遵从x.x.x的格式

      • 版本从1.0.0开始
      • 修复错误,兼容旧版(补丁)1.0.1、1.0.2
      • 添加功能,兼容旧版(小更新)1.1.0
      • 更新功能,影响兼容(大更新)2.0.0
    • script:自动脚本

      • 可以自定义一些命令,定义以后可以直接通过npm来执行这些命令

        {
            "scripts": {
                "test": "xxx",
                "start": "xxx"
            },
        }
        
      • start 和 test 可以直接通过npm start npm test执行

      • 其他自定义命令需要通过npm run xxx执行

  • 引入从npm下载的包时,不需要书写路径,直接写包名即可

yarn

pnpm

模块nodemon

  • 自动监视代码的修改,代码修改以后可以自动重启服务器

  • 使用方式:

    • 全局安装

      • npm i / r nodemon -g

      • yarn global add / remove nodemon

        同yarn进行全局安装时,默认yarn的目录并不在环境变量中,需要手动将路径添加到环境变量中

      • 启动:

        nodemon:运行index.js

        nodemon xxx:运行指定的js

    • 在项目中安装(开发依赖而不是项目依赖,会在packagejson中添加)

      • npm i nodemon -D

        yarn add nodemon -D

      • 启动:npx nodemon /xxx

网络通信与协议基础

  • 在浏览器中输入地址以后发生了什么?
    1. DNS解析,获取网站的ip地址(一个服务器,存储域名所对应的ip地址)
    2. 浏览器需要和服务器建立连接(tcp/ip)(三次握手)
    3. 向服务器发送请求(http协议)
    4. 服务器处理请求,并返回响应(http协议)
    5. 浏览器将响应的页面渲染
    6. 断开和服务器的连接(四次挥手)

TCP/IP协议族

  • TCP/IP协议族中包含了一组协议,这组协议规定了互联网中所有的通信的细节
  • 网络通信的过程由四层组成
    • 应用层:软件的层面,浏览器、服务器都属于应用层
    • 传输层:负责对数据进行拆分,把大数据拆分为一个一个小包
    • 网络层:负责给数据包,添加信息(盖戳)
    • 数据链路层:传输信息
  • HTTP协议就是应用层的协议,用来规定客户端和服务器间通信的报文格式的
  • TCP/IP协议是传输层的协议,在传输层提供可靠的、面向连接的数据传输。
    • 客户端如何和服务器建立(断开)连接
      • 三次握手建立连接
        • 客户端向服务器发送连接请求:SYN
        • 服务器收到连接请求,向客户端返回消息:SYN ACK
        • 客户端向服务器发送同意连接的信息:ACK
      • 四次挥手断开连接
        • 客户端向服务器发送请求,通过之服务器数据发送完毕,请求断开来接:FIN
        • 服务器向客户端返回数据,知道了:ACK
        • 服务器向客户端返回数据,收完了,可以断开连接:FIN ACK
        • 客户端向服务器发数据,可以断开了:ACK
      • 请求和响应实际上就是一段数据,只是这段数据需要遵循一个特殊的格式,这个特殊的格式由HTTP协议来规定

HTTP

HTTP(hypertext transport protocol)超文本传输协议,规定了浏览器和万维网服务器之间互相通信的规则

URL

  • 统一资源定位符,用于标识和定位互联网上资源的地址

www.example.com:8080/path/to/res…

  • 组成成分:

    • 协议名 + 域名 + 端口 + 路径 + 查询字符串 + 锚点
      • 协议名:http ftp ...
      • 域名 domain:网络中存在着无数个服务器,每个服务器都有它自己的唯一标识,这个标识被称为ip地址 192.168.1.17,但是ip地址不方便记忆,域名就相当于是ip地址的别名

报文(message)

  • 浏览器服务器之间通信是基于请求和响应的

    • 浏览器向服务器发送请求报文(request)
    • 服务器向浏览器返回响应报文(response)
    • 浏览器向服务器发送请求相当于浏览器给服务器写信,服务器向浏览器返回响应,相当于服务器给浏览器回信,这个信在HTTP协议中就被称为报文
    • 而HTTP协议就是对这个报文的格式进行规定
  • 服务器

    1. 可以接收到浏览器发送的请求报文
    2. 可以向浏览器返回响应报文
  • 报文格式

    请求报文请求报文
    请求首行响应首行
    请求头响应头
    空行空行
    请求体响应体
请求报文
  • 请求首行:GET /index.html?username=xxx HTTP/1.1

    • 第一部分:请求方式

      • get请求主要用来向服务器请求资源
      • post请求主要用来向服务器发送数据
    • 第二部分:表示请求资源的路径,? 后边的内容叫做查询字符串

      • 查询字符串是一个名值对结构,用&分割(username=admin&password=123123)
      • get请求通过查询字符串将数据发送给服务器,由于查询字符串会在浏览器地址栏中直接显示,所以安全性较差;由于url地址长度有限制,所以get请求无法发送较大的数据
      • post请求通过请求体来发送数据,无法在地址栏直接查看,所以安全性较好,请求体的大小没有限制,可以发送任意大小的数据
    • 第三部分:HTTP/1.1 协议的版本

  • 请求头

    • 请求头也是名值对结构,用来告诉服务器我们浏览器的信息
    • 每一个请求头都有它的作用:
      • Accept 浏览器可以接受的文件类型
      • Accept-Encoding 浏览器允许的压缩的编码
      • User-Agent 用户代理,它是一段用来描述浏览器信息的字符串
  • 空行:用来分隔请求头和请求体

  • 请求体:post请求通过请求体来发送数据

响应报文
  • 请求首行:HTTP/1.1 200 OK
    • 200 响应状态码,ok 对响应状态码的描述
      • 1xx 请求处理中
      • 2xx 表示成功
      • 3xx 表示请求的重定向
      • 4xx 表示客户端错误
      • 5xx 表示服务器的错误
  • 响应头:
    • 响应头也是一个一个的名值对结构,用来告诉浏览器响应的信息
      • Content-Type 用来描述响应体的类型
      • Content-Length 用来描述响应体大小
  • 空行:用来分隔请求头和请求体
  • 响应体:服务器返回给客户端的内容

Express

  • express是node中的服务器软件,通过express可以快速的在node中搭建一个web服务器

  • 使用步骤:

    1. 创建并初始化项目:yarn init -y / npm init -y

    2. 安装express:yarn add express / npm i express

    3. 创建index.js 并编写代码

    4. 启动服务器:app.listen(端口号) 用来启动服务器

      服务器相当于一台大的计算机,里面有很多程序,端口号就相当于程序的编号,当我执行app.listen(3000)时,如果有客户端访问3000端口,则由此express服务器进行接待,相当于当前服务器只为端口3000进行服务

      const express = require("express")
      
      // 获取服务器的实例(对象)
      const app = express()
      
      app.listen(3000, () => {
          console.log("服务器已经启动~")
      })
      

路由

  • 路由可以根据不同的请求方式和请求地址来处理用户的请求
  • app.METHOD(...):METHOD 可以是 get 或 post ...
  • 路由功能:读取用户的请求(request)、根据用户的请求返回响应(response)
  • /hello hello
    • 在路由路径中以斜杠开头,表示一个绝对路径。当客户端发送 GET 请求到 /hello 路径时,对应的处理函数会被执行。
    • 在路由路径中没有斜杠开头,表示一个相对路径。当客户端发送 GET 请求到任意路径时,只要请求的路径末尾与 hello 匹配,就会执行对应的处理函数。当客户端请求/foo/hello/bar/hello 或者 /hello 等路径时,由于路径末尾匹配到了 hello,对应的处理函数会被执行。
// "/" 相当于根目录http://localhost:3000
// 路由的回调函数执行时,会接收到三个参数,第一个 request 第二个 response 
app.get("/", (req, res) => 
{
    console.log("有人访问我了~") // 服务器的输出 
    console.log(req) // req 表示的是用户的请求信息,通过req可以获取用户传递数据

    // 可以通过res来向客户端返回数据
    // sendStatus和send都会使响应结束
    res.status(200) //status() 用来设置响应状态码,但是并不发送
    res.sendStatus(404) //sendStatus() 向客户端发送响应状态码
    res.send("<h1>这是我的第一个服务器</h1>") //send() 设置并发送响应体
})
  • 配置错误路由

    // 可以在所有路由的后边配置错误路由
    app.use((req, res) => {
        // 只要这个中间件一执行,说明上边的地址都没有匹配
        res.status(404)
        res.send("<h1>您访问的地址已被外星人劫持!</h1>")
    })
    

中间件

  • app.use:定义中间件,作用、用法和路由很像
  • express的所有功能都是通过中间件扩展的
  • 和路由的区别:
    • 会匹配所有请求(post get都执行)
    • 路径设置父目录
// 路径只要包含/hello就可以 省略和"/"一样
app.use("/hello", (req, res) => {
    console.log("收到请求");
    res.send("中间件")
})

// next() 是回调函数的第三个参数,它是一个函数,调用函数后,可以触发后续的中间件
// next() 不能在响应处理完毕后调用
app.use((req, res, next) => {
    console.log("111", Date.now())
    // res.send("<h1>111</h1>")
    next() // 放行,我不管了~~
})

app.use((req, res, next) => {
    console.log("222", Date.now())
    res.send("<h1>222</h1>")
})
  • 中间件的使用:用中间件检查权限
const express = require('express');
const app = express();
// 自定义的权限检查中间件
const checkPermission = (req, res, next) => {
  const isAuthenticated = true; // 假设这里是通过某种方式进行权限检查(例如用户登录)
  if (isAuthenticated) {
    next();// 权限通过,继续下一个中间件或路由处理程序
  } else {
    res.status(401).send('Unauthorized');// 权限未通过,发送错误响应
  }
}
// 路由A
app.get('/routeA', (req, res) => {
  res.send('This is route A');
});
// 路由B
app.get('/routeB', (req, res) => {
  res.send('This is route B');
});
// 应用中间件来进行权限检查
app.use(checkPermission);
// 以下路由将会在通过权限检查后才被执行
app.get('/protected', (req, res) => {
  res.send('Protected route');
});
app.listen(3000, () => {
  console.log('Server is running on port 3000');
});

get

  • 静态资源:服务器中的代码,对于外部来说都是不可见的。浏览器若想访问,则需要将页面所在的目录设置为静态资源目录

  • get请求传递参数的方式:

    1. 查询字符串

    2. param

      • 在路径中以冒号命名的部分我们称为param,在get请求它可以被解析为请求参数

      • param传参一般不会传递特别复杂的参数

    • 约定优于配置:通过param传参约定好,直接传递值就可以
<form action="/login" method="get"></form>

<script>
/* 
    希望用户返回根目录时,可以给用户返回一个网页
*/
// 设置static中间件后,浏览器访问时,会自动去public目录寻找是否有匹配的静态资源
// 访问public下的index.html时,不需要写public http://localhost:3000/index.html
// f5执行的是大目录,而node执行的是小目录,大目录下没有public所以无法访问
app.use(express.static(path.resolve(__dirname, "./public")))

app.get("/", (req, res) => 
{
    res.send("怎么办呢?")
})

app.get("/login", (req, res) => {
    // 获取到用户输入的用户名和密码
    // req.query 表示查询字符串中的请求参数
    console.log("请求已经收到~~")

    // 验证用户输入的用户名和密码是否正确
    if(req.query.username === "admin" && req.query.password === "123123"){
        res.send("<h1>登录成功!</h1>")
    }else{
        res.send("<h1>用户名或密码错误!</h1>")
    }
})
    
// /hello/:id 表示当用户访问 /hello/xxx 时就会触发
// app.get("/hello/:name/:age/:gender", (req, res) => {
app.get("/hello/:name", (req, res) => {
    // 在网址中输入/hello/aaa,此时name的值就是aaa,可以通过req.params属性来获取这些参数
    res.send("<h1>这是hello路由</h1>")
})
</script>

post

  • req.body:可以获取请求体中的参数
    • 使用req.body时需要引入解析请求体的中间件
const express = require("express")
const app = express()
const path = require("path")

// public相当于http://localhost:3000/
app.use(express.static(path.resolve(__dirname, "public")))
// 引入解析请求体的中间件
app.use(express.urlencoded())

// /login -->  http://localhost:3000/login
app.get("/login", (req, res) => {
    if (req.query.username === "admin" && req.query.password === "123123") {
        res.send("<h1>欢迎您回来!登录成功</h1>")
    } else {
        res.send("<h1>用户名或密码错误!</h1>")
    }
})

app.post("/login", (req, res) => {
    // 通过req.body来获取post请求的参数(请求体中的参数)
    // 默认情况下express不会自动解析请求体,需要通过中间件来为其增加功能
    const username = req.body.username
    const password = req.body.password

    if (username === "admin" && password === "123123") {
        res.send("<h1>登录成功</h1>")
    } else {
        res.send("<h1>登录失败</h1>")
    }
})

app.listen(3000, () => {
    console.log("服务器已经启动~")
})

ejs模板引擎

模板:像网页,但是可以嵌入变量。ejs是node中的一款模板引擎

  • js端使用步骤:

    1. 安装ejs npm i ejs

    2. 配置express的模板引擎为ejs:app.set("view engine", "ejs")

    3. 配置模板路径:app.set("views", path.resolve(__dirname, "views"))

      • 第一个参数只能是views
    4. 渲染:res.render("students", { 变量 })

      • res.render 方法用于将指定的视图渲染为 HTML 响应并发送给客户端
      • 第一个参数是视图的文件路径或视图名称
      • 第二个参数是一个可选的对象,用于传递给视图的数据。可以将一个对象传递,这样在模板中可以访问到对象中的数据
    // 将ejs设置为默认的模板引擎
    app.set("view engine", "ejs")
    // 配置模板的路径:配置的名字和配置的路径
    app.set("views", path.resolve(__dirname, "views"))
    
    app.get("/students", (req, res) => {
        res.render("students", { name })
    })
    
  • ejs使用:

    • <%= %>:在网页中展示内容,它会自动对字符串中的特殊符号进行转义,为了避免 xss 攻击
    • <%- %> :直接将内容输出,不进行转义
    • <% %>:可以在其中直接编写js代码,js代码会在服务器中执行(在服务器渲染,不是在客户端,代码是在nodejs中执行的)
    • <%# %>:ejs注释
    <%# 可以在服务端判断,在客户端展示 %>
    <% if(name === "孙悟空") {%>
    <h2>大师兄来了</h2>
    <%} else{%>
    <h2>二师兄来了</h2>
    <%}%>
    
  • 例子:创建一个添加学生信息的路由

    • res.redirect():它将客户端(浏览器)重定向到一个新的URL,用来发起请求重定向(post请求)
    app.post("/add-student", (req, res) => {
        // 生成一个id
        const id = STUDENT_ARR.at(-1).id + 1
    
        // 1.获取用户填写的信息
        const newUser = {id, name:req.body.name}
    
        // 2. 验证用户信息(略)
    
        // 3. 将用户信息添加到数组中
        STUDENT_ARR.push(newUser)
    
        // 4. 返回响应
        // 直接在添加路由中渲染ejs,会面临表单重复提交的问题,所以使用重定向
        // res.render("students", { stus: STUDENT_ARR })
    
        // 将新的数据写入到json文件中
        fs.writeFile(
            path.resolve(__dirname, "./data/students.json"),
            JSON.stringify(STUDENT_ARR)
        ).then(()=>{
            res.redirect("/students")
        }).catch(()=>{
            // ....
        })
    })
    

Router

  • Router是express中创建的一个对象,实际上是一个中间件,可以在该中间件上绑定各种路由和其他的中间件

  • app是服务器的实例,只能有一个,但是express可以有多个

    const express = require("express")
    const app = express()
    const router = express.Router()
    router.get("/hello", ()=> {})
    app.use(router)
    
  • 由于router和app不是同一个对象,所以router可以不写在index.js中,方便拆分管理路由

    • app.use("/user", userRouter) ----> /user/list
    • app.use("/good", userRouter) ----> /good/list
    • 当此时两个路由中都有某个路径时,会自动在路径前添加所指定的路径(该js中包含的所有路由),防止冲突
    // user.js
    const express = require("express")
    const router = express.Router()
    router.get("/list", (req, res) => {res.send("hello 我是list")})
    module.exports = router // 将router暴露到模块外
    
    // index.js
    const express = require("express")
    const app = express()
    const userRouter = require("./routes/user") //导入路由
    app.use("/user", userRouter) // 使路由生效
    app.use("/xxx", require("xxx")) 
    

Cookie

  • HTTP协议是一个无状态的协议,服务器无法区分请求是否发送自同一个客户端。cookie是HTTP协议中用来解决无状态问题的技术,它的本质就是一个头

  • 服务器以响应头的形式将cookie发送给客户端,客户端收到以后会将其存储在浏览器中,并在下次向服务器发送请求时将其传回,这样服务器就可以根据cookie来识别出客户端了

  • 读取cookie:需要安装中间件来使得express可以解析cookie

    1. 安装cookie-parser:yarn add cookie-parser

    2. 引入:const cookieParser = require("cookie-parser")

    3. 设置为中间件:app.use(cookieParser())

    const cookieParser = require("cookie-parser")
    app.use(cookieParser())
    
    // 给客户端发送一个cookie
    app.get("/get-cookie", (req, res) => {
        res.cookie("username", "admin")
        res.send("cookie已经发送")
    })
    
    //浏览器已经收到cookie,此时需要携带cookie将数据发给服务器,后面的路由都携带cookie
    app.get("/hello", (req, res) => {
    
        // req.cookies 用来读取客户端发回的cookie
        console.log(req.cookies)
        res.send("hello路由")
    })
    
    
  • cookie有效期:默认情况下cookie的有效期就是一次会话(session),会话就是一次从打开到关闭浏览器的过程

    • maxAge: 用来设置cookie有效时间,单位是毫秒
    app.get("/set", (req, res) => {
    
        res.cookie("name", "sunwukong", {
            // expires:new Date(2022, 11, 7)
            maxAge: 1000 * 60 * 60 * 24 * 30
        })
        res.send("设置cookie")
    })
    
  • cookie一旦发送给浏览器就不能再修改,但是可以通过发送新的同名cookie来替换旧cookie,从而达到修改的目的

    app.get("/delete-cookie", (req, res) => {
        res.cookie("name", "", {
            maxAge: 0
        })
        res.send("删除Cookie")
    })
    
  • cookie的不足:

    • cookie是由服务器创建,浏览器保存,每次浏览器访问服务器时都需要将cookie发回,导致不能在cookie存放较多的数据
    • cookie是直接存储在客户端,容易被篡改盗用,不会在cookie存储敏感数据
  • 希望将用户的数据统一存储在服务器中,每一个用户的数据都有一个对应的id,只需通过cookie将id发送给浏览器,浏览器只需每次访问时将id发回,即可读取到服务器中存储的数据,这个技术我们称之为session(会话)

Session

id(cookie) ----> session对象

  • session:服务器中的一个对象,这个对象用来存储用户的数据,每一个session都会有一个唯一的id,id会通过cookie的形式发送给客户端,客户端每次访问时只需将存储有id的cookie发回即可获取它在服务器中存储的数据

  • session会先检查客户端有没有cookie,有的话则可以访问该session内的东西

  • 使用步骤:

    1. 安装:yarn add express-session
    2. 引入:const session = require("....")
    3. 设置为中间件:app.use(session({...}))
    const session = require("express-session")
    
    // 设置session中间件
    app.use(
        session({
            secret: "hello",
            cookie: {
                maxAge: 1000
            }
        })
    )
    
    app.get("/set", (req, res) => {
        req.session.username = "sunwukong"
        res.send("查看session")
    })
    
    app.get("/get",(req, res) => {
        const username = req.session.username
        console.log(username)
        res.send("读取session")
    })
    
  • 默认有效期是一次会话

    • session失效:

      • 第一种:浏览器的cookie没了,浏览器会话时间到导致cookie消失
      • 第二种:服务器中的session对象没了,服务器重启导致session丢失
    • express-session默认是将session存储到内存中的,服务器一旦重启session会自动重置,所以使用session通常会对session进行一个持久化的操作(写到文件或数据库)

      • 如果将session存储到本文件中:

        • 引入一个中间件session-file-store

        1. 安装:yarn add session-file-store
        2. 引入:const FileStore = require("session-file-store")(session)
        3. 设置为中间件
      // 引入file-store
      const FileStore = require("session-file-store")(session)
      
      app.use(
          session({
              store: new FileStore({
                  // path用来指定session本地文件的路径
                  path: path.resolve(__dirname, "./sessions"),
                  secret: "哈哈" // 加密
                  // session的有效时间 秒 默认1个小时 需要同时设置ttl和cookie中的maxAge
                  ttl: 10,
                  // 默认情况下,fileStore会每间隔一小时,清除一次session对象
                  // reapInterval 用来指定清除session的间隔,单位秒,默认 1小时
                  reapInterval: 10
              }),
              secret: "dazhaxie"
          })
      )
      
      app.get("/logout", (req, res) => {
          // 使session失效
          req.session.destroy(() => {
              res.redirect("/")
          })
      })
      

CSRF攻击

  • 跨站请求伪造(Cross-Site Request Forgery,CSRF)。攻击者会事先构造一条HTTP请求,其中包含了攻击者想要执行的非法操作,比如更改用户密码、发表恶意评论等。接着,攻击者诱使用户进入恶意网站或者点击特定链接,使得用户的浏览器自动发出这条构造好的恶意请求。由于用户已经在目标网站上进行了身份验证,因此目标网站会错误地认为这是用户本人发送的合法请求,从而执行了攻击者预期的操作。

  • 现在大部分的浏览器的都不会在跨域的情况下自动发送cookie

  • 解决

    1. 使用referer头(请求报文里的头)来检查请求的来源

      router.use((req, res, next) => {
          // 获取一个请求头referer
          const referer = req.get("referer")
          if(!referer || !referer.startsWith("http://localhost:3000/")){
              res.status(403).send("你没有这个权限!")
              return
          }
          // 登录以后,req.session.loginUser是undefined
          if (req.session.loginUser) {
              next()
          } else {
              res.redirect("/")
          }
      })
      
    2. 使用验证码

    3. 尽量使用post请求(结合token)

      • **token(令牌)**可以在创建表单时随机生成一个令牌,将令牌存储到session中,并通过模板发送给用户,用户提交表单时,必须将token发回,才可以进行后续操作(可以使用uuidnpm i uuid来生成token)
      // 引入uuid
      const uuid = require("uuid").v4
      
      // 学生列表的路由
      router.get("/list", (req, res) => {
          // 生成一个token
          const csrfToken = uuid()
      
          // 将token添加到session中
          req.session.csrfToken = csrfToken
      
          req.session.save(() => {
              res.render("students", {
                  stus: STUDENT_ARR,
                  username: req.session.loginUser,
                  csrfToken
              })
          })
      })