返回
Featured image of post Node.js 全栈学习笔记(持续更新)

Node.js 全栈学习笔记(持续更新)

持续记录我的 Node.js 学习过程,目前学到 Buffer、fs、HTTP、CommonJS 和 Express,包含路由、中间件、EJS 与文件上传练习。

这是一篇还在更新的 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

  1. 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('写入成功')
    }
})
  1. writeFileSync 同步写入,主线程停滞,等同步写入完再继续执行
1
fs.writeFileSync('./data.txt', 'text')
  1. 第三个参数{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('写入成功')
    }
})
  1. appendFile 异步追加
  2. appendFileSync 同步追加
1
fs.appendFileSync('./座右铭.txt', '\r\n温故而知新,可以为师矣')
  1. 流式写入 *

程序打开一个文件是需要消耗资源的,流式写入可以减少打开关闭文件的次数。

流式写入方式适用于大文件写入或者频繁写入的场景,writeFile适合于写入频率较低的场景 如

  • 方案 1【分片上传,多请求并发】:前端slice切文件,并发发起多个请求上传分片;后端接收每一片,流式写入临时分片。优点:断网只重传失败分片,支持断点续传;缺点:请求多,要做并发控制。

  • 方案 2【单请求 body 流式上传】:前端把文件作为请求 body 的流,单个 HTTP 请求源源不断推送数据,后端同步流式写入磁盘。缺点:一旦网络中断,本次上传全部作废,无法断点续传。

 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. 文件写入场景
  • 下载文件
  • 安装软件
  • 程序日志、如git
  • 视频录制

需持久化保存数据的时候,就应该想到文件写入

文件读取

  1. 异步和同步读取
 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())
  1. 读取文件应用场景
  • 电脑开机
  • 程序运行
  • 编辑器打开文件
  • 查看图片
  • 播放视频、音乐
  • git查看日志,git log
  • 上传文件,(读取文件,然后传到服务端)
  • 查看聊天记录
  1. 流式读取
 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. 创建文件
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. 递归创建
1
2
3
4
5
6
7
8
//递归创建
fs.mkdir('./a/b/c', { recursive: true }, err => {
    if (err) {
        console.log('创建失败')
        return
    }
    console.log('创建成功')
})
  1. 读取文件夹
1
2
3
4
5
6
7
fs.readdir('../资料', (err, data) => {
    if (err) {
        console.log('读取失败')
        return
    }
    console.log(data)
})
  1. 删除文件夹
1
2
3
4
5
6
7
fs.rmdir('./html', (err, data) => {
    if (err) {
        console.log('读取失败')
        return
    }
    console.log(data)
})
  1. 递归删除
  • 不推荐使用
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模块

  1. 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
  1. 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
  1. sep 分隔符,获取当前操作系统的路径分隔符
1
2
3
const path = require('path')

console.log(path.sep) // window 为 ‘\’  linux‘/’
  1. parse方法 解析路径并且返回对象
1
2
const path = require('path')
console.log(path.parse(__filename))
  1. 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('服务已经启动')
})

当文件有很多的时候,就要再次优化,见后文

静态资源服务

  1. 静态资源的封装导入 目录结构:

静态资源目录结构

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('服务已经启动')
})

实现静态资源配置,静态资源有变化不需要和往常一样重复操作导入

  1. 静态资源的url引用 url、src等,常用<link rel="stylesheet" href="/css/index.css" />,’./‘的话会有隐藏bug,后文模板引擎部分会讲,直接用’/‘可以防止域名或者端口发生变化,从而直接拼接。

  2. 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
  1. err错误处理

文件不存在时的 ENOENT 错误信息

通过读取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:

  1. 不加文件后缀也会直接匹配,如果同名会从js优先匹配
  2. 自定义后缀文件如.abc文件,默认为js文件
  3. 路径推荐使用相对路径’./’

导入文件夹

  1. 会检查是否有package.json文件是否存在,并且检查其main属性的指向文件。如果存在则导入,不存在则报错。 如package.json文件不存在,则默认检查文件夹下的index.js或者index.json文件,没有则报错
1
2
3
{
    "main": "./app.js"
}

require的导入原理

  1. 将相对路径转为绝对路径,定位目标文件
  2. 缓存检测
  3. 读取文件代码
  4. 包裹为一个函数,然后执行
  5. 缓存结果
  6. 返回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中间件

  1. 概念
  • 本质是一个回调函数
  • 参数:和路由函数类似,(req,res)
  1. 作用
  • 封装公共操作,简化代码
  1. 类型
  • 全局中间件
  • 路由中间件
  1. 全局中间件
  • 常用场景:

访问日志打印(如下例子)

全局错误捕获

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. 路由中间件
  • 常用场景:页面检验登录信息
 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正在监听中...')
})
  1. 静态资源中间件
  • 区别于“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正在监听中...')
})
  • 注意点
  1. 静态资源匹配下,如果根路由没有返回,则会自动匹配public下的index.html文件
  2. 如果静态资源匹配和路由匹配相同,谁先响应匹配谁,看同步函数的代码顺序

获取请求体数据

  • 使用中间件body-parser
1
npm i body-parser
 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思想,新项目基本不用

  1. 安装
1
npm i express ejs
  1. 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. 路由渲染
1
2
3
4
app.get('/',(req,res)=>{
  // res.render(模板名, 传给模板的数据)
  res.render('index',{name:'小明',arr:['A','B']})
})
  1. EJS标记
标记作用
<% js代码 %>执行JS,不输出(if/for)
<%= 变量 %>输出变量,自动转义HTML,防XSS ✅推荐
<%- 变量 %>原样输出HTML标签,不安全慎用
<%- include('part/header') %>引入公共模板片段
  1. 模板示例 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>
  1. 要点总结
  • 模板JS运行在服务器,不是浏览器,即SSR
  • res.render 自动读取ejs,拼接完整HTML返回浏览器
  • 适用:老项目;现代开发优先前后端分离,SSR用Next/Nuxt
  • <%- include() 复用页面头部底部
  1. 我发现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包

  • uuid
1
2
const { v4: uuidv4 } = require('uuid')
const id = uuidv4() // 示例:b3a2f078-901c-4221-b978-27218ac34123
  • nanoid
1
2
const { nanoid } = require('nanoid')
const id = nanoid(10) // 10位短字符串
评论区 正在连接 GitHub Discussions…
使用 Hugo 构建
主题 Stack 由 Jimmy 设计