这是一篇还在更新的 Node.js 学习笔记,记录我学到的知识点和练习代码。目前从 Buffer、文件读写和 HTTP 学到了 Express,后面会边学边补。
Buffer(缓冲器)
概念
- 一个类似于Array的对象,即表示一段固定长度的内存空间,用于处理二进制数据
特点
- Buffer大小固定且无法调整
- 性能较好,直接对计算机内存进行操作
- 每个元素的大小为1字节(byte),即8个bit
创建方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| //1. alloc
let buf = Buffer.alloc(10)
//2.allocUnsafe,不会对原内存清空,包含旧数据
// 不需要归零操作,因此速度快于 alloc
let buf_2 = Buffer.allocUnsafe(10)
//3.from
let buf_3 = Buffer.from('hello')
let buf_4 = Buffer.from([166, 233, 145, 764, 103, 221])
console.log(buf_4) // <Buffer a6 e9 91 fc 67 dd>
// ps:Buffer.from('hello') 创建时,会使用UTF-8编码算法,把字符串转为一串字节;
// console.log打印buffer时,只是把Buffer内部每个字节的值,用16进制格式展示出来;
// Buffer底层存储的是字节(本质二进制数据)。
|
buffer 的元素转换
1
2
3
4
5
6
7
| // buffer与字符串的转换
let buf_4 = Buffer.from([105, 108, 111, 118, 101, 121, 111, 117])
console.log(buf_4.toString()) //iloveyou ,通过 utf--8,即toString('utf-8')
// 字节2进制转换
let buf = Buffer.from('hello')
console.log(buf[0].toString(2)) //01101000
|
注意
因为8位数二进制最多为255
1
2
| let buf = Buffer.from('hello')
buf[0] = 361 // 舍弃高位数字 0001 0110 1001 => 0110 1001
|
一个中文utf-8中占3个字节
fs模块 *
概念与使用
fs模块可以实现与硬盘的交互,例如
- 文件的创建、删除、重命名、移动
- 文件内容的写入读取
- 文件夹的相关操作
文件写入
换行得用\r\n
- writeFile 异步写入,即单开线程,主线程继续走
1
2
3
4
5
6
7
8
9
10
11
12
| // 1.导入 fs 模块
const fs = require('fs')
//2. 写入文件
fs.writeFile('./座右铭.txt', '三人行,则必有我师焉', err => {
// err 写入失败:错误对象 写入成功:null
if (err) {
console.log('写入失败')
} else {
console.log('写入成功')
}
})
|
- writeFileSync 同步写入,主线程停滞,等同步写入完再继续执行
1
| fs.writeFileSync('./data.txt', 'text')
|
- 第三个参数{flag:‘a’}
1
2
3
4
5
6
7
8
9
10
11
12
| // 1.导入 fs 模块
const fs = require('fs')
//2. 写入文件
fs.writeFile('./座右铭.txt', '三人行,则必有我师焉', { flag: 'a' }, err => {
// err 写入失败:错误对象 写入成功:null
if (err) {
console.log('写入失败')
} else {
console.log('写入成功')
}
})
|
- appendFile 异步追加
- appendFileSync 同步追加
1
| fs.appendFileSync('./座右铭.txt', '\r\n温故而知新,可以为师矣')
|
- 流式写入 *
程序打开一个文件是需要消耗资源的,流式写入可以减少打开关闭文件的次数。
流式写入方式适用于大文件写入或者频繁写入的场景,writeFile适合于写入频率较低的场景
如
1
2
3
4
5
6
7
8
9
10
11
| const fs = require('fs')
//创建写入流对象
const ws = fs.createWriteStream('./观书有感.txt')
ws.write('半亩方糖一件开1\r\n')
ws.write('半亩方糖一件开2\r\n')
ws.write('半亩方糖一件开3\r\n')
ws.write('半亩方糖一件开4')
//关闭通道
ws.close()
|
- 文件写入场景
需持久化保存数据的时候,就应该想到文件写入
文件读取
- 异步和同步读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const fs = require('fs')
//异步读取
fs.readFile('./观书有感.txt', (err, data) => {
if (err) {
console.log('读取失败')
return
}
console.log(data) //输出为buffer,因此要转字符串
console.log(data.toString())
})
// 同步读取
let dataSync = fs.readFileSync('./观书有感.txt')
console.log(dataSync.toString())
|
- 读取文件应用场景
- 电脑开机
- 程序运行
- 编辑器打开文件
- 查看图片
- 播放视频、音乐
- git查看日志,git log
- 上传文件,(读取文件,然后传到服务端)
- 查看聊天记录
- 流式读取
1
2
3
4
5
6
7
8
9
10
11
12
| const fs = require('fs')
// 创建读取流对象
const rs = fs.createReadStream('../资料/笑看风云.mp4')
//绑定data事件 chunk 块儿 大块儿
rs.on('data', chunk => {
console.log(chunk)
console.log(chunk.length) // 66536 字节 => 64KB
})
//end 可选事件
rs.on('end', () => {
console.log('读取完成')
})
|
文件复制案例*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| const fs = require('fs')
const process = require('process')
//方式1 readFile
// 读取文件内容
let data = fs.readFileSync('../资料/笑看风云.mp4')
// 写入文件
fs.writeFileSync('../资料/笑看风云复制.mp4', data)
//检查内存使用量 rss字段
console.log(process.memoryUsage()) //110710784字节 105MB
// 方式2 流式操作
// 创建写入流对象
const ws = fs.createWriteStream('../资料/笑看风云流式复制.mp4')
// 创建读取流对象
const rs = fs.createReadStream('../资料/笑看风云.mp4')
// // 为读取流绑定data事件
// rs.on('data', chunk => {
// ws.write(chunk)
// })
// rs.on('end', () => {
// //检查内存使用量 rss字段
// console.log(process.memoryUsage()) //43106304 41MB
// })
// 更好的写法,自动处理背压,因为纯手写没处理背压。读得比写得快,数据块排队堆内存,rss 升高
rs.pipe(ws)
|
- 方式2性能更好,比如MP4资源为78MB,则方式1内存资源占用78M,而理想状态方式2只占64KB,为什么理想,是因为读取速度比写入速度快,因此会大于64KB,但是也优于方式1
文件的重命名和移动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const fs = require('fs')
// 调用 rename 方法
fs.rename('./座右铭.txt', './论语.txt', err => {
if (err) {
console.log('重命名失败')
return
}
console.log('重命名成功')
})
// 文件的移动
fs.rename('./座右铭.txt', '../资料/移动文件.txt', err => {
if (err) {
console.log('移动失败')
return
}
console.log('移动成功')
})
|
文件删除
一样有同步函数,较简单
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const fs = require('fs')
// 调用unlink方法 unlinkSync
fs.unlink('./观书有感.txt', err => {
if (err) {
console.log('删除失败')
return
}
console.log('删除成功')
})
// 调用rm方法 14.4 rmSync
fs.rm('../资料/移动文件.txt', err => {
if (err) {
console.log('删除失败')
return
}
console.log('删除成功')
})
|
文件夹操作
参数1path路径,参数2options选型配置(可选),回调函数
- 创建文件
1
2
3
4
5
6
7
8
9
| const fs = require('fs')
fs.mkdir('./html', err => {
if (err) {
console.log('创建失败')
return
}
console.log('创建成功')
})
|
- 递归创建
1
2
3
4
5
6
7
8
| //递归创建
fs.mkdir('./a/b/c', { recursive: true }, err => {
if (err) {
console.log('创建失败')
return
}
console.log('创建成功')
})
|
- 读取文件夹
1
2
3
4
5
6
7
| fs.readdir('../资料', (err, data) => {
if (err) {
console.log('读取失败')
return
}
console.log(data)
})
|
- 删除文件夹
1
2
3
4
5
6
7
| fs.rmdir('./html', (err, data) => {
if (err) {
console.log('读取失败')
return
}
console.log(data)
})
|
- 递归删除
1
2
3
4
5
6
7
| // fs.rmdir('./a', { recursive: true }, err => {
// if (err) {
// console.log('删除失败')
// return
// }
// console.log('删除成功')
// })
|
1
2
3
4
5
6
7
8
| //递归删除
fs.rm('./a', { recursive: true }, err => {
if (err) {
console.log('删除失败')
return
}
console.log('删除成功')
})
|
查看资源信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const fs = require('fs')
//stat 方法 status 缩写 状态
fs.stat('../资料/笑看风云.mp4', (err, data) => {
if (err) {
console.log('查看失败')
return
}
console.log(data)
// isFile
console.log('是否为文件', data.isFile())
// 目标资源是不是一个文件夹
console.log('是否为文件夹:', data.isDirectory())
})
// 常用信息size,最后访问时间atime,文件创建时间birthtimeMs,最后修改时间mtime,最后一次修改文件状态的时间ctime
|
fs路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const fs = require('fs')
// 相对路径的bug
// 脚本执行的时候,默认参照命令行执行时的所在目录,如:
// 以下命令PS D:\代码学习\后端\node.js后端> node ./代码/11-fs路径.js,执行结果html与代码是同级目录
fs.writeFileSync('./index.html', 'love')
// 绝对路径'全局变量',本文件的所在目录的绝对路径'__dirname'
// 本文件的绝对路径'__filename'
console.log(__dirname) //D:\代码学习\后端\node.js后端\代码
const path = __dirname + '/index.html'
fs.writeFileSync(path, 'love')
|
批量重命名练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| const fs = require('fs')
const path = require('path')
const basePath = __dirname
const files = fs.readdirSync(basePath)
let fileList = []
try {
files.forEach(item => {
let data = item.split('-')
let [numStr, ...rest] = data
let num = Number(numStr)
// 不是【数字-xxx】格式,直接跳过
if (isNaN(num)) return
let name = rest.join('-')
fileList.push({ num, name, item })
})
// 按数字从小到大排序,对象捆绑,不会错位
fileList.sort((a, b) => a.num - b.num)
// 计算最大序号的位数,用来补0
const maxNum = fileList[fileList.length - 1].num
const maxLen = String(maxNum).length
// 生成所有新文件名
const result = fileList.map(obj => {
const padded = String(obj.num).padStart(maxLen, '0')
return `${padded}-${obj.name}`
})
// ==========【试运行预览模式】只打印,不修改任何文件!!==========
result.forEach((item, index) => {
console.log(`预览:${fileList[index].item} → ${item}`)
})
// ==========【真正重命名代码,预览确认无误后,再取消下面注释!】==========
result.forEach((item, index) => {
const oldPath = path.join(basePath, fileList[index].item)
const newPath = path.join(basePath, item)
fs.renameSync(oldPath, newPath)
console.log(`已重命名:${fileList[index].item} → ${item}`)
})
} catch (err) {
console.log('出现错误:')
console.log(err)
}
|
path模块
- path.resolve 解决路径问题的规范(常用)
调试环境:当前 js 文件所在目录(基准路径):D:\代码学习\后端\node.js后端\express框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const path = require('path')
console.log(path.resolve(__dirname, './index.html'))
// D:\代码学习\后端\node.js后端\express框架\index.html
console.log(path.resolve(__dirname, 'index.html'))
// D:\代码学习\后端\node.js后端\express框架\index.html
// ✅ 上面两者输出结果一致,自动转为系统标准分隔符 Windows为 \
// ⚠️ 重点坑:第二个参数开头带 /,代表绝对路径,直接抛弃前面所有内容
console.log(path.resolve(__dirname, '/index.html'))
// D:\index.html
// ⚠️ 只要任意片段以 / 开头,该片段左侧全部作废,继续向右拼接剩下片段
console.log(path.resolve(__dirname, '/index.html', './test'))
// D:\index.html\test
|
- path.json 和 path.resolve区别
1
2
3
4
5
6
7
8
9
10
11
12
13
| const path = require('path')
console.log(path.join(__dirname, './index.html'))
// D:\代码学习\后端\node.js后端\express框架\index.html
console.log(path.join(__dirname, 'index.html'))
// D:\代码学习\后端\node.js后端\express框架\index.html
// ✅ join不会跳到磁盘根,忽略片段开头的 /
console.log(path.join(__dirname, '/index.html'))
// D:\代码学习\后端\node.js后端\express框架\index.html
console.log(path.join(__dirname, '/index.html', './test'))
// D:\代码学习\后端\node.js后端\express框架\index.html\test
|
- sep 分隔符,获取当前操作系统的路径分隔符
1
2
3
| const path = require('path')
console.log(path.sep) // window 为 ‘\’ linux‘/’
|
- parse方法 解析路径并且返回对象
1
2
| const path = require('path')
console.log(path.parse(__filename))
|
- basename路径基础名称,dirname目录名,extname扩展名
3和4都是字符串处理函数,不经过磁盘扫描
注意
- 同步函数没有回调参数,如需捕获错误需通过try{}catch{}
HTTP模块
获取请求行和请求头信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const http = require('http')
const server = http.createServer((request, response) => {
// 获取请求的方法
console.log(request.method) //GET
// 获取请求的 url
console.log(request.url) //只包含url中的路径与查询参数
// 获取http 协议的版本号
console.log(request.httpVersionp)
// 获取http的请求头
console.log(request.headers.host)
response.end('http') // 设置响应体
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
获取请求体信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<form action="http://127.0.0.1:9000" method="post">
<input type="text" name="username" />
<input type="text" name="password" />
<input type="submit" value="提交" />
</form>
</body>
</html>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // 响应上方html的post请求
const http = require('http')
const server = http.createServer((request, response) => {
let body = ''
// 绑定data事件
request.on('data', chunk => {
console.log(chunk)
body += chunk
})
// 绑定 end 事件
request.on('end', () => {
console.log(body)
// 响应
response.end('Hello HTTP')
})
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
获取url的请求参数和查询字符串
1
2
3
4
5
6
7
8
9
10
| const http = require('http')
const server = http.createServer((request, response) => {
let url = new URL(request.url, 'http://127.0.0.1')
console.log('pathname输出路径:', url.pathname) // 访问 /search?keyword=nodejs 输出:/search
console.log(url.searchParams.get('keyword')) // 访问 /search?keyword=nodejs 输出:nodejs
response.end('url new')
})
server.listen(9000, () => {
console.log('服务已经启动') // 服务启动成功打印:服务已经启动
})
|
http请求练习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| const http = require('http')
const server = http.createServer((request, response) => {
// 设置响应头
response.setHeader('Content-Type', 'text/plain;charset=utf-8')
// 获取url参数
let url = new URL(request.url, 'http://127.0.0.1')
const { method } = request
const { pathname } = url
console.log(method, pathname)
if (method !== 'GET') response.end('请求方式错误') // 设置响应体
if (pathname == '/login') {
response.end('登录页面') // 设置响应体
} else if (pathname == '/reg') {
response.end('注册页面') // 设置响应体
} else response.end('页面未开发') // 设置响应体
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
设置http响应报文
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| const http = require('http')
const server = http.createServer((request, response) => {
// 设置响应状态码
response.statusCode = 200 // 默认状态码为200
// 响应状态码描述
response.statusMessage = 'i love u' // 用地较少
// 响应头设置
response.setHeader('Content-Type', 'text/plain;charset=utf-8')
response.setHeader('Server', 'node.js')
response.setHeader('MyHeader', 'linda')
// 设置同名响应头
response.setHeader('test', ['a', 'b', 'c'])
// 可多次调用的响应体,用了write可不写end
response.write('love')
response.write('love')
response.write('love')
response.write('love')
response.end('响应报文')
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
响应报文练习
实现四行三列的表格,点击项对应变色
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| <!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
td {
padding: 20px;
}
table tr:nth-child(odd) {
background: rgb(201, 255, 170);
}
table tr:nth-child(even) {
background: rgb(119, 119, 136);
}
table,
td {
border-collapse: collapse;
}
</style>
</head>
<body>
<table border="1">
<tr>
<td>test1</td>
<td>test1</td>
<td>test1</td>
</tr>
<tr>
<td>test1</td>
<td>test1</td>
<td>test1</td>
</tr>
<tr>
<td>test1</td>
<td>test1</td>
<td>test1</td>
</tr>
<tr>
<td>test1</td>
<td>test1</td>
<td>test1</td>
</tr>
</table>
<script>
// 获取所有td
let tds = document.querySelectorAll('td')
// 点击事件
tds.forEach(item => {
item.onclick = function () {
this.style.backgroundColor = '#631'
}
})
</script>
</body>
</html>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| const http = require('http')
const fs = require('fs')
const server = http.createServer((request, response) => {
const data = fs.readFileSync(__dirname + '/06-响应报文练习.html')
//end 可读取buffer也可读取字符串
response.end(data)
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
响应练习扩展
如果进行目录及js,css,html拆分,考虑浏览器渲染原理以及响应体的资源请求路由没有判断,一直返回html文件,导致js和css无法导入到html(可从浏览器控制台查看网络接口的访问),因此需要优化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| const http = require('http')
const fs = require('fs')
const server = http.createServer((request, response) => {
const html = fs.readFileSync(__dirname + '/07-响应报文练习.html')
// 需要区分请求,和06不一样了,不然js,css走9000端口返回的都是这个 html
// response.end(html) // ❌
const css = fs.readFileSync(__dirname + '/index.css')
const js = fs.readFileSync(__dirname + '/index.js')
// 实例化url对象
let url = new URL(request.url, 'http://127.0.0.1')
const { pathname } = url
// 输出路径
console.log('pathname输出路径:', pathname)
if (pathname == '/') response.end(html)
else if (pathname == '/index.css') response.end(css)
else if (pathname == '/index.js') response.end(js)
else response('404')
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
当文件有很多的时候,就要再次优化,见后文
静态资源服务
- 静态资源的封装导入
目录结构:

page属于静态资源目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| const http = require('http')
const fs = require('fs')
const server = http.createServer((request, response) => {
// 实例化url对象
let url = new URL(request.url, 'http://127.0.0.1')
const { pathname } = url
//拼接文件路径
let filePath = __dirname + '/page' + pathname
// 读取文件 fs 异步 API
fs.readFile(filePath, (err, data) => {
if (err) {
response.statusCode = 500
response.end('文件读取失败')
return
}
//响应文件内容
response.end(data)
})
})
server.listen(9000, () => {
console.log('服务已经启动')
})
|
实现静态资源配置,静态资源有变化不需要和往常一样重复操作导入
静态资源的url引用
url、src等,常用<link rel="stylesheet" href="/css/index.css" />,’./‘的话会有隐藏bug,后文模板引擎部分会讲,直接用’/‘可以防止域名或者端口发生变化,从而直接拼接。
mime类型
response.setHeader('ContentType','xxx),‘xxx’指的就是文件类型的判断,但是通常浏览器已经有这个嗅觉了
手动判断也更规范,可以用path.exthname()来判断后缀从而匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const filePath = path.join(__dirname, '/css/index.css')
const extWithDot = path.extname(filePath) // '.css'
const ext = extWithDot.slice(1).toLowerCase() // 'css'
// mime映射表
const mime = {
html: 'text/html;charset=utf-8',
css: 'text/css;charset=utf-8',
js: 'application/javascript;charset=utf-8',
png: 'image/png',
jpg: 'image/jpeg'
}
const contentType = mime[ext] || 'application/octet-stream'
console.log(contentType) // text/css;charset=utf-8
|
- err错误处理

通过读取err打印出来的code类型去进行状态码反馈,如404、403等,详情可见官网的error的错误类型
CommonJS规范模块化
暴露数据及文件导入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const duanzi = function () {
console.log('我是个段子')
}
const lezi = function () {
console.log('我是个乐子')
}
// exports.duanzi = duanzi
// exports.lezi = lezi
// 直接用这种比较好,因为exports不能用直接=value的方式,而module.exports = '111'可以
// 因为require返回结果是module中的exports的值而不是exports。因此当exports = 521的时候拿不到结果
// exports.duanzi = duanzi可以是因为属于对象的引用数据(地址),modules同样可以访问到 exports = modules.exports = {}
module.exports = {
duanzi,
lezi
}
|
1
2
3
4
5
6
7
8
9
10
11
| const modules = require('./01-暴露的方式')
console.log(modules)
console.log(modules.duanzi())
console.log(modules.lezi())
log:
{ duanzi: [Function: duanzi], lezi: [Function: lezi] }
我是个段子
undefined
我是个乐子
undefined
|
ps:
- 不加文件后缀也会直接匹配,如果同名会从js优先匹配
- 自定义后缀文件如.abc文件,默认为js文件
- 路径推荐使用相对路径’./’
导入文件夹
- 会检查是否有package.json文件是否存在,并且检查其main属性的指向文件。如果存在则导入,不存在则报错。
如package.json文件不存在,则默认检查文件夹下的index.js或者index.json文件,没有则报错
1
2
3
| {
"main": "./app.js"
}
|
require的导入原理
- 将相对路径转为绝对路径,定位目标文件
- 缓存检测
- 读取文件代码
- 包裹为一个函数,然后执行
- 缓存结果
- 返回module.exports的值
包管理工具
express框架
创建express项目:npm init => npm i express
路由
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
app.get('/', (req, res) => {
res.end('hello express')
})
app.get('/home', (req, res) => {
res.end('home')
})
app.post('/login', (req, res) => {
res.end('login login')
})
// 所有请求都可跳转
app.all('/test', (req, res) => {
res.end('test,all request is ok')
})
// 通配符 404响应,兜底路由规则
app.all('*', (req, res) => {
res.end('404 Not Found')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
获取请求参数
兼容原生,大同小异
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| // 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
app.get('/', (req, res) => {
// 原生操作
console.log('[ req.url ] >', req.url)
console.log('[ req.method ] >', req.method)
console.log('[ req.httpVersion ] >', req.httpVersion)
console.log('[ req.headers ] >', req.headers)
// express 操作
console.log('[ req.path ] >', req.path)
console.log('[ req.query ] >', req.query)
// 获取IP
console.log('[ req.ip ] >', req.ip)
// 获取请求头
console.log('[ req.host ] >', req.host)
res.end('hello express')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
log:----------------------
[ req.url ] > /?a=100&b=200
[ req.method ] > GET
[ req.httpVersion ] > 1.1
[ req.headers ] > {
host: '127.0.0.1:3000',
connection: 'keep-alive',
'cache-control': 'max-age=0',
'sec-ch-ua': '"Chromium";v="154", "Google Chrome";v="154", "Not A(Brand";v="99"',
// ...省略
}
[ req.path ] > /
[ req.query ] > [Object: null prototype] { a: '100', b: '200' }
[ req.ip ] > ::ffff:127.0.0.1
[ req.host ] > 127.0.0.1:3000
|
获取路由参数 (params)
示例:京东商城的商品访问,路由路径皆为各个商品的id.html,因此通过动态路由匹配,获取对应路由信息,从而匹配对应的商品资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
// app.get('/1000031.html', (req, res) => {
// res.setHeader('content-type', 'text/html;charset=utf-8')
// res.end('商品详情')
// })
// 但是商品有成千上万,因此要做动态路由的匹配
app.get('/:id.html', (req, res) => {
// 获取url 路由参数,字段名字'.id'需要和路由保持一致
console.log('[ req.params.id ] >', req.params.id)
res.setHeader('content-type', 'text/html;charset=utf-8')
res.end('商品详情')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
路由参数练习
require('./xxx.json'):一次性加载、不会实时更新,适合项目固定配置(服务启动后json不再改动)。fs读取 + JSON.parse:每次读取都拿最新文件内容,适合运行中会修改的json数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // 导入express
const express = require('express')
const { singers } = require('./05-singers.json') // 为何不用fs见上文
// 创建应用对象
const app = express()
// 创建路由
app.get('/singers/:id.html', (req, res) => {
let result = singers.find(item => {
if (item.id == req.params.id) {
return true
}
})
console.log('[ result ] >', result)
if (!result) {
res.end('404 没有找到对应歌手')
return
}
res.end(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h2>${result.singer_name}</好>
<br>
<img src="${result.singer_pic}"/>
</body>
</html>
`)
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
设置响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
app.get('/response', (req, res) => {
// 原生响应
// res.statusCode = 200 // 默认状态码为200
// res.statusMessage = 'i love u'
// res.setHeader('Content-Type', 'text/plain;charset=utf-8')
// res.write('love')
// res.end('响应报文')
// express响应
// res.status(500)
// res.set('aaa', 'bbb')
// res.send('你好 Express') // express的send方法内置utf-8配置
// 连贯设置
// res.status(500).set('abc', 'def').send('这都是 OK 的')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
其他响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
app.get('/other', (req, res) => {
// 跳转响应
// res.redirect('http://atguigu.com')
// 下载响应
// res.download(__dirname + '/package.json')
// JSON响应
// res.json({
// name: 'linda',
// slogon: '我想要工作'
// })
//响应文件内容
// res.sendFile(__dirname + '/02-form.html')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
express中间件
- 概念
- 本质是一个回调函数
- 参数:和路由函数类似,(req,res)
- 作用
- 类型
- 全局中间件
访问日志打印(如下例子)
全局错误捕获
CORS跨域处理(设置响应头,放行跨域请求)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| /**
* 记录url和ip
*/
// 导入express
const express = require('express')
const fs = require('fs')
const path = require('path')
// 创建应用对象
const app = express()
// 声明中间件函数
function recordMiddleware(req, res, next) {
// 获取url和ip
let { url, ip } = req
console.log('[ url,ip ] >', url, ip)
// 将信息保存于文件中
// fs.appendFileSync('./08-access.log', `${url} ${ip}\r\n`) // ❌!!!这里写的时候路径直接用了'./',回顾上文这里文件创建会取决于cmd的执行目录
fs.appendFileSync(path.resolve(__dirname, '08-access.log'), `${url} ${ip}\r\n`)
// 调用next,必须加
next()
}
// 全局中间件挂载
app.use(recordMiddleware)
// 创建路由
app.get('/home', (req, res) => {
res.send('前台首页')
})
app.get('/admin', (req, res) => {
res.send('后台首页')
})
// Express5 无法使用之前的app.all('*',()=>{})来做404。
app.use((req, res) => {
res.status(404).send('404 页面不存在')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
- 路由中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| /**
* 针对/admin,/setting的请求,要求url携带code=521参数,如未携带显示暗号错误
*/
// 导入express
const express = require('express')
// 创建应用对象
const app = express()
// 创建路由
app.get('/home', (req, res) => {
res.send('主页')
})
// 创建路由中间件
let checkCodeMiddleware = (req, res, next) => {
if (req.query.code === '521') {
next()
} else {
res.send('暗号错误')
}
}
app.get('/setting', checkCodeMiddleware, (req, res) => {
res.send('后台设置')
})
app.get('/admin', checkCodeMiddleware, (req, res) => {
res.send('后台首页')
})
// Express5 无法使用之前的app.all('*',()=>{})来做404。
app.use((req, res) => {
res.status(404).send('404 页面不存在')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
- 静态资源中间件
- 区别于“08-静态资源服务”栏目的内容,这里更加便捷了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 导入express
const express = require('express')
const path = require('path')
// 创建应用对象
const app = express()
// 静态资源中间件设置
app.use(express.static(path.resolve(__dirname, './public')))
// 创建路由
app.get('/', (req, res) => {
res.send('我才是真的表格!!!')
})
// Express5 无法使用之前的app.all('*',()=>{})来做404。
app.use((req, res) => {
res.status(404).send('404 页面不存在')
})
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
- 静态资源匹配下,如果根路由没有返回,则会自动匹配public下的index.html文件
- 如果静态资源匹配和路由匹配相同,谁先响应匹配谁,看同步函数的代码顺序
获取请求体数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/**
* 搭建HTTP服务
*
* GET /login 登录页
* POST /login 获取表单中的用户名和密码
*/
const express = require('express')
const path = require('path')
// 导入中间件
const bodyParser = require('body-parser')
// 创建实例
const app = express()
// 解析JSON格式的请求体中间件
const jsonParser = bodyParser.json()
// 解析querystring格式的请求体中间件
const urlencodeedParser = bodyParser.urlencoded({ extended: false })
app.get('/login', (req, res) => {
res.sendFile(path.resolve(__dirname, './11-form.html'))
})
app.post('/login', urlencodeedParser, (req, res) => {
console.log('[ req.body ] >', req.body)
res.send('获取表单响应')
})
app.listen(3000, () => {
console.log('在监听3000端口')
})
|
防盗链
- 防止外部网站盗用我们的网站资源
- 如请求头的referer字段,显示了请求域名是多少,判断为外来域名,因此图片不返回响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| const express = require('express')
const path = require('path')
const app = express()
// 防盗链中间件
app.use((req, res, next) => {
// 检测请求头中的referer 是否为127.0.0.1
let referer = req.get('referer')
console.log('[ referer ] >', referer)
if (referer) {
const url = new URL(referer)
let hostname = url.hostname
if (hostname !== '127.0.0.1') {
res.status(404).send('<h1>404 NOT FOUND</h1>')
return
}
}
next()
})
app.use(express.static(path.resolve(__dirname, './public')))
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(3000, () => {
console.log('Server is running on port 3000')
})
|
此处防盗链防止了127.0.0.1以外的域名访问静态资源,如果想只防盗图片的访问的话,则需要再做一层校验
路由模块化 *(开发范式)
- 路由的模块化更容易维护,如中间件和相同类似的路由维护。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // ./routes/homeRouter.js
// 导入express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 创建路由规则
router.get('/home', (req, res) => {
res.send('主页')
})
router.get('/home/search', (req, res) => {
res.send('路由搜索')
})
// 暴露router
module.exports = router
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| /**
* 针对/admin,/setting的请求,要求url携带code=521参数,如未携带显示暗号错误
*/
// 导入express
const express = require('express')
const homeRouter = require('./routes/homeRouter')
const adminRouter = require('./routes/adminRouter')
// 创建应用对象
const app = express()
// 路由导入
app.use(homeRouter)
app.use(adminRouter)
app.listen(3000, () => {
console.log('express服务已启动,端口3000正在监听中...')
})
|
- 路由导入模板时可增加路由前缀
app.use('home',homeRouter),此时homeRouter.js的文件可变化为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // ./routes/homeRouter.js
// 导入express
const express = require('express')
// 创建路由对象
const router = express.Router()
// 创建路由规则
router.get('/', (req, res) => {
res.send('主页')
})
router.get('/search', (req, res) => {
res.send('路由搜索')
})
// 暴露router
module.exports = router
|
ejs模板引擎(了解即可)
EJS:嵌入式JS模板引擎,Express服务端渲染SSR,类比Java Thymeleaf / Python Jinja2 学习目标:看懂基础语法,理解SSR思想,新项目基本不用
- 安装
- Express配置
1
2
3
4
5
| const express = require('express')
const app = express()
app.set('view engine','ejs') // 设置模板引擎
app.set('views','./views') // 模板存放目录,默认views
app.listen(3000,()=>console.log('服务启动'))
|
目录:项目/views/xxx.ejs
- 路由渲染
1
2
3
4
| app.get('/',(req,res)=>{
// res.render(模板名, 传给模板的数据)
res.render('index',{name:'小明',arr:['A','B']})
})
|
- EJS标记
| 标记 | 作用 |
|---|
<% js代码 %> | 执行JS,不输出(if/for) |
<%= 变量 %> | 输出变量,自动转义HTML,防XSS ✅推荐 |
<%- 变量 %> | 原样输出HTML标签,不安全慎用 |
<%- include('part/header') %> | 引入公共模板片段 |
- 模板示例 views/index.ejs
1
2
3
4
5
6
7
8
9
| <p>名字:<%= name %></p>
<% if(name){ %>
<p>有用户名</p>
<% } %>
<ul>
<% for(let item of arr){ %>
<li><%= item %></li>
<% } %>
</ul>
|
- 要点总结
- 模板JS运行在服务器,不是浏览器,即SSR
res.render 自动读取ejs,拼接完整HTML返回浏览器- 适用:老项目;现代开发优先前后端分离,SSR用Next/Nuxt
<%- include() 复用页面头部底部
- 我发现res.sendFile和res.render都可以响应html文件,区别就是render属于服务端渲染,后续可加参数进行模板传值,而sendFile是读取磁盘上的静态资源文件
express-generator工具,快速搭建express框架项目
- 不需要再重复一点一点敲,直接在脚手架基础上开发即可
1
2
3
4
5
6
7
8
| //方式1.
npx express-generator
//方式2.
npm install -g express-generator
//创建方式
express -e 15-generator //即可创建出该命名项目,-e表示支持ejs
|
处理文件上传
npm i formidable,这里使用的是npm install formidable@2版本,目前是3
基于express-geanrator搭建框架,在index.js做文件上传路由操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| var express = require('express')
var router = express.Router()
const formidable = require('formidable')
/* GET home page. */
router.get('/', function (req, res, next) {
res.render('index', { title: 'Express' })
})
//显示网页的表单
router.get('/portrait', function (req, res) {
res.render('portrait')
})
router.post('/portrait', function (req, res) {
//创建form对象
const form = formidable({
multiples: true,
// 设置上传文件的保存目录
uploadDir: __dirname + '/../public/images',
// 保存文件后缀
keepExtensions: true
})
// 解析请求报文
form.parse(req, (err, fields, files) => {
if (err) {
next(err)
return
}
// console.log('[ fields ] >', fields) // 保存非文件数据的字段 {username:'abc'}
// console.log('[ files ] >', files) // 保存文件数据的字段
// 服务器保存该图片的访问url
// /images/bzu57fhd4mc7t2x16kx294qz5.jpg
let url = '/images/' + files.portrait.newFilename // 将来将此数据保存在数据库中
res.send(url)
})
})
module.exports = router
|
对应的portrait.ejs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 文件上传
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>文件上传</title>
</head>
<body>
<!-- enctype="multipart/form-data" 文件上传的必须属性 -->
<form action="/portrait" method="post" enctype="multipart/form-data">
用户名
<input type="text" name="username" />
头像
<input type="file" name="portrait" />
<button type="submit">上传</button>
</form>
</body>
</html>
|
唯一id标识npm包
1
2
| const { v4: uuidv4 } = require('uuid')
const id = uuidv4() // 示例:b3a2f078-901c-4221-b978-27218ac34123
|
1
2
| const { nanoid } = require('nanoid')
const id = nanoid(10) // 10位短字符串
|