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/版本号:安装nodenvm 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 = window globalThis = global
process
- 表示当前的node进程,通过该对象可以获取进程的信息,或者对进程做各种操作
- process是一个全局变量,可以直接使用
- 属性和方法:
process.exit():结束当前进程,终止nodeprocess.nextTick(callback[, …args]):将函数插入到 tick队列中- tick队列中的代码,会在微任务队列和宏任务队列中任务之前执行(node中独有,浏览器没有)
path
- 通过path可以用来获取各种路径,使用时需引入
path.resolve():用来生成一个绝对路径- 如果直接调用resolve,则返回当前的工作目录
- 通过不同的方式执行代码时,它的工作目录是有可能发生变化的(f5执行的时候找的是node目录,而在控制台node找的是03_核心模块目录)
- 将一个相对路径作为参数,则resolve会自动将其转换为绝对路径
- 此时根据工作目录的不同,它所产生的绝对路径也不同
- 一般会将一个绝对路径作为第一个参数,一个相对路径作为第二个参数,这样它会自动计算出最终的路径
- 如果直接调用resolve,则返回当前的工作目录
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时发生了什么?
- 将包下载当前项目的node_modules目录下
- 会在package.json的dependencies属性中添加一个新属性,"lodash": "^4.17.21"
- 会自动添加package-lock.json文件(帮助加速npm下载的,不用管)
- install时发生了什么?
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 startnpm 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
-
-
网络通信与协议基础
- 在浏览器中输入地址以后发生了什么?
- DNS解析,获取网站的ip地址(一个服务器,存储域名所对应的ip地址)
- 浏览器需要和服务器建立连接(tcp/ip)(三次握手)
- 向服务器发送请求(http协议)
- 服务器处理请求,并返回响应(http协议)
- 浏览器将响应的页面渲染
- 断开和服务器的连接(四次挥手)
TCP/IP协议族
- TCP/IP协议族中包含了一组协议,这组协议规定了互联网中所有的通信的细节
- 网络通信的过程由四层组成
- 应用层:软件的层面,浏览器、服务器都属于应用层
- 传输层:负责对数据进行拆分,把大数据拆分为一个一个小包
- 网络层:负责给数据包,添加信息(盖戳)
- 数据链路层:传输信息
- HTTP协议就是应用层的协议,用来规定客户端和服务器间通信的报文格式的
- TCP/IP协议是传输层的协议,在传输层提供可靠的、面向连接的数据传输。
- 客户端如何和服务器建立(断开)连接
- 三次握手建立连接
- 客户端向服务器发送连接请求:SYN
- 服务器收到连接请求,向客户端返回消息:SYN ACK
- 客户端向服务器发送同意连接的信息:ACK
- 四次挥手断开连接
- 客户端向服务器发送请求,通过之服务器数据发送完毕,请求断开来接:FIN
- 服务器向客户端返回数据,知道了:ACK
- 服务器向客户端返回数据,收完了,可以断开连接:FIN ACK
- 客户端向服务器发数据,可以断开了:ACK
- 请求和响应实际上就是一段数据,只是这段数据需要遵循一个特殊的格式,这个特殊的格式由HTTP协议来规定
- 三次握手建立连接
- 客户端如何和服务器建立(断开)连接
HTTP
HTTP(hypertext transport protocol)超文本传输协议,规定了浏览器和万维网服务器之间互相通信的规则
URL
- 统一资源定位符,用于标识和定位互联网上资源的地址
-
组成成分:
- 协议名 + 域名 + 端口 + 路径 + 查询字符串 + 锚点
- 协议名:http ftp ...
- 域名 domain:网络中存在着无数个服务器,每个服务器都有它自己的唯一标识,这个标识被称为ip地址 192.168.1.17,但是ip地址不方便记忆,域名就相当于是ip地址的别名
- 协议名 + 域名 + 端口 + 路径 + 查询字符串 + 锚点
报文(message)
-
浏览器和服务器之间通信是基于请求和响应的
- 浏览器向服务器发送请求报文(request)
- 服务器向浏览器返回响应报文(response)
- 浏览器向服务器发送请求相当于浏览器给服务器写信,服务器向浏览器返回响应,相当于服务器给浏览器回信,这个信在HTTP协议中就被称为报文
- 而HTTP协议就是对这个报文的格式进行规定
-
服务器
- 可以接收到浏览器发送的请求报文
- 可以向浏览器返回响应报文
-
报文格式
请求报文 请求报文 请求首行 响应首行 请求头 响应头 空行 空行 请求体 响应体
请求报文
-
请求首行:
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 表示服务器的错误
- 200 响应状态码,ok 对响应状态码的描述
- 响应头:
- 响应头也是一个一个的名值对结构,用来告诉浏览器响应的信息
- Content-Type 用来描述响应体的类型
- Content-Length 用来描述响应体大小
- 响应头也是一个一个的名值对结构,用来告诉浏览器响应的信息
- 空行:用来分隔请求头和请求体
- 响应体:服务器返回给客户端的内容
Express
-
express是node中的服务器软件,通过express可以快速的在node中搭建一个web服务器
-
使用步骤:
-
创建并初始化项目:
yarn init -y/npm init -y -
安装express:
yarn add express/npm i express -
创建index.js 并编写代码
-
启动服务器:
app.listen(端口号)用来启动服务器- 协议名://ip地址:端口号/路径
服务器相当于一台大的计算机,里面有很多程序,端口号就相当于程序的编号,当我执行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,对应的处理函数会被执行。
- 在路由路径中以斜杠开头,表示一个绝对路径。当客户端发送 GET 请求到
// "/" 相当于根目录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请求传递参数的方式:
-
查询字符串
-
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端使用步骤:
-
安装ejs npm i ejs
-
配置express的模板引擎为ejs:
app.set("view engine", "ejs") -
配置模板路径:
app.set("views", path.resolve(__dirname, "views"))- 第一个参数只能是views
-
渲染:
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/listapp.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
-
安装cookie-parser:
yarn add cookie-parser -
引入:
const cookieParser = require("cookie-parser") -
设置为中间件:
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内的东西
-
使用步骤:
- 安装:
yarn add express-session - 引入:
const session = require("....") - 设置为中间件:
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
- 安装:
yarn add session-file-store - 引入:
const FileStore = require("session-file-store")(session) - 设置为中间件
-
// 引入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
-
解决:
-
使用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("/") } }) -
使用验证码
-
尽量使用post请求(结合token)
- **token(令牌)**可以在创建表单时随机生成一个令牌,将令牌存储到session中,并通过模板发送给用户,用户提交表单时,必须将token发回,才可以进行后续操作(可以使用uuid
npm 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 }) }) }) - **token(令牌)**可以在创建表单时随机生成一个令牌,将令牌存储到session中,并通过模板发送给用户,用户提交表单时,必须将token发回,才可以进行后续操作(可以使用uuid
-