Node JS学习笔记

NodeJS

在学习Node JS前就必须了解为什么要学习它

  1. NodeJS 可以作为服务端,方便学过 JavaScript 的开发者上手开发一个自己的后端(服务端),甚至有公司要求掌握Node JS
  2. 实现了前后端的语法统一,有利于和前端代码整合,甚至共用部分代码
  3. Node JS性能好,生态系统活跃
  4. 其他有点我暂时想不到了,省略哈哈哈哈…

什么就来了解了解Node JS它是个什么东东

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。Node.js 使用了一个事件驱动、非阻塞式 I/O的模型,使其轻量又高效。Node.js 的包管理工具 npm 是全球最大的开源库生态系统。Node.js 不是一门语言,也不是 JavaScript 的框架,也不是像Nginx一样的Web服务器 ,Node.js 是 JavaScript 在服务器端的运行环境(平台)。

ps:浏览器是JavaScript的前端运行环境,Node.js 是JavaScript的后端运行环境;

那么它能干嘛呢?

Nodejs 作为一个JavaScript的运行环境,仅仅提供了基础的功能和API。然而,基于Node.js 提供的这些基础能,很多强大的工具和框架如雨后春笋,层出不穷,所以学会了Node.js,可以让前端程序员胜任更多的工作和岗位。

  • 基于 Express框架(http://www.expressjs.com.cn/),可以快速构建Web应用
  • 基于Electron框架(https://electronjs.org/),可以构建跨平台的桌面应用
  • 基于 restify框架(http://restify.com/),可以快速构建API接口项目
  • 读写和操作数据库、创建实用的命令行工具辅助前端开发、等…

下面呢,我们就从Node.js的内置模块讲起,来掌握Node.js的基本使用

基本使用(内置模块)

运行使用

先查看是否下载了Node JS,使用命令:node -v

image-20231009161031410

首先我们得先学会如何运行Node.js的代码(确保下载了NodeJS)

  1. 创建js文件,写入NodeJS代码(JavaScript)
  2. 打开命令行,输入node 文件名.文件后缀
  3. 回车运行

image-20231009161237359

Buffer

Buffer 中文译为缓冲区,是一个类似于数组 Array 的对象,用于表示固定长度的字节序列;换一句话说,Buffer 是一段固定长度的内存空间,用于处理二进制数据

image-20231009161626490

Buffer 的特点:

  1. 大小固定且无法调整
  2. 性能较好,可以直接对计算机的内存进行操作
  3. 每个元素的大小为1个字节(1 Bit)
创建Buffer

Buffer创建方法:

// 1. alloc() 创建初始化为0的10字节buffer 参数:字节
let buf = Buffer.alloc(10)
console.log(buf);

// 2. allocUnsafe() 创建未初始化的buffer
let buf_unsafe = Buffer.allocUnsafe(20)
console.log(buf_unsafe);

// 3. from() 将一个数组或者字符串转化为buffer形式
let buf_formString = Buffer.from('Hello')
let buf_formArr = Buffer.from([105,108,111,118,101,121,111,117])
console.log(buf_formString);
console.log(buf_formArr);

运行结果:

image-20231009163228108

其中,alloc() 和from() 是常用的创建方式,因为安全;allocUnsafe() 创建方式的速度较快,但数据为初始化

读写Buffer
let buf_formArr = Buffer.from([105,108,111,118,101,121,111,117])
// 打印buffer的字符串形式
console.log(buf_formArr.toString());
// 获取buffer数据及修改
console.log(buf_formArr[3]);
buf_formArr[3] = 97 // 修改
console.log(buf_formArr.toString());

结果:

image-20231009163825853

若给Buffer 添加溢出内容,如给Buffer 的某个字节写入361,但每个字节最大表示为255,就会产生溢出(舍弃高位)

buf_formArr[3] = 361 // 溢出: 0001 0110 1001 => 0110 1001(105)
console.log(buf_formArr);

运行结果:

image-20231009164324902

若给Buffer 赋值中文,会给到3个字节,因为2个字节表示一个中文

let buf_form = Buffer.from('你好')
console.log(buf_form);

结果:

image-20231009164602013

fs 模块

fs 模块是Node JS内置用来操作文件的模块,提供了一些列方法和属性,用来满足用户对文件的操作需求。它可以实现与硬盘的交互如:文件的创建,修改,删除,重命名以及移动等操作

在使用前需要先引入fs 模块:

const fs = require('fs')

下面来介绍它的使用吧

写入文件
方法名说明
writeFile异步写入
writeFileSync同步写入
appendFile / appendFileSync追加写入
createWriteStream流式写入
writeFile方法
// 语法:
fs.writeFile(/* 文件路径及文件名 */,/* 写入数据 */,/* 配置对象(可选) */,/* 回调函数 */)
/*
配置对象内容:
{
	encoding: 'utf8',
	mode: '0666',
	flag: 'w',
}
其中flag最常用,它的值:
	'w' - 覆盖写入,从头开始写,文件内容覆盖
	'a' - 追加写入,从文件末尾开始写,不覆盖原本文件内容
*/

例:

// require 是 Node.js 环境中的'全局'变量,用来导入模块
const fs = require('fs');
//将 『生活不止眼前的苟且,还有诗和远方。』 写入到当前文件夹下的『座右铭.txt』文件中
fs.writeFile('./座右铭.txt', '生活不止眼前的苟且,还有诗和远方。', err => {
	//如果写入失败,则回调函数调用时,会传入错误对象,如写入成功,会传入 null
	if(err){
		console.log(err);
	return;
	}
	console.log('写入成功')});

结果:创建了文件,并写入了内容(若文件不存在,自动创建文件

image-20231012141242046

writeFileSync方法
// 语法:
fs.writeFileSync(/* 文件路径及文件名 */, /* 写入数据 */, /* 配置对象(可选) */)
/*
配置对象内容:
{
	encoding: 'utf8',
	mode: '0666',
	flag: 'w',
}
其中flag最常用,它的值:
	'w' - 覆盖写入,从头开始写,文件内容覆盖
	'a' - 追加写入,从文件末尾开始写,不覆盖原本文件内容
*/

例:

const fs = require('fs');
fs.writeFileSync('文件.txt', '同步写入~')

结果:

image-20231012141940431

附:同步写入和异步写入对比

同步写入是执行完才进行下一段代码的,异步写入同发送 Ajax 请求,是写入文件内容和执行下面代码同时执行的(程序不会等写入完成再进行下面的代码),下面给出个例子

const fs = require('fs');
// 代码进行到这会等同步写入完成后,再进行下面的代码
fs.writeFileSync('同步.txt', '同步写入~')
console.log('同步写入完成~'); // 在同步写入完成后执行
// 代码进行到这不会等待写入过程,写入的代码执行同时进行
fs.writeFile('异步.txt','异步写入~',() => {
	console.log('异步写入完成~'); // 在异步写入后执行
})
console.log('异步写入下面的代码'); // 在异步写入的同时执行

结果:

image-20231012142640000

可以看到,他们的区别就是同步写入会等待写入过程,而异步写入不会(因为它分出一个线程来进行写入,主线程继续执行代码)

Node.js 中的磁盘操作是由其他 线程 完成的,结果的处理有两种模式:

同步处理 JavaScript 主线程会等待其他线程的执行结果,然后再继续执行主线程的代码, 效率较低
异步处理 JavaScript 主线程 不会等待其他线程的执行结果,直接执行后续的主线程代码, 效率较好

appendFile / appendFileSync 方法

他们的使用差不多,都是往文件后面追加内容;

// 语法:
fs.appendFile(/* 文件路径及文件名 */, /* 写入数据 */,/* 配置选项(可选) */,/* 回调函数 */)
fs.appendFileSync(/* 文件路径及文件名 */,/* 写入数据 */,/* 配置选项 */)

他们的区别同writeFilewriteFileSync都是同步和异步的区别,给个栗子

const fs = require('fs');
fs.appendFile('./文件.txt','异步追加',() => {
	console.log('异步追加完成');
})
console.log('异步追加后的代码,准备进行同步追加');
fs.appendFileSync('./文件.txt','\n同步追加')
console.log('同步追加完成');

结果:

image-20231012143840015

image-20231012143829586

createWriteStream方法

该方法为流式写入,什么是流式写入呢?

流式写入就是打开文件后,一段一段得写入,确认写完后,再关闭文件

// 语法:
// 1.打开文件
let file = fs.createWriteStream(/* 文件路径及文件名 */, /* 配置对象(可选) */)
// 2.写入内容
file.write(/* 写入数据 */)
file.write(/* 写入数据 */)
//  ...
// 3.关闭文件(也可以不写,程序执行完成后自动关闭,但最好加)
file.end() // file.close() 也可以

例:

const fs = require('fs');
let file = fs.createWriteStream('./文件.txt')
file.write('第1段\n')
file.write('第2段\n')
file.write('第3段\n')
file.end()

结果:

image-20231012144948459

为什么要使用流式写入呢?

因为打开文件是需要耗费资源的,因为 I / O 操作是花费时间的;因此流式写入适合大文件的写入以及频繁操作文件的写入

读取文件
方法说明
readFile异步读取
readFileSync同步读取
createReadStream流式读取
readFile / readFileSync方法
// 语法:
fs.readFile(/* 文件路径及文件名 */,/* 配置对象(可选) */, /* 回调函数(err错误信息,data读取数据) */))
let data = fs.readFileSync(/* 文件路径 */,/* 配置对象 */)

栗子:

const fs = require('fs');
console.log('----异步读取----');
fs.readFile('./文件.txt',(err,data) => {
	if(err) throw err;
	console.log('异步读取完成,读取内容:\n',data);
	console.log('将Buffer转化成字符:',data.toString());
})
console.log('----同步读取----');
let data = fs.readFileSync('./文件.txt')
console.log('同步读取完成,读取内容:\n',data);
console.log('将Buffer转化成字符:',data.toString());

结果:

image-20231012152751551

createReadStream方法

该方法就是流式读取文件,也就是一段一段得读取文件

// 语法:
// 1.创建读取流对象
let rs = fs.createReadStream(/* 文件路径及文件名 */);
//绑定data事件,每次取出 64k 数据后执行一次 data 回调
rs.on('data', data => { });
//绑定end事件,读取完毕后, 执行 end 回调
rs.on('end', () => { })

栗子:

const fs = require('fs');
let rs = fs.createReadStream('./文件.txt');
rs.on('data', data => {
	console.log(data);
	console.log(data.toString());
	console.log(data.length);
 });
rs.on('end', () => { 
	console.log('读取完毕');
})

结果:

image-20231012154641416

流式读取有什么作用呢?

最大的作用就是提高读取效率,减少读取占用的内存空间,从而让程序占用的内存减少

栗子:

读取同一个文件

  • 使用 同步读取 / 异步读取 所占用的内存空间就是读取文件的大小,若文件大小为1GB,那么读取该文件就使用了1GB的内存空间,very占用资源;
  • 但若使用流式读取,那么读取该我呢见的内存空间就是64K,极大减少了程序的内存占用
文件重命名及移动

文件重命名也可以作为文件位置移动(剪切文件),方法如下

方法说明
rename异步重命名
renameSync同步重命名
// 语法:
fs.rename(/* 旧路径及文件名 */, /* 新路径及文件名 */, /* 回调函数 */)
fs.renameSync(/* 旧路径及文件名 */, /* 新路径及文件名 */)

直接上栗子:

const fs = require('fs');
// 将文件从 本路径 移动到 本路径/资源路径
// 方法1:同步移动
fs.renameSync('./文件.txt','./资源/文件改名啦.txt')
// 方法2:异步移动
fs.rename('./资源/文件改名啦.txt','./文件.txt',err => {
	if(err) throw err;
	console.log('完成移动');
})
console.log('完成重新移动');

结果:文件完成了移动以及重命名

image-20231012161213571

文件删除

有两个方法unlinkrm,没有什么区别

方法说明
unlink异步删除
unlinkSync同步删除
rm异步删除
rmSync同步删除
// 语法:
fs.unlink(/* 文件路径及文件名 */,/* 回调函数 */)
fs.unlinkSync(/* 文件路径及文件名 */)
fs.rm(/* 文件路径及文件名 */,/* 回调函数 */)
fs.rmSync(/* 文件路径及文件名 */)

直接拿去用就完事,栗子:

fs.rm('./文件.txt',err => {
	if(err) throw err
    console.log('删除成功~')
})
文件夹操作
方法说明
mkdir / mkdirSync创建文件夹
readdir / readdirSync读取文件夹
rmdir / rmdirSync删除文件夹
创建文件夹
// 语法:
fs.mkdir(/* 文件路径 */,/* 配置对象(可选) */, /* 回调函数(err错误信息) */)
fs.mkdirSync(/* 文件路径 */,/* 配置对象(可选) */)
/*
配置对象参数及默认值:{
	recursive: false, 
}
recursive表示是否递归创建,如创建文件夹./a/b/c
	若./a不存在,recursive为false,报错
    若./a不存在,recursive为true,创建文件夹 ./a 和 ./a/b 还有 ./a/b/c
*/

栗子:

const fs = require('fs');
//异步创建文件夹
fs.mkdir('./page', err => {
	if(err) throw err;
	console.log('创建成功');
});
//递归异步创建
fs.mkdir('./1/2/3', {recursive: true}, err => {
	if(err) throw err;
	console.log('递归创建成功');
});
//递归同步创建文件夹
fs.mkdirSync('./x/y/z', {recursive: true});

结果:

image-20231012163846386

读取文件夹
// 语法:
fs.readdir(/* 文件路径 */,/* 配置对象(可选) */, /* 回调函数(err错误信息,data读取数据) */)
fs.readdirSync(/* 文件路径 */,/* 配置对象(可选) */)

栗子:

image-20231012164241568

const fs = require('fs');
//异步读取
fs.readdir('./资源', (err, data) => {
	if(err) throw err;
	console.log(data);
});
//同步读取
let data = fs.readdirSync('./资源');
console.log(data);

结果:

image-20231012164250229

删除文件夹
// 语法:
fs.rmdir(/* 文件路径 */,/* 配置对象(可选) */, /* 回调函数(err错误信息) */)
fs.rmdirSync(/* 文件路径 */,/* 配置对象(可选) */)
/*
配置对象参数及默认值:{
	recursive: false, 
}
recursive表示是否递归创建,如创建文件夹./a/b/c
	若./a不存在,recursive为false,报错
    若./a不存在,recursive为true,创建文件夹 ./a 和 ./a/b 还有 ./a/b/c
*/

栗子:

const fs = require('fs');
//异步删除文件夹
fs.rmdir('./page', err => {
	if(err) throw err;
	console.log('删除成功');
});
//异步递归删除文件夹
fs.rmdir('./1', {recursive: true}, err => {
	if(err) {
		console.log(err);
	}
console.log('递归删除')
});
//同步递归删除文件夹
fs.rmdirSync('./x', {recursive: true})

image-20231012170008503

结果:

image-20231012170021604

注意:

  • 删除文件夹中,不能存在文件
查看文件资源状态
// 语法:
fs.stat(/* 文件路径 */,/* 配置对象(可选) */, /* 回调函数(err错误信息) */)
fs.statSync(/* 文件路径 */,/* 配置对象(可选) */)

栗子:

const fs = require('fs');
//异步获取状态
fs.stat('./data.txt', (err, data) => {
	if(err) throw err;
		console.log(data);
	});
//同步获取状态
let data = fs.statSync('./data.txt');
console.log(data);
/*
附:
查看是否为文件或文件夹:
	data.isFile()
	data.isDirectory()
返回值:true / false
*/

结果:

image-20231012170544474

常看的数据:

  • size 文件体积
  • birthtime 创建时间
  • mtime 最后修改时间
  • isFile 检测是否为文件
  • isDirectory 检测是否为文件夹
相对路径的小问题

在Node JS中使用相对路径有着小问题,就是Node JS中,相对路径的根路径是命令行打开的路径,而不是JS文件的路径;用代码来看:

dir/fs.js文件内容:

const fs = require('fs');
fs.writeFileSync('./1.txt','111')

现在目录情况是这样的

image-20231012171342617

从dir文件夹中打开命令行,运行fs.js,创建的1.txtdir目录下

image-20231012171445189

dir文件夹的上一层nodeJsPratice打开命令行,运行fs.js,在nodeJsPratice文件夹下出现1.txt

image-20231012171620539

这就导致Node JS中,相对路径不清晰的问题

相对路径中所谓的 当前目录 ,指的是 命令行的工作目录 ,而并非是文件的所在目录
所以当命令行的工作目录与文件所在目录不一致时,会出现一些 BUG

__dirname

__dirname 与 require 类似,都是 Node.js 环境中的’全局’变量

__dirname 保存着 当前文件所在目录的绝对路径 ,可以使用 __dirname 与文件名拼接成绝对路径

代码示例:

let data = fs.readFileSync(__dirname + '/data.txt');
console.log(data);

使用 fs 模块的时候,尽量使用 __dirname 将路径转化为绝对路径,这样可以避免相对路径产生的
Bug,但也阔以使用下一节的path模块~

Path 模块

path 模块是 Node.js 官方提供的用来处理路径的模块,它提供了一系列的方法和属性,用来满足用户对路径的处理需求;和fs模块一样,在使用前需要require引入

const path = require('path')

path模块的常用方法:

方法说明
resolve拼接规范的绝对路径
sep获取操作系统的路径分隔符(windows:\ ,Linux:/)
parse解析路径并返回对象
basename获取路径的文件名称
dirname获取路径的目录名
extname获取文件扩展名

栗子:

const path = require('path')
console.log('普通字符串路径拼接:' + __dirname + '/index.html')
console.log('resolve路径拼接:' + path.resolve(__dirname,'/index.html'))
console.log(path.sep)
console.log(__filename) // 和__dirname一样,但__filename保存着文件的绝对路径
console.log(path.parse(__filename))
console.log(path.basename(__filename))
console.log(path.dirname(__filename))
console.log(path.extname(__filename))

结果:

image-20231013090424843

Http 模块

Http 模块主要用于接收前端的请求进行响应,和前面的模块一样,都需要先进行引入再使用

const http = require('http');

创建一个基础的HTTP服务:

const http = require('http');
 
//1.创建服务对象 内部调用时接收两个实参
//request 对请求报文的封装对象
//response 对响应报文的封装
const server = http.createServer((request,response)=>{
    response.setHeader('content-type','text/html;charset=utf-8'); // 解决中文乱码
    response.end('hello') //设置响应体,并结束
});
 
//2.监听端口,启动服务
server.listen(9000,()=>{
    console.log('9000服务已经启动')
})

Tips:

  • 代码更新,必须重新启动服务才会生效;也可以使用nodemon,进行热部署,就不用重新启动
  • HTTP默认端口 80,HTTPS默认端口 443。HTTP开发常用端口3000,808,8090,900

运行后,浏览器访问127.0.0.1:9000

image-20231013091322902

阔以看到响应内容,这就完成了一个基本的http服务;

请求报文

前面的http服务过于简单,没有对请求报文内容进行判断,我们希望得到请求报文的内容,对不同内容进行不同的响应,就使用到request对象,它的基本属性

属性含义
method请求方法
httpVersion请求的http版本
url请求路径
headers请求头,返回一个对象,属性全转化为小写

注意事项:

  • request.url只能获取路径和查询字符串,无法获取URL中的域名及协议内容
  • request.headers 将请求信息转化成一个对象,并将属性都转化成小写
  • 关于路径:如果访问网站只填写了IP地址或者域名,此时请求路径为 /
  • favicon.ico :这个是浏览器自动发送的请求,用于获取网页图标

栗子:

const http = require('http');
const server = http.createServer((request,response)=>{
    console.log(request.method);
    console.log(request.httpVersion);
    console.log(request.url);
    console.log(request.headers);
    response.setHeader('content-type','text/html;charset=utf-8'); // 解决中文乱码
    response.end('hello') //设置响应体,并结束
});
server.listen(9000,()=>{
    console.log('9000服务已经启动')
})

结果:

image-20231013094744740

获取请求体

栗子:

const http = require('http');
const server = http.createServer((request,response)=>{
    response.setHeader('content-type','text/html;charset=utf-8'); // 解决中文乱码
    let body = '' // 请求体载体变量
    // 1. 绑定data事件,获取请求体的数据(一部分一部分获取)
    request.on('data',chunk => { // chunk是得到的请求体的一部分,是一个Buffer
        body += chunk // 将数据存入载体
    })
    // 2. 绑定end事件,当我们把数据读完就触发
    request.on('end',() => {
        console.log(body);
        response.end('请求体:' + body)
    })
    response.end('hello')//设置响应体,并结束
});
server.listen(9000,()=>{
    console.log('9000服务已经启动')
})

下面我们在POSTMAN里发送post请求,并携带请求体

image-20231013101004513

点击发送,获取请求体结果:

image-20231013101034078

但这里获取的请求体内容中文会乱码,由于后面使用express框架更为方便,所以这里就不进行解决了嘿嘿,我懒

url 模块(请求路径和参数的获取)

url模块主要用于请求路径和参数的获取,和其他模块一样,需要先引入

const url = require('url');

主要方法

方法说明
parse将一个URL字符串转化为URL对象
format将一个URL对象转化为URL字符串
resolve拼接URL

栗子:

const http = require('http');
//导入url模块
const url = require('url');
const server = http.createServer((request, response) => {
    response.setHeader('content-type', 'text/html;charset=utf-8');
    console.log(url.parse(request.url).pathname) // 获取路径
    console.log(url.parse(request.url, true).query) // 获取参数
    response.end('提取请求路径和字符') //设置响应体,并结束
});
server.listen(9000, () => {
    console.log('9000服务已经启动')
})

POSTMAN发送请求:

image-20231013102734022

控制台结果:

image-20231013102800810

Tops:不用管 [Object: null prototype],没什么影响

阔以简单看看url.parse(request.url)里面有什么

image-20231013103145662

到此,我们就能判断请求路径,并且返回不同的响应内容了,模板如下

const http = require('http')
const server = http.createServer((request, response) => {
    response.setHeader('content-type','text/html;charset=utf-8');
    // 获取请求方法
    let {method} = request;
    // 获取请求的url路径
    let {pathname} = url.parse(request.url)
    if(method === /* 请求方法 */ && pathname === /* 请求路径 */){
        response.end(/* 相对响应体 */)
    }else if(method === /* 请求方法 */ && pathname === /* 请求路径 */){
        response.end(/* 相对响应体 */)
    }else{
        //未改变响应状态码仍然是200
        response.end('404')
    }
})
server.listen(5000, () => {
    console.log('5000...')
})

如上,我们就能对不同请求做出不同响应了,但也有个缺点,就是整个代码结构读起来很难受;但后面使用到express框架就好很多

响应报文

学完了怎么接收请求内容,那么我们就应该学习怎么回应请求就使用到response对象,它的基本属性

属性含义
statusCode设置响应状态码
statusMessage设置响应状态信息(少用)
setHeader(key,value)设置响应头信息
write() / end()设置响应体

附:

  • write()可多次调用
  • write(),end()里都存在内容,内容将会拼接成一个,所有一般情况下write()里有内容,end()不进行设置
  • 必须有end(),有且只要一个

上栗子

const http = require('http');
const server = http.createServer((request, response) => {
    // 设置响应头
    response.setHeader('content-type', 'text/html;charset=utf-8')
    // 设置响应状态码
    response.statusCode = 203
    // 设置响应状态码描述信息
    response.statusMessage = 'test'
    // 设置响应体(可多次调用,追加数据)
    response.write('hello')
    // 必须有一个end()结尾
    response.end()
});
server.listen(9000, () => {
    console.log('9000服务已经启动')
})

访问结果:

image-20231013151225895

image-20231013151236140

若项目为前后端不分离项目,那么服务端就还存着前端的jscsshtml文件,我们也需要为这些文件设置响应,从而才能让它们被请求到,比如在某页面中:

<link rel="stylesheet" src="./index.css">

必须在后端设置相应的响应内容,才能被请求到,但当这些静态文件多起来,就很麻烦,如下

const http = require('http');
const fs = require('fs');
const server = http.createServer((request, response) => {
    let {pathname} = new URL(request.url, 'http://127.0.0.1'); // 获取请求路径
    if(pathname ==='/'){
        let html = fs.readFileSync(__dirname+'/index2.html');
        response.end(html); 
    }else if(pathname ==='/index.css'){
        let css = fs.readFileSync(__dirname+'/index.css');
        response.end(css); 
    }else if(pathname ==='/node.png'){
        let img = fs.readFileSync(__dirname+'/node.png');
        response.end(img); 
    }else if(pathname ==='/index.js'){
        let js = fs.readFileSync(__dirname+'/index.js');
        response.end(js); 
    }else{
        response.statusCode = 404;
        response.end('<h3>404</h3>')
    }
})
server.listen(3000, () => {
    console.log('3000')
})

这样写起来就very繁琐,每个静态文件(eg:js,css,html,json,图片…),都要在后端这里写上面内容;这是很痛苦的,内容简单重复,且多;所以我们阔以用到搭建静态服务

搭建静态资源服务

将需要暴露的静态文件放在static文件夹下

image-20231013153955566

http.js

const http = require('http');
const fs = require('fs');
const path = require('path');
// 匹配媒体类型(后面有解释)
let mines = {
    html :'text/html',
    css :'text/css',
    png :'image/png',
    js:'text/javascript',
    json:'application/json'
}
const server = http.createServer((request, response) => {
    if(request.method !== 'GET'){
        response.setHeader('content-type','text/html;charset=utf-8');
        response.statusCode = 405;
        response.end('方法不被允许');
        return;
    }
    let {pathname} = new URL(request.url, 'http://127.0.0.1'); // 获取请求路径
    // 网站的静态资源目录,可根据需求调整,这里我设置为当前文件夹下的static目录为静态资源目录
    let root = __dirname + '/static';
    // 拼接文件路径
    let filename = root + pathname; 
    // 读取文件 
    fs.readFile(filename,(err,data)=>{
        if(err){ // 错误处理
            switch(err.code){
                case 'ENOENT':
                    response.statusCode = 404;
                    response.end('文件不存在');
                case 'EPERM':
                    response.statusCode = 403;
                    response.end('无权限进行访问');
                default:
                    response.statusCode = 500;
                    response.end('服务器内部错误');
            }
            return;
        }
        // 设置响应文件类型,根据文件类型后缀决定
        // 获取文件后缀名
        let etx = path.extname(filename).slice(1);
        // 获取对应的类型
        let type = mines[etx];
        if(type){
            // 解决乱码问题
            response.setHeader('content-type',type + ';charset=utf-8');        
        }else{
            response.setHeader('content-type','application/octet-stream');
        }
        // 响应文件内容
        response.end(data);
    })    
})
server.listen(5000, () => {
    console.log('5000');
})

然后访问对应的路径,就能得到对应内容

image-20231013154107767

上面的mime数组是媒体类型对应:

媒体类型通常被称为一种标准,用来表示文档、文件或字节流的性质和格式。

  • mime类型结构 [type]/[subType],如:text/html text/css image/jpeg application/json
  • HTTP设置Content-type来表明响应体的类型,浏览器会根据该类型决定如何处理资源
  • 对于未知的资源可以application/octet-stream类型,浏览器会在遇到该类型时,会对响应体内容进行独立存储,也就是常见的下载效果

模块化

模块化就是引入其他人的代码或者自己之前写的代码,提高代码利用率(不重复写代码),模块分类:

  • 内置模块 - node官方提供
  • 自定义模块 - 自己写的js文件
  • 第三方模块 - 第三方开发出来,给大家用的js文件,使用前需要使用npm下载代码,再使用
require()引入模块

require方法是一个强大的方法,阔以引入所有模块

// 语法:
require(/* 模块名称或者路径 */)
/*
引入后,node会先看内置模块中是否有该模块,若没用,再看npm包管理工具中是否有下载相应的模块;若是路径,就会根据路径去找对应文件,看该文件是否有暴露内容
*/
const fs = require('fs') // 引入内置模块
const custom = require('./custom.js') // 引入自定义模块
const moment = require('moment') // 引入第三方模块

注意:

  • 使用 require() 方法加载其它模块时,会执行被加载模块中的代码
  • Node JS具有模块作用域,也就是模块中的变量和函数,只能在模块内被访问(除非暴露),很好得避免了全局变量污染问题
module.exports暴露模块

有两种方法暴露模块:

  • module.exports暴露
  • exports暴露

它们其实是一个东西,exports指向的对象也是module.exports对象;

模块默认暴露空对象

module.exports = {}

栗子:

module.js

// module.exports暴露
module.exports = {
    data: 'test'
}
// exports暴露
exports.data = 'test'
/*
上面两个写法都一样,但注意,不能写成:
	exports = {  }
这种形式,下面的注意事项会讲,写成上面形式是无法接收暴露的数据的!
因为module.exports的指向没有改变
*/

test.js

const m = require('./module.js');
console.log(m);

结果:

image-20231013162814448


注意:

  • 默认情况下 , exports 和 module.exports 指向同一个对象 。最终共享的结果,还是以 module.exports 指向的对象为准

栗子:

image-20231013163658674

上面代码暴露的是它们下面对应的对象

npm

npm看其他笔记哈哈哈哈哈哈,我懒得写了;它是Node JS的一个包管理工具;

nodemon(热部署)

上面我们修改了代码需要重新运行才能应用,无法实现热部署(不用重新运行,自动检查代码是否更新,自动重新运行),就使用到nodemon,使用方法也简单,下载就阔以

npm i -g nodemon

nodemon下载为全局可用,然后运行时使用命令

nodemon /* node文件名 */

之后代码修改后,会自动重启动,完成热部署

image-20231015102615170

express框架

上面内容中,使用Node JS自己的内置模块搭建服务,阔以是阔以,但是very麻烦,代码不清晰;使用express 框架就能让Node JS搭建服务更加清晰,更快速,更方便

什么是express?

官方给出的概念:Express是基于Node.js平台,快速,开放,极简的Web开发框架

通俗的理解:Express的作用和Node.js内置的http模块类似,是专门用来创建Web服务器的

express能干嘛?

对于前端程序员来说,最常见的两种服务器,分别是:

  • Web 网站服务器:专门对外提供 Web 网页资源的服务器。
  • API接口服务器:专门对外提供API接口的服务器。

因此使用Express,我们可以方便、快速的创建Web 网站的服务器或API接口的服务器,下面就让我们来学习它;首先呢,我们先来个大纲,其实学习express,主要学习它的两个部分:

  • 路由
  • 中间件

路由用来配置对应不同请求,不同的响应;

中间件用来拦截路由,进行相应检查或数据操作

入门

首先,它是个第三方模块,我们需要使用npm先下载它(在此前确保npm init,在该目录下初始化npm)

npm i express

然后在需要用它的地方引入它,然后使用它进行路由配置,最后监听;下面是一般的使用形式

// 1. 引入express包
const express = require('express')
// 2. 实例化对象
const app = express()
// 3. 编写路由 以/路由为栗,返回hello world
app.get('/', function(req, res) {
  res.send('hello world') // 响应体设置
})
// 4. 监听端口
app.listen(/* 端口号 */)

路由

路由的使用如上,具体语法格式:

// 语法:
app./* 请求方式 */(/* 路由路径 */,/* 回调函数(req请求封装对象,res响应封装对象) */)
/*
请求方式:
get,post,put,delete
*/

栗子:

// 1. 导入express包
const express = require('express')
// 2. 实例化app对象
const app = express()
// 3. 编写路由(根据不同的path, 返回不同的内容)
app.get('/', function (req, res) {
  // req: request(请求对象)
  // res: response(响应对象)
  res.send('首页')
})
// 4. 监听端口
app.listen(3000, function () {
  console.log('server is running on http://localhost:3000')
})
请求URL

接收请求的URL有三种写法:

  • 不带参数写法 - /test
  • 带参数写法 - /test/:id
  • 正则表达式写法 - /.html$/

栗子:

// 第一种, 不带参数
app.get('/test', function(req, res) {
  res.send('hello world')
})
// 第二种, 带参数
app.get('/test/:id', function(req, res) {
  res.send('hello world')
})
// 第三种, 正则表达式 - 接收以html结尾的请求
app.get(/.html$/, function(req, res) {
  res.send('hello world')
})
回调函数

回调函数中,有两个形参:

  • 请求封装对象 - req
  • 响应封装对象 - res
请求参数获取

express 框架封装了一些API来方便获取请求报文中的数据,并且兼容原生的HTTP模块的获取方式,如下

app.get('/test', function(req, res) {
 	console.log(req.method) // 请求方法
    console.log(req.url) // 请求url
    console.log(req.httpVersion) // 请求http版本
    console.log(req.headers) // 请求头
})

同时,还有一些express 自己封装的API,如下

app.get('/test', function(req, res) {
 	console.log(req.get('host')) // 获取指定请求头
	console.log(req.path) // 获取请求路径
    console.log(req.ip) // 获取请求的ip地址
    
})

然后,在请求封装对象中,我们经常需要获取请求携带的参数数据,来进行后端处理,参数的形式:

  • GET请求
    • path参数,在路径中的参数(eg:/user/1)
    • query参数,在路径的参数中(eg:/user?id=1&name=bokey)
  • POST请求
    • 请求体中携带参数

它们的接收:

GET请求获取参数:

const express = require('express');
const app = express();
// path参数接收:
app.get('/test/:id', function(req, res) {
 	console.log(req.params)
	res.send()
})
// query参数接收:
app.get('/test', function(req, res) {
    console.log(req.query)
  	res.send()
})

POST请求获取请求体:

const express = require('express');
const app = express();
// 获取请求体的配置!注意需要在 app.use(router); 之前!
/*
只要加入这个配置,则在req请求对象上会多出来一个属性:body
也就是说可以直接通过req.body来获取表单post请求数据
*/
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// 请求体参数接收:
app.post('/test', (req, res) => {
	console.log(req.body)
	res.send()
})

来个栗子

const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// path参数接收:
app.get('/testParams/:id/:name', function(req, res) {
    console.log(req.params)
   res.send('params测试')
})
// query参数接收:
app.get('/testQuery', function(req, res) {
   console.log(req.query)
     res.send('query测试')
})
// 请求体参数接收:
app.post('/testBody', (req, res) => {
   console.log(req.body)
   res.send('body测试')
})
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

POSTMAN测试

image-20231014102023555

image-20231014102142440

image-20231014104654553

控制台结果:

image-20231014104821780

附:

express4.x及以上版本已经不需要安装body-parser中间件去获取post请求的请求体了,且已经被弃用.

  • Express v4.16.0 引入了 express.json()、express.urlencoded() 中间件,express.json() 可解析json类型的req.body,express.urlencoded() 可解析urlencoded类型的req.body;
  • Express v4.17.0 又引入 express.raw() 、express.text() 中间件,express.raw() 可解析raw类型的req.body(解析为Buffer), express.text() 可解析text类型的req.body(解析为String);

但目前还没有中间件能处理multipart/form-data,类型的数据(阔以使用第三方的formidable中间件,用于文件上传,下面讲

Formidable 文件上传获取

为了获取multipart/form-data类型的数据,用于获取上传文件的内容,我们阔以使用Formidable这个第三方中间件,先在项目里下载它

npm i formidable

然后在需要使用到它的地方进行引入使用,如下

var express = require('express');
var router = express.Router();
// 1. 引入Formidable的构造器
const { IncomingForm } = require('formidable')
router.post('/fileUpload',(req,res) => {
  // 2. 创建form表单对象
  const form = new IncomingForm()
  // 3. 配置form表单对象(所有配置项都非必须)
  form./* 配置项 */ = /* 配置值 */
  // 4. 解析请求报文 fields存储一般的数据对象 files中存储文件内容
  form.parse(req,(err,fields,files) => {
    if(err) {
      next(err)
      return
    }
    // 其他操作,如数据存入数据库等...
    res.json({fields , files }) // 返回响应,这以返回json为栗子
  })
})

form表单对象的配置项:

  • encoding :设置表单域的编码,默认为utf-8
  • uploadDir :设置上传文件存放的文件夹,默认为系统的临时文件夹,可以使用fs.rename()来改变上传文件的存放位置和文件名;注意,一定给一个绝对路径,不然报错!
  • keepExtensions :设置该属性为true可以使得上传的文件保持原来的文件的扩展名,默认为false
  • maxFieldsSize :限制上传文件的大小,如果超出,则会触发error事件,默认为2M
  • maxFields :设置可以转换多少查询字符串,默认为1000

form.pase()讲解:

表单解析语法:form.parse(request, callback)

参数:

  1. request:表示请求信息,因为我们是对表单进行解析,所以首先需要获取到表单信息,所以这个参数是 request 事件的第一个参数。
  2. callback:表示一个回调函数,接收解析完成之后的信息。有三个参数(err, fields, files),分别表示err请求出错信息,fields普通请求参数,files上传文件请求参数

formidable 模块中包含的事件:

在使用parse方法对表单进行解析时,解析对象经历了一系列过程,我们可以监听这些过程从而完成某些操作,这些过程我们可以理解为事件。

  • progress:当有数据块被处理之后会触发该事件。具体语法:form.on('progress', (bytesReceived, bytesExpected) => {});
  • field:每当一个字段 / 值对已经收到时会触发该事件。具体语法:form.on('field', (name, value) => {});
  • fileBegin:在post流中检测到任意一个新的文件便会触发该事件。具体语法:form.on('fileBegin', (name, file) => {});
  • file:每当有一对字段/文件已经接收到,便会触发该事件。具体语法:form.on('file', (name, file) => {});
  • error:当上传流中出现错误便会触发该事件,当出现错误时,若想要继续触发request的data事件,则必须手动调用request.resume()方法。具体语法:form.on('error', (err) => {});
  • aborted:当用户中止请求时会触发该事件。具体语法:form.on('aborted', () => {});
  • end:当所有的请求已经接收到,并且所有的文件都已上传到服务器中,该事件会触发。具体语法:form.on('end', () => {});

上面内容参考文章:http://t.csdnimg.cn/MuCbX

下面还是老规矩,一个栗子

var express = require('express');
var router = express.Router();
const { IncomingForm } = require('formidable')
router.post('/fileUpload',(req,res) => {
  // 创建form表单对象
  const form = new IncomingForm()
  // 解析请求报文 fields存储一般的数据对象 files中存储文件内容
  form.parse(req,(err,fields,files) => {
    if(err) {
      next(err)
      return
    }
    res.json({fields , files })
  })
})
module.exports = router;

POSTMAN发送请求,以及响应

image-20231015153214904

响应设置

现在已经阔以得到请求的内容了,但还需要对请求进行响应,就使用到响应封装对象res,和req一样,它也支持原生的HTTP模块写法,如下

app.get('/test', function(req, res) {
 	res.statusCode = 404 // 设置响应状态码
    res.statusMessage = '描述信息' // 设置响应状态描述信息
    res.setHeader(/* headerKey */,/* value */) // 设置响应头
    res.write(/* 响应体 */) // 设置响应体
    res.end(/* 响应体 */) // 设置响应体
})

同时呢,和请求一样,express 框架也有自己的封装的响应API,如下

app.get('/test', function(req, res) {
 	res.status(404) // 设置响应状态码
    res.set(/* headerKey */,/* value */) // 设置响应头
    res.send(/* 响应体 */) // 设置响应体(常用!)(会自动设置'Content-Type:text/html;charset=utf-8'响应头,适配中文)
    res.redirect('http://baidu.com') // 设置重定向
    res.download(/* 下载文件路径及文件名 */) // 设置下载响应
    res.json(/* {  key : value  } */) // 设置json格式响应(常用!)
    res.sendFile(path.resolve(__dirname,/* 文件路径及文件名 */)) // 设置响应文件内容(注意:必须是绝对路径!记得导入path模块)
})
/*
express 框架对响应的设置阔以使用连贯设置如:
res.status(200).set('xxx','yyy').send('你好')
*/

对它们的附加内容

Tips:

  • res.write()res.end()必须成对出现,且不能使用res.send(),因为send相当于write+end
  • 使用res.send()时,要注意请求头中设置Cotent-Type参数,告诉前端响应数据类型
  • res.json()res.send()一样,都只能调用一次
  • 使用res.end()会影响性能

栗子

const express = require('express')
const path = require('path')
const app = express()
// send()
app.get('/sendTest', function(req, res) {
   res.send('send message')
})
// redirect
app.get('/redirectTest',(req,res) => {
   res.redirect('http://baidu.com')
})
// json
app.get('/jsonTest',(req,res) => {
   res.json({
      data: 'test'
   })
})
// download
app.get('/downloadTest',(req,res) => {
   res.download('./package.json')
})
// sendFile
app.get('/sendFile',(req,res) => {
   console.log(path.resolve(__dirname,'./package.json'));
   res.sendFile(path.resolve(__dirname,'./package.json'))
})
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

上面都是阔以测试的,没什么问题就

路由模块化

如果所有类型的路由都写在同一个文件里,那么会非常混乱;比如,路由中有关于用户的路由,有关于管理员的路由;Express 不建议将路由直接挂载到app上,而是推荐将路由抽离为单独的模块。将路由抽离为单独模块的步骤如下:

  • 创建路由模块对应的.js文件(一般在/router文件夹下)
  • 调用express.Router()函数创建路由对象
  • 向路由对象上挂载具体的路由
  • 使用module.exports 向外共享路由对象
  • 使用app.use()函数注册路由模块

语法:

// 1. Router的创建和暴露(其他文件中)
var express = require('express')  //导入express
var router = express.Router() //创建路由对象
router.use(/* 该路由的中间件 */)
// 该路由其他中间件....
router./* 请求方式 */(/* 请求路径 */,/* 回调函数 */)
// 该路由的其他请求...
module.exports = router

// 2. Router的引入和应用(核心js文件)
const express = require('express')
const app = express()
const /* Router名称 */ = require(/* Router路径及文件名 */)
app.use(/* Router名称 */)
// 和中间件一样,也阔以添加前缀                          ↓ 这个 / 必须加          
app.use(/* Router前缀 */,/* Router名称 */) // app.use('/test',RouterName)

栗子:

创建router文件夹和对应文件

image-20231015121238362

admin.js

// admin路由
var express = require('express')  //导入express
var router = express.Router() //创建路由对象
// 创建路由规则
router.get('/adminTest',(req,res) => {
    res.send('adminTest success')
})
// 暴露路由
module.exports = router

user.js

// user路由
var express = require('express')  //导入express
var router = express.Router() //创建路由对象
// 创建路由规则
router.get('/userTest',(req,res) => {
    res.send('userTest success')
})
// 暴露路由
module.exports = router

index.js

const express = require('express')
const app = express()
// 引入路由
const userRouter = require('./router/user')
const adminRouter = require('./router/admin')
// 应用路由               ↓ 这个 / 必须加
app.use(userRouter).use('/admin',adminRouter)
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

结果:

image-20231015143229899

image-20231015143156015

中间件

什么是中间件middleware

中间件的本质是一个函数,相当于拦截器;在请求到达后,进行相应操作或者检查;在中间件中也阔以正常访问请求封装对象req和响应封装对象res;阔以将中间件当作火车站的检票口,在进站前对东西进行检查

如下

image-20231015102659731

在Express中,中间件相当于插件,可以通过使用中间件来实现丰富的功能;有官方自己的内置中间件,也有第三方的中间件让我们使用

中间件长什么样子?

// 它实际上就是一个函数(箭头函数也可以,这里用function),有三个参数 req,res,next
const /* 函数名 */ = function(req,res,next) {
    /* 函数体.... */
    next() // 放行
}
/*
当然,也可以用res.send(),res.end(),res.write()去返回其他信息
每个中间件一般有next()用于放行,将程序执行放行到下个项目(中间件或者路由),不然下面的路由怎么样都不会执行
*/

附:

Tips:

  • 多个中间件之间,共享同一份reqres,基于这样的特征,我们可以在上游的中间件中,统一为req或res对象添加自定义的属性或者方法,供下游的中间件或者路由使用
  • 客户端发送过来的请求,可以连续调用多个中间件进行处理
  • 一定要在路由之前注册中间件

中间件的分类:

  • 应用级(全局)中间件 - 每次请求都会经过全局中间件处理
  • 路由级中间件 - 请求到当前路由,才经过路由中间件处理
  • 错误级中间件 - 用来处理错误的中间件,报错时处理
  • Express 内置中间件 - Express 框架内置的中间件,相当于内置插件
  • 第三方中间件 - 第三方的中间件,相当于第三方插件

下面我们就依次介绍它们的使用方法

全局中间件

每个路由前都会执行全局中间件,语法

const /* 中间件名称 */ = (req,res,next) => {
   /* 函数体 */
   next() // 放行
}
app.use(/* 中间件名称 */)
// ------------------------------
// 附:使用中间件判断是否放行栗子:
const myMiddleware = (req,res,next) => {
   if(/* 条件 */) {
       next() // 满足条件放行
   } else {
       res.send(/* 响应错误信息 */) // 不满足条件,返回错误信息
   }
}
app.use(myMiddleware)

栗子:

const express = require('express')
const app = express()
// 定义中间件
const myMiddleware = (req,res,next) => {
   console.log('全局中间件!');
   next()
}
const myMiddleware2 = function(req,res,next) {
   console.log('全局2号');
   next()
}
// 全局使用 全局中间件的调用按照use()的先后顺序调用
app.use(myMiddleware)
app.use(myMiddleware2)
// 测试
app.get('/meddleWareTest',(req,res) => {
   res.send('meddleWareTest finish')
})
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

访问

image-20231015110106931

控制台输出

image-20231015110009688

路由中间件

每个路由执行前,会先执行全局中间件,然后到该路由的中间件,若都放行才会进行路由回调;语法

const /* 中间件名称 */ = (req,res,next) => {
   /* 函数体 */
   next() // 放行
}
app./* 请求方法 */(/* 路由规则 */,/* 中间件名称 */,/* 路由回调函数 */)

上栗子

const express = require('express')
const app = express()
// 定义中间件
const myMiddleware = (req,res,next) => {
   console.log('路由中间件!');
   next()
}
const myMiddleware2 = function(req,res,next) {
   console.log('路由中间件2号');
   next()
}
// 路由中间件挂载               ↓ 按照挂载的顺序执行
app.get('/meddleWareTest',myMiddleware,myMiddleware2,(req,res) => {
   res.send('meddleWareTest finish')
})
// 测试路由
app.get('/routeTest',(req,res) => {
   res.send('routeTest')
}) 
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

访问/meddleWareTest,控制台输出

image-20231015111436384

访问/routeTest路由,没有任何输出

错误中间件

错误级别的中间件的作用:专门用来捕获整个项目发送的异常错误,从而防止项目异常奔溃的问题;语法

app.get('/',(req,res)=>{    //路由
	throw new Error('服务器内部发生了错误')  	//1.1 抛出一个自定义的错误
	res.send('home Page')
})
app.use(function(err,req,res,next){    //2 错误级别的中间件
	console.log('发生了错误'+err.messgae)    //2.1 在服务器打印错误消息
	res.send('Error!'+err.messgae)    //2.2 在客户端响应错误的相关内容
})
内置中间件

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

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

其中,express.jsonexpress.urlencoded两个中间件,我们在获取请求体中有使用到

下面来介绍express.static,它主要使用于托管静态资源

express.static静态资源托管

通过它,我们可以非常方便的创建一个静态资源服务器;例如,通过如下代码可以将public目录下的图片,css文件,JavaScript文件对外开放访问了

// 绑定静态资源目录(可绑定多个,多次调用就可以)
app.user(express.static(/* 静态资源路径 */))
// 添加绑定前缀,访问静态资源前,需要添加前缀才能访问 注意:前缀需要用 / 开头,eg:'/test'
app.user(/* 前缀 */,express.static(/* 静态资源路径 */)) // 访问路径:http://localhost:9000/前缀/静态资源路径

栗子:

image-20231015114711974

const express = require('express')
const app = express()
// 静态资源托管
app.use(express.static('./public'))
// 前缀  ↓ 这个 / 必须加!
app.use('/test',express.static('./static'))
app.listen(9000, function () {
  console.log('server is running on http://localhost:9000')
})

然后访问

  • http://localhost:9000/css/test.css
  • http://localhost:9000/js/test.js
  • http://localhost:9000/image/bokey.png
  • http://localhost:9000/test/html/test.html

都可以访问到相应资源了


注意事项:

  • index.html是默认返回的资源(可省略)
  • 若静态资源于路由规则相同,那么谁先执行就响应谁
第三方中间件

非Express官方内置的,而是由第三方开发出来的中间件,叫做第三方中间件;在项目中,大家可以按需下载并配置第三方中间件,从而提高项目的开发效率,使用步骤

  • 运行npm install body-parser安装中间件
  • 使用require 导入中间件
  • 调用app.use0注册并使用中间件

express generator(脚手架)

express granerator是构建express 框架项目最快的方式,构建方法简单省事,先进行全局下载

npm i express-generator -g

安装完成后,使用命令构建express 项目

express /* 项目名 */

就阔以看到项目的基础骨架了,但还需要进行npm初始化,下载相关依赖;进入项目文件夹下,使用命令

npm i

image-20231015145055414

其中:

  1. app.js是项目主文件;
  2. views目录用于存放页面文件;
  3. routes目录用于存放路由文件;
  4. public用于存放静态文件;
  5. bin中的www是项目的启动文件;

于是就完成整个项目的创建了,运行命令(默认运行的端口为3000)

npm start

但这是不支持nodemon(热部署)的,需要热部署就打开package.json文件,修改scriptsstart属性值的node改为nodemon

"scripts": {
    "start": "nodemon ./bin/www"
},

就支持热部署了,还是npm start启动项目

会话控制

后端项目中,我们常常需要辨认当前发送请求的是哪个用户,这时候就用到会话控制;这里讲两种会话的控制:

  • Cookie
  • Session

它们的区别:

  1. 存在位置
    • cookie:存于浏览器
    • session:存于服务端
  2. 安全性
    • cookie以明文方式存放,安全性比较低
    • session存放在服务器,安全性相对较高
  3. 网络传输占用
    • cookie是携带在请求头的,存储的数据过多,会直观影响传输效率
    • session只是存储id在cookie中,携带在请求头,增加传输效率
  4. 存储限制
    • 浏览器限制单个cookie最大4k,且单个域名下也有存储限制
    • session存储在服务器,没有限制
Cookie

语法:

// 创建Cookie
res.cookie(/* key键 */,/* value值 */,/* 配置对象 */)
/*
配置对象属性: {
	domain: 域名  
	name=value:键值对,可以设置要保存的 Key/Value,注意这里的 name 不能和其他属性项的名字一样
	Expires: 过期时间(秒),在设置的某个时间点后该 Cookie 就会失效,如 expires=Wednesday, 09-Nov-99 23:12:40 GMT
	maxAge: 最大失效时间(毫秒),设置在多少后失效
	secure: 当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
	Path: 表示 在那个路由下可以访问到cookie
	httpOnly:是微软对 COOKIE 做的扩展。如果在 COOKIE 中设置了“httpOnly”属性,则通过程序(JS 脚本、applet 等)将无法读取到COOKIE 信息,防止 XSS 攻击的产生
	singed:表示是否签名cookie, 设为true 会对这个 cookie 签名,这样就需要用 res.signedCookies 而不是 res.cookies 访问它。被篡改的签名 cookie 会被服务器拒绝,并且 cookie 值会重置为它的原始值
}
*/

// 获取Cookie 注意:若不是使用express generator构建的项目,需要下载包:cookie-parser,才能获取
req.cookie./* Cookie的key键 */
/*
下载cookie-parser命令:npm i cookie-parser --save
引入:
	const cookieParser = require("cookie-parser");
	app.use(cookieParser());
*/

// 删除Cookie
res.clearCookie(/* Cookie的key键 */)

栗子

var express = require('express');
var router = express.Router();
router.get('/setCookie',(req,res) => {
  res.cookie('name','bokey')
  res.cookie('data','test', { maxAge:60 })
  res.send('Cookie设置成功')
})
router.get('/getCookie',(req,res) => {
  res.send(req.cookies)
})
router.get('/deleteCookie/:name',(req,res) => {
  res.clearCookie(req.query.name)
  res.send('Cookie删除成功')
})
module.exports = router;

测试结果:

image-20231015160702433

image-20231015161105976

image-20231015161153460

Session

Session的存储是需要用到数据库的,我们先使用mysql数据库;首先先下载包

npm i express-session express-mysql-session --save

基本使用,如下

const express = require('express')
const app = express()
// 导入
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
// 配置SessionStore
const sessionStore = new MySQLStore({
	host: 'localhost',
	port: 3306,
	user: 'root',
	password: '123456',
	database: 'test'
});
// app应用session
app.use(session({
	name: 'sid', // 前端保存的Cookie的key键
	secret: 'bokey', // 密钥 用来加密Session内容
	store: sessionStore, // sessionStore配置,配置mysql数据库的连接
	resave: true, // 是否在每次重新请求时更新session(登录后,有操作会保持登录状态)
	saveUninitialized: false, // 是否每次都设置一个Cookie存储sessionId(没登陆也设置)
    cookie: {
        httpOnly: true, // 前端无法通过JS脚本访问
        maxAge: 1000, // sessionID的Cookie过期时间,单位:ms
    }
}));
// 设置session信息
req.session./* Session信息key */ = /* Session信息value */
// 获取Session信息,或者查看Session是否存在;会自动从数据库读取数据
req.session.username
// 销毁Session
req.session.destroy(/* 回调函数 */)

来个栗子

const express = require('express')
const app = express()
// 导入
const session = require('express-session');
const MySQLStore = require('express-mysql-session')(session);
// 配置SessionStore
const sessionStore = new MySQLStore({
	host: 'localhost',
	port: 3306,
	user: 'root',
	password: '123456',
	database: 'test'
});
// app应用session
app.use(session({
	name: 'sid', // 前端保存的Cookie的key键
	secret: 'bokey', // 密钥 用来加密Session内容
	store: sessionStore, // sessionStore配置,配置mysql数据库的连接
	resave: true, // 是否在每次重新请求时更新session(登录后,有操作会保持登录状态)
	saveUninitialized: false, // 是否每次都设置一个Cookie存储sessionId(没登陆也设置)
    cookie: {
        httpOnly: true, // 前端无法通过JS脚本访问
        maxAge: 1000, // sessionID的Cookie过期时间,单位:ms
    }
}));
// 创建session
app.get('/createSession/:sessionMsg',(req,res) => {
    if(req.query.username == 'admin' && req.query.password == '123456') {
        // 设置session信息
        req.session.username = req.params.sessionMsg
        req.session.uid = 'test'
        res.send('登录成功~')
    } else {
        res.send("登录失败~")
    }
})
// session读取
app.get('/sessionGet',(req,res) => {
    // 检测session是否存在
    if(req.session.username) {
        res.send('session存在:' + req.session.username)
    } else {
        res.send('session不存在')
    }
})
// session的销毁
app.get('/destroySession',(req,res) => {
    req.session.destroy(() => {
        res.send('Session已销毁')
    })
})
app.listen(3000)

运行后,会自动在MySQL数据库生成一个表session,如下

image-20231016153054006

测试创建Session,阔以看到对应的Cookie被生成

image-20231016152846866

数据库内容也增加了,并且有着相应Session信息

image-20231016152906736

查看Session内容

image-20231016152933876

销毁Session

image-20231016153024689

数据库中也销毁了

image-20231016153037800

数据库操作

现在,我们已经具备了使用express 框架的能力,下面就是进行数据库的学习;

MySQL

学习express 框架和MySQL数据的连接和获取,首先先下载个中间件(插件)

npm i mysql

在使用前需要导入

const mysql = require('mysql')

基本的使用流程

// 1. 引入mysql包
const mysql = require('mysql')
// 2. 创建数据库连接
const con = mysql.createConnection({
  host: /* 数据库的主机ip地址 */,
  user: /* 数据库用户名 */,
  password: /* 该用户密码 */,
  database: /* 数据库名称 */,
})
// 3. 连接数据库
con.connect()
// 4. 执行查询
con.query(/* sql查询语句 */,/* 查询参数 */,/* 回调函数(err错误信息,data查询结果数据) */)
// 5. 关闭连接
con.end()

栗子:

先准备MySQL数据库,如下

image-20231016114634894

user表内容

image-20231016114708284

然后创建文件列表,如下

image-20231016114602738

mysql.js文件中配置MySQL的连接和封装

// 引入mysql(注意下载mysql包:npm i mysql)
const mysql = require('mysql')
// 创建数据库连接
const conn = mysql.createConnection({
    host: 'localhost',
	user: 'root',
	password: '123456',
	port: '3306',
	database: 'test'
})
// 开始连接
conn.connect()
// 封装执行SQL的函数
function exec(sql,params) {
	const promise = new Promise((resolve,reject)=>{
		conn.query(sql, params, (err,result) => {
			if(err){
				reject(err);
				return;
			}
			resolve(result);
		})
	});
	return promise;
}
// 暴露SQL执行函数
module.exports = exec

创建Controller控制层,对数据库操作进行封装;这里以userController为栗

// 引入SQL执行函数
const exec = require('../db/mysql')
// 暴露Controller相关方法
module.exports = {
    // 查找所有用户数据
    getAllUser: () => {
        const sql = 'select * from user'
        return exec(sql).then(data => {
            console.log(data);
            return data
        })
    },
    // 添加用户
    addUser: (user) => {
        const sql = 'insert into user(name,age) values(?,?)'
        const params = [user.name,user.age]
        return exec(sql,params).then(data => {
            console.log(data);
            return data
        })
    },
    // 删除用户
    deleteUser: (id) => {
        const sql = 'delete from user where id=?'
        const params = [id]
        return exec(sql,params).then(data => {
            console.log(data);
            return data
        }) 
    }
}

然后在路由中,使用Controller提供接口服务

var express = require('express');
var router = express.Router();
let userController = require('../controller/userController')
// 获取所有用户            ↓ 使用async await等待Proemise的返回结果,不然无法返回给客户端
router.get('/getAllUser',async(req,res) => {
  const data = await userController.getAllUser()
  res.send(data)
})
// 添加用户
router.get('/addUser',async(req,res) => {
  const user = {
    name: req.query.name,
    age: req.query.age
  }
  res.send(await userController.addUser(user))
})
// 删除用户
router.get('/deleteUser/:id',async(req,res) => {
  res.send(await userController.deleteUser(req.params.id))
})
module.exports = router;

POSTMAN发送请求测试

image-20231016121910309

image-20231016121927128

image-20231016122129085

其中插入,删除,修改操作都会返回一个对象,该对象的常用属性如下

  • affectedRows - 受影响的行数
  • insertId - 插入数据的返回id
  • warningCount - 警告次数
  • changedRows - 已更改行数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值