Node知识点
Node.js是一个基于Chrome V8引擎的JavaScript运行环境,即运行在服务端的js,用来编写服务器;特点:单线程、异步、非堵塞I/O模式,统一API。
堵塞I/O:进程发起I/O系统调用后,若不能获得资源,则进程会被阻塞挂起,被挂起后将进入休眠状态(放弃CPU),直到资源准备好了,进程才会被唤醒
非堵塞I/O:进程发起I/O系统调用后,若不能立即获得资源,则进程立即返回一个错误(或进程不断地查询),而不会被阻塞挂起。
一、Node.js和Java的区别?
1、java是编程语言;node.js是js在服务端的运行环境;
2、java web服务器是多线程的,每个请求由一个单独的线程来处理,多线程允许在不排队的情况下同时执行多个任务,适合CPU密集型应用;
node.js是单线程的,所有请求都在一个线程中处理,事件被放置在队列中,排队处理。node.js为CPU密集型应用提供了"工作线程"的解决方案…等…
二、Node.js和JavaScript的区别?
ECMAScript、BOM(node没有)、DOM(node没有)
三、同步和异步
3.1 进程和线程
进程(厂房):程序的运行环境;
线程(工人):线程是实际进行运算的东西。
3.2 同步
通常情况代码都是自上向下一行一行执行的;前面的代码不执行后面的代码不会执行;同步的代码执行会出现堵塞;一行代码执行慢会影响到整个程序的执行。
解决同步问题:Java通过多线程来解决;Node.js通过异步方式解决。
3.3 异步
不会堵塞其他代码执行,一行代码慢不会影响其他程序执行。
function sum(a, b) {
const begin = Date.now();
setTimeout(()=> {
return a + b;
}, 10000)
}
console.log('1111111');
const result = sum(123,567)
console.log(result); // result接收sum()的结果,sum()里面没有返回值,所以返回undefined
console.log('222222')
异步问题:无法通过return来设置返回值。
解决异步问题:通过回调函数来返回结果。
function sum(a, b, cb) {
const begin = Date.now();
setTimeout(()=> {
cb(a + b);
}, 10000)
}
console.log('1111111');
const result = sum(123,567, result => {
sum(result, 777, result => {
sum(result, 888, reesult => {
console.log(result)
})
})
}) // result接收sum()的结果,sum()里面没有返回值,所以返回undefined
console.log('222222')
异步调用必须依赖回调函数返回结果带来的问题:代码可读性差,可调试性差,回调地狱问题。
解决:使用Promise对象解决。
四、Promise
Promise是一个存储数据的对象,可以用来存储异步调用的数据,解决同步地狱问题。
// 创建Promise时,构造函数中需要一个函数作为参数
const promise = new Promise((resolve, reject) => {
// resolve,reject是两个函数,通过这两个函数可以向Promise中存储数据
// 通过函数来向Promise中添加数据,好处就是可以用来异步添加异步调用的数据
// resolve在执行正常是存储数据,编写处理数据的代码; reject在执行错误是存储数据,编写处理异常的数据
resolve('resolve返回数据');
// reject('reject返回数据')
})
// console.log(promise)
// 通过Promise的实例方法then来读取Promise中存储的数据,需要两个回调函数作为函数,回调函数用来获取Promise中的数据
promise.then((result)=>{
console.log('111', result)
}, (reason) => {
console.log('222', reason)
})
4.1 Promise维护了两个隐藏属性
1、PromiseResult:用来存储数据;
2、PromiseState:记录Promise的状态——pending(进行中)、fulfilled(完成,通过resolve存储数据时)、rejected(出错或通过reject存储数据时);state只能修改一次,修改以后永远不会再变。
流程:当Promise创建时,PromiseState初始值为pending,当通过resolve存储数据时,PromiseState变为fulfilled,PromiseResult变为存储的数据。当通过reject存储数据或出错时,PromiseState变为rejected,PromiseResult变为存储的数据或异常数据。
then:当我们通过then读取数据时,相当于为Promise设置了回调函数:如果PromiseState变为fulfilled,则调用then的第一个回调函数来返回数据;如果PromiseState变为rejected,则调用then的第二个回调函数来返回数据。
catch:用法和then相似,只需要返回一个回调函数作为参数,专门处理Promise异常的方法。catch()中的回调函数只会在Promise被拒绝时才调用,catch相当于then(null, reason => {})。
promise.catch(reason => {
console.log(333333)
})
finally:无论是正常存储数据还是异常处理数据,finally总会执行,但是finally的回调函数中不会接收到数据;通常用来编写无论成功与否都要执行的代码。
promise的then、catch、finally这三个方法都会返回一个新的Promise,Promise中会存储回调函数的返回值;finally的返回值,不会存储到新的Promise中。
promise.catch(reason => {
console.log(666666)
})
回调地狱问题解决
function sum(a, b) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(a + b)
}, 100)
}) // 返回一个Promise
}
sum(123, 567).then(result => {
return result + 777
}).then(result => {
return result + 888
}).then(result => {
console.log(result)
})
Promise链式调用读取的是上一步的执行结果,如果上一步的执行结果不是当前想要的结果,则跳过当前的方法。
const promise = new Promise((resolve, reject) => {
reject('xixi')
})
promise.then(result => console.log('oneone', result))
.catch(result => {
console.log('error', result);
return 'haha'
})
.then(result => console.log('twotwo', result))
// 第一个then跳过
// error xixi
// twotwo haha
对于Promise出现异常时,而整个链中没有出现catch,则异常向外抛出。Promise是个“甩锅侠”,catch只会处理前面抛出的错误,自己的错误由后边的处理,所以通常将catch写到最后,统一处理所有错误。
4.2 Promise的静态方法
1、Promise.resolve():创建一个立即执行的Promise;
2、Promise.rejecr():创建一个立即拒绝的Promise;
Promise.resolve(666).then(result => console.log(result)) // 666
// 等价于
new Promise((resolve,reject) => {
resolve(666)
}).then(result => console.log(result)) // 666
Promise.rejecr('error').catch(result => console.log(result))
3、Promise.all(iterable):同时返回对个Promise的执行结果(iterable一个可迭代对象,如Array、String、Set、Map),其中有一个错误就会返回错误。
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1,promise2,promise3])
.then((values) => {
console.log(values);
});
// [3, 42, 'foo']
4、Promise.allSettled(iterable):Promise.all()仅有当全部Promise都完成时才会返回有效数据,而Promise.allSettled()用法和all一致,但是它里边无论Promise是否完成都会返回数据,只是他会根据不同的状态返回不同的数据。
5、Promise.race():返回执最快的Promise,不考虑对错。
6、Promise.aany():只会返回第一个成功的Promise,如果所有的Promise都失败才会返回一个错误信息。
4.2 手写Promise
五、微任务和宏任务
js是单线程,运行时基于事件循环机制(EventLoop)
执行栈:放的是要执行的代码(栈是一种数据结构,“后进先出”);
任务队列:当执行栈中的代码执行完毕后,队列中才会按照顺序一次进入栈中执行。
在js中任务队列有两个:
宏任务队列(setTimeout()、大部分任务都是宏任务)
微任务队列(Promise的回调函数[then、catch、finally]…等),queueMicrotask()用来向微任务队列中添加一个任务
六、async和await
通过async可以快速创建异步函数,异步函数的返回值会自动封装到一个Promise中,使用await关键字来调用异步函数。
当通过await去调用以异步函数时,它会暂停异步函数内部await后的代码运行,直到得到异步代码执行完成时,才将结果返回。通过await处理异步代码时,需要通过try…catch来处理异常。
注意:await只能用于async声明的异步函数中,或者es模块的顶级作用域中。
async function fn1() {
console.log(1)
}
// 等价于
function fn2() {
return new Promise(resolve => {
console.log(1)
resolve()
})
}
function sum(a, b) {
return new Promise(resolve => {
setTimeout(() => {
resolve (a + b)
}, 100)
})
}
async function fn() {
try{
let result = await sum(123,567);
result = await sum(result, 678);
result = await sum(result, 789);
console.log(result)
} catch(e) {
console.log('出错了~')
}
}
fn()
七、模块化
7.1 CommonJS模块化
模块简单理解其实就是一个代码片段,本来写在一起的JS代码,我们按照不同的功能将它拆分为一个一个小的代码片段,这个代码片段就是一个模块。简单来说,就是化整为零。
代码被拆分后更容易被复用,同样一个模块可以在不同的项目中使用,大大的降低了我们的开发成本。
早期使用script标签来引入多个js文件,jQuery也可以理解为一个模块,引入之后实际效果是向全局作用域中添加了一个变量$(或jQuery)这样很容易出现模块间互相覆盖的情况;并且当我们使用了较多的模块时,有一些模块是相互依赖的,必须先引入某个组件再引入某个组件,模块才能够正常工作。比如jQuery和jQueryUI,就必须先引入jQuery,如果引入顺序出错将导致jQueryUI无法使用。所以通过script标签实现模块化无法选择引入模块的哪些内容,在复杂的模块场景下也容易出错等一系列问题
CommonJs是Node.js中默认支持的模块化规范;
模块就是一个js文件,在模块内部任何变量或其他对象都是私有的,不会暴露给外部模块,模块与模块之间相互独立。
在定义模块化时,模块中的内容默认是不能被外部看到的,可以通过exports来设置要向外暴露的内容;访问exports的方式有exports(一个一个导出值)、module.exports(同时导出多个值、整体导)。
CommonJS规范
引入模块:
使用require(“模块路径”)函数来引入模块
引入自定义模块时要以./或…/开头 (否则)
文件 扩展名可以省略(在CommonJS中,如果省略js文件的扩展名,node会自动补全扩展名,如果没有.js,找.json,也没有找.node)
引入核心模块
直接写核心模块即可 (const path = require(“path”))
也可以在核心模块前添加node: (const path = require(“node:path”))
引入文件夹时,直接引入文件夹名就行,文件夹的默认入口文件是index.js
所有的CommonJS模块都被包装到一个函数里
(function(exports, require, module, __filename, __dirname){
// 代码会被写入这里
})
// __filename表示当前文件所在的绝对路径
// __dirname 表示当前模块所在位置
所以我们之所以能在CommonJS模块中使用exports、require并不是因为它们是全局变量。它们实际上以参数的形式传递进模块的。
默认情况下,Node.js会将以下内容视为CommonJS模块:
使用.cjs为扩展名的文件
当前的package.json的type属性为commonjs时,扩展名为.js的文件
当前的package.json不包含type属性时,扩展名为.js的文件
文件的扩展名是mjs、cjs、json、node、js以外的值时(type不是module时)
require()是同步加载模块的方法,所以无法用来加载ES6的模块。当我们需要在CommonJS中加载ES模块时,需要通过import()方法来加载。
import("./m2.mjs").then(m2 => {
console.log(m2)
})
7.2 ES模块化
2015年随着ES6标准的发布,ES的内置模块化系统也应运而生,并且在Node.js中同样也支持ES标准的模块化.
使用mjs作为文件名扩展名
修改package.json将模块化规范设置为ES模块
设置"type": “module” 当前项目下所有的js文件都默认为es module
导出
// 导出变量(命名导出)
export let name1, name2, …, nameN;
export let name1 = …, name2 = …, …, nameN;
// 导出函数(命名导出)
export function functionName(){...}
// 导出类(命名导出)
export class ClassName {...}
// 导出一组
export { name1, name2, …, nameN };
// 重命名导出
export { variable1 as name1, variable2 as name2, …, nameN };
// 解构赋值后导出
export const { name1, name2: bar } = o;
// 默认导出
export default expression;
export default function (…) { … } // also class, function*
export default function name1(…) { … } // also class, function*
export { name1 as default, … };
// 聚合模块
export * from …; // 将其他模块中的全部内容导出(除了default)
export * as name1 from …; // ECMAScript® 2O20 将其他模块中的全部内容以指定别名导出
export { name1, name2, …, nameN } from …; // 将其他模块中的指定内容导出
export { import1 as name1, import2 as name2, …, nameN } from …; // 将其他模块中的指定内容重命名导出
export { default, … } from …;
导入
// 引入默认导出
import defaultExport from "module-name";
// 将所有模块导入到指定命名空间中
import * as name from "module-name";
// 引入模块中的指定内容
import { export1 } from "module-name";
import { export1 , export2 } from "module-name";
// 以指定别名引入模块中的指定内容
import { export1 as alias1 } from "module-name";
import { export1 , export2 as alias2 , [...] } from "module-name";
// 引入默认和其他内容
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
// 引入模块
import "module-name";
开发时应尽量避免import * 情况
默认导出的内容,可以随意命名,一个文件中只一个默认导出export default
es模块都是运行在严格模式下
ES模块化,在浏览器中同样支持,但是通常我们不会直接使用,通常都会结合打包工具使用
7.3 核心模块
核心模块是node中自带的模块,可以在node中直接使用;
window是浏览器的寄主对象,node没有的;
global是node的全局对象,作用类似于window;
ES标准下,全局对象的标准名是 globalThis。
7.3.1 process
process模块:
表示当前的node进程;
通过该对象可以获取进程的信息,或者对进程做各种操作。
如何使用:
process是一个全局变量,可以直接使用
属性和方法:
process.exit()结束当前进程,终止node;
process.nextTick(callback[, …args]) 将函数插入到tick队列,tick队列中的代码,会在下一次事件循环之前执行,会在微任务队列和宏任务队列之前执行。
setTimeout(() => {
console.log(1)
}) // 宏任务队列
queueMicrotask(() =>{
console.log(2)
}) // 微任务队列
process.nextTick(() =>{
console.log(3)
}) // 向tick任务队列中添加任务。
console.log(4) //执行栈
// 4 3 2 1
7.3.2 path
path模块:
表示路径,通过path可以获取各种路径;
要使用path, 需要先对其进行引入。
如何使用:
path.resolve([…paths])用来生成一个绝对路径
相对路径:./xxx …/xxx ;
绝对路径:在计算机本地C:\XXX /user/xxxx ; 在网络中 http://www.xxxx/…
如果直接调用resolve,则返回当前的工作目录
注意:通过不同方式执行时,工作目录可能不同
如果将一个相对路径作为参数,则resolve会自动将其转化为绝对路径,此时工作目录不同,所产生的绝对路径不同
__diename表示当前目录所在的绝对路径,不会因为调用方式的改变,路径不会改变。
consr result = path.resolve(__diename, "./hello.js")
1
7.3.3 fs
fs模块:
帮助node来操作磁盘的文件;
文件操作就是所谓的I/O;
使用fs模块,需要引入。
如何使用:
fs.readFileSync() 同步读取文件的方法,会堵塞后续代码的执行;通过fs读取磁盘中的数据时,读取到的数据总是会以Buffer(临时用来存储数据的缓冲区)对象形式返回;(不推荐使用)
fs.readFile()异步读取文件的方法;
fs.appendFile() 创建新文件,或将数据添加到已有文件中;
fs.mkdir() 创建目录
fs.rmdir() 删除目录
//fs.readFile()异步读取文件的方法
const fs = require("node:fs")
const path = require("node:path")
fs.readFile(
path.resolve(path.resolve(__diename, "./hello.txt")),
(err, buffer) => {
if(err) {
console.log('出错啦')
} else {
console.log(buffer.toString())
}
) // 第二个参数回调函数将结果返回
// Promise版本的fs的方法
const fs = require("node:fs/promise")
const path = require("node:path")
fs.readFile(path.resolve(__diename, "./hello.txt"))
.then(buffer => {
console.log(buffer.toString())
})
.catch(e => {
console.log("出错啦")
});
// async await版本fs的方法
const fs = require("node:fs")
const path = require("node:path")
(async () => {
try{
const buffer = await fs..readFile(path.resolve(__diename, "./hello.txt");
console.log(buffer.toString())
} catch(e) {
console.log("出错啦")
}
})();
八、npm包管理器
node中的包管理局叫做npm,npm是世界上最大的包管理库。(jQuery这种外部代码在项目中,我们将其称之为包)
npm由以下三个部分组成:
npm网站 (通过npm网站可以查找包,也可以管理自己开发提交到npm中的包。(https://www.npmjs.com/>)
npm CLI(Command Line Interface 即 命令行)(通过npm的命令行,可以在计算机中操作npm中的各种包(下载和上传等))
仓库(仓库用来存储包以及包相关的各种信息)
package.json包的描述文件,node通过对该文件的对项目进行描述,每个node必须有package.json;
因为node插件包相对来说非常庞大,所以不加入版本管理,将配置信息写入package.json并将其加入版本管理,其他开发者对应下载即可。
name(必备):包的名称,可以包含小写字母、_和-
version(必备):包的版本,需要遵从x.x.x的格式
版本从1.0.0开始;修复错误,兼容旧版(补丁)1.0.1、1.0.2;添加功能,兼容旧版(小更新)1.1.0;更新功能,影响兼容(大更新)2.0.0
author:包的作者,格式:Your Name email@example.com
description: 包的描述;
repository:仓库地址(git);
scripts:自动脚本
通常情况下,我们的自己创建的每一个node项目,都可以被认为是一个包。都应该为其创建package.json描述文件。同时,npm可以帮助我们快速的创建package.json文件。只需要进入项目并输入npm init即可进入npm的交互界面,只需根据提示输入相应信息即可。
8.1 安装
// 例如:
// 安装lodash这个包
npm install lodash // 在终端输入
// 1、它会自动连接npm服务器,将最新的loadsh包下载到项目的node_modules目录下,如果目录不存在下载包时会自动创建。
// 2、修改package.json文件,在dependencies字段中将刚刚下载的包设置为依赖项
"dependencies": {
"lodash": "^4.17.21"
}
// 3、自动添加package-lock.json文件,主要是用来记录当前项目的下包的结构和版本的,提升重新下载包的速度,以及确保包的版本正确。
// `"^4.17.21"`表示匹配最新的4.x.x的版本,也就是如果后期lodash包更新到了4.18.1,我们的包也会一起更新,但是如果更新到了5.0.0,我们的包是不会随之更新的
// `"~4.17.21"`,~表示匹配最小依赖,也就是4.17.x
// `"*"`则表示匹配最新版本,即x.x.x(不建议使用)。当然也可以不加任何前缀,这样只会匹配到当前版本。
//在安装时直接指定,要安装的包的版本,像是这样
npm install lodash@3.2.0
npm install lodash@"> 3.2.0"
如果不希望,包出现在package.json的依赖中,可以添加–no-save指令禁止
npm install lodash --no-save
// 通过-D或–save-dev,将其添加到开发依赖
npm install lodash -D
// 全局安装:将包安装到计算机中,可以通过命令行在任何地方调用它,通常安装的是一些工具。
npm i lodash -g
//卸载
npm uninstall xxx
8.2 配置镜像
8.2.1 npm/cnpm
npm的服务器位于国外,有时访问速度会比较慢,可以通过配置国内镜像来解决该问题,配置代码:
npm install -g cnpm --registry=https://registry.npmmirror.com
上边的指令会为计算机安装一个名为cnpm的新指令,该指令的功能和npm相同,不同点在于它会通过国内的镜像服务器下载包(npm是node官方的包管理器,cnpm是个中国版的npm)。
使用cnpm后,计算机中便同时存储在cnpm和npm两个命令,可以根据需要选择使用.
直接使用npm时访问的就是国内的npm镜像服务器
npm set registry https://registry.npmmirror.com
如果想恢复到原版的配置
npm config delete registry
8.2.1 yarn
期的npm存在有诸多问题,不是非常的好用。yarn的出现就是为了帮助我们解决npm中的各种问题,当然现在的npm相较于之前的已经得到了很大的优化,所以你完全可以选择不使用yarn。
在新版本的node中,corepack中已经包含了yarn,可以通过启用corepack的方式使yarn启用。首先执行以下命令启用corepack:
corepack enable
查看yarn版本
yarn -v
切换yarn版本,最新版:
corepack prepare yarn@stable --activate
九、Express
基于Node.js平台,快速、开放、极简的 Web 开发框架,通过express可以快速搭建在node中的一个web服务器。
// 引入
const express = require('express')
// 获取服务器实例 一切皆对象
const app = express()
// 启动服务器 3000端口号(虚拟的) 端口类似门牌号
// 路由的回调参数执行时,会收到三个参数request(用户请求报文) response(服务发送给客户端的响应信息) next(是一个函数,调用函数后,会调用后续中间件,不能在响应处理完毕后调用)
app.get("/", (req, res)=>{
console.log("有人访问我了");
// console.log(req.url)
// console.log(res.sendStatus(404))
res.send('服务请求成功')
})
app.listen(3000, ()=>{
console.log("启动了");
})
中间件:在express中使用app.use来定义一个中间件,中间件类似路由,用法很像。
app.use((req, res) => {
console.log("111")
// res.send('这是通过中间件返回的响应')
next()
})
app.use((req, res) => {
console.log("222")
res.send('这是通过中间件返回的响应')
})
nodemon:在检测到目录中的文件更改时通过自动重新启动节点应用程序来帮助开发基于 node.js 的应用程序。
全局安装
npm i nodemon -g
// 启动命令 nodemon
项目中安装
npm i nodemon -D
// 启动
npx nodemon
服务器中的代码,对于外部来说都是不可见的,所以说我们写的HTML页面,浏览器是无法直接访问的,所以说希望浏览器直接访问,则需要将页面所在的目录设置为静态资源目录。
const path = require("path");
app.use(express.static(path.resolve(__dirname, "../public")))
// 静态资源都放到public目录下才能访问
简单登录实现
// public/index.html
<form action="/login" method="post">
<div>用户名
<input type="text" name="username">
</div>
<div>密码
<input type="password" name="password">
</div>
<div>
<input type="submit" value="登录" id="">
</div>
</form>
// index.js
const exp = require('constants')
const express = require('express')
const app = express()
const path = require('path')
app.use(express.static(path.resolve(__dirname, './public')))
// 默认情况下,express不会自动解析请求体 引入解析请求体的中间价
// app.use(express.urlencoded())
app.get("/login", (req, res) => {
console.log(req.query); // 表示查询字符串的请求参数
if (req.query.username === "xixi" && req.query.password === "123123") {
res.send('<h1>登录成功!</h1>')
}
else {
res.send('<h1>用户名或密码错误!</h1>')
}
})
app.post("/login", (req, res) => {
// res.send('<h1>post请求收到了哦!</h1>')
console.log(req.body) // 获取请求体中的参数
const username = req.body.username;
const password = req.body.password;
if (username === "xixi" && password === "123123") {
res.send('<h1>登录成功!</h1>')
}
else {
res.send('<h1>用户名或密码错误!</h1>')
}
})
///hello/:id 表示用户访问必须携带一个参数 params参数
app.use('/hello/:id', (req, res) => {
// 约定优于配置
console.log(req.params)
res.send('<h1>小世界大视野</h1>')
})
app.listen(3000, () => {
console.log('服务器启动成功了唔')
})
十、模板引擎
为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
node中存在有限个的模板引擎,ejs是node的一款模板引擎(数据和模板合并然后生成HTML文本)
// 1.安装
npm i ejs
// 2.配置express模板引擎
app.set('view engine', 'ejs')
// 3.配置模板路径
app.set('views', path.resolve(__dirname, "views"))
// 注意:模板引擎需要被express渲染后才能使用
res.render() // 用来渲染一个模板引擎,并将其返回给浏览器;可以将一个对象作为render的第二个参数传递,这样就可以在模板中访问到对象中的数据
与vue不同ejs是服务器端渲染,服务器端完成渲染,输出HTML文件,返回给浏览器,而vue默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。
十一、Router模块化路由
Router路由是express4.x后加入的api,这个方便了我们项目的解藕,通过模块化路由我们可以将应用中不同的功能封装成一个个的模块。
Router实际上也是个中间件,可以在该中间件上去绑定各种路由以及其他中间件。
const express = require('express')
const router = express.Router()
router.get('hello', (req, res) => {
res.send('Hello World!')
})
app.use(router) // 中间件生效
十二、客户端存储
12.1 cookie
http是一个无无状态的协议,服务器无法区分是否发送自同一个客户端,cookie是HTTP协议中用来解决无状态的技术方案。服务器以响应头的形式将cookie发送给客户端,客户端收到后会将其存储,并在下次向服务器发送请求时将其传回,这样服务器就可以根据cookie来识别出客户端了。
// 需要安装中间件来是的express可以解析cookie
npm i cookie-parser
// 引入
const cookieParser = require('cookie-parser')
// 设置中间件
app.use(cookieParser())
app.get('/set', (req, res) => {
res.cookie('name', 'didi', {
//expires: new Date(2022, 11, 25) // 设置有效期到2022-12-25 expires不灵活
maxAge: 1000 * 60 * 60 * 24 * 30 // 用来设置cookie有效时间,单位是毫秒
})
res.send('成功设置cookie')
})
app.get('/get', (req, res) => {
const name = req.cookies.name
console.log(name);
res.send("读取cookie")
})
app.get('/delete', (req, res) => {
// cookie 一旦发送给浏览器就不能修改了
// 但是可以通过发送信息的cookie来替换旧的cookie,从而达到修改目的
res.cookie("name", "", {
maxAge: 0
})
res.send('删除cookie')
})
cookie不足:cookie是由服务器创建的,浏览器保存,每次浏览器访问服务器时都会将所有cookie作为请求头发送,所以如果在cookie越大,请求完成时间越长。cookie是明文存储在客户端,安全性差,容易被篡改盗用。
所以我们希望可以将用户数据存储在服务器中,每一个用户的数据都有一个对应的id,只需通过cookie将id发送给浏览器,浏览器每次访问只需将id发回,即可读取服务器中存储的数据
12.2 Session
session是服务器中的一个对象,用来存储用户数据,每一个session对象都有一个唯一的id,每一个id会通过cookie的形式发送给客户端,客户端每次访问只需要将存储有id的cookie发回即可获得它在服务器中的数据。
在express中可以通过express-session中间件来实现session的功能
npm i express-session
const session = require("express-session")
app.use(session({
secret: "hellohellohowareyou", // 密钥
}))
app.get('/set', (req, res) => {
res.cookie('name', "didi", {
maxAge: 1000 * 60 * 60 * 24 * 30
})
// console.log(req.session);
req.session.name = 'didi'
res.send('查看session')
})
app.get('/get', (req, res) => {
// console.log(req.cookies);
// session是通过存了个connect.sid的方式来与客户端会话的,不过session是存在内存中的
const name = req.cookies.name
res.send("读取session中数据")
})
当浏览器第一次访问服务器时,服务器创建一个session对象(该对象有一个唯一的id,一般称之为sessionId),服务器会将sessionId 以cookie的方式发送给浏览器;当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据 sessionId就可以找到对应的session对象。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session;然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
session不足:
服务端保存大量数据,增加服务端压力;
客户端请求依赖服务端,多次请求必须访问同一台服务器
12.3 CSRF跨站请求伪造
CSRF就是攻击者盗用了你的身份,以你的名义发送恶意请求。
解决方法:
使用referer头来检查请求的来源
使用验证码
尽量使用post请求(结合token)
token(令牌)可以在创建表单时随机生成一个令牌(可以使用uuid),然后将令牌存储到session中,必须将token发回,才可以进行后续操作。
当客户端第一次请求服务时,服务端对用户进行信息认证(登录);认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证;以后每次请求,客户端都携带认证的token;服务的对token进行解密,判断是否有效。