当客户端需要数据时,它会发送 HTTP
请求到某台服务器来获取资源。提供这些资源的服务器就叫做 Web 服务器,也可以称为 web server
。目前有很多开源的 Web 服务器,比如 Nginx
、Apache
、Tomcat
等
创建服务器
// app.js => 使用node.js编写的服务器入口文件一般为app.js
// 所以createServer的返回值也一般被命名为 app 或 server
import http from 'node:http'
// 创建服务器
// 在 `createServer` 方法中,我们可以传入一个回调函数。这个回调函数会在每次客户端发送请求时执行
const server = http.createServer((req, res) => {
// req 是请求对象,包含请求行、请求头、请求体等信息,是可读流
// res 是响应对象,包含响应的所有信息,比如响应头、响应体等,可以用于向客户端返回数据,是可写流
res.end('Hello World')
})
// 监听端口
// + 参数是端口号
// - 1024 以下的端口通常是操作系统分配给特殊服务的,尽量不要使用。
// - 1024 到 65535 之间的端口可以自由选择。例如,开发中常用的端口有 `3000`、`8000` 等。
// + 回调函数是当服务器启动成功后执行的回调函数
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀');
})
createServer
通过createServer
方法可以创建一个服务器实例,其底层本质是通过new Server
来实现的
所以我们可以同时创建多个服务器实例,他们彼此之间是相互独立的
import http from 'node:http'
const server1 = new http.Server((req, res) => {
res.end('你好')
})
const server2 = http.createServer((req, res) => {
res.end('你好')
})
server1.listen(3000, () => {
console.log('服务器1已成功开启,端口号:3000 🚀');
})
server2.listen(4000, () => {
console.log('服务器2已成功开启,端口号:4000 🚀');
})
参数
http.createServer
方法接收一个回调函数作为参数,该回调函数会在每次客户端发送请求时被调用
这个回调函数接收两个参数:
req
:请求对象,表示客户端的 HTTP 请求。它包含了请求行、请求头、请求体等信息,并且是一个可读流。res
:响应对象,表示服务器的 HTTP 响应。它用于向客户端返回数据,包含响应头、响应体等信息,并且是一个可写流。
listen
listen
方法用于启动 HTTP 服务器,并指定服务器监听的端口号和主机地址。它接收三个参数:
-
端口号:
-
范围为 0 到 65535:
- 0-1023:系统保留端口,通常用于特定服务(如 HTTP 的 80,HTTPS 的 443),不建议使用。
- 1024-65535:用户可用端口,常见的开发端口如
3000
、8000
、8080
等是比较好的选择,便于记忆和标准化。
-
如果端口号设置为
0
或者省略,系统会自动分配一个可用端口-
分配的端口号可以通过
server.address().port
获取。 -
server.listen(0, () => { console.log(`服务器已成功开启,端口号:${server.address().port} 🚀`); })
-
-
-
主机地址:
- 如果省略,默认行为是监听
0.0.0.0
,即所有可用的 IPv4 地址。这意味着服务器可以被本机和外部设备访问。 - 表示服务器监听的网络地址,常用值包括:
localhost
:主机名,解析为127.0.0.1
(IPv4)或::1
(IPv6),仅限本机访问。127.0.0.1
:IPv4 回环地址,仅允许本机访问。0.0.0.0
:监听所有可用的 IPv4 地址,允许外部设备通过网络访问。
- 如果需要支持 IPv6,可以使用
::
表示监听所有 IPv6 地址。
- 如果省略,默认行为是监听
-
回调函数:
-
在服务器成功启动后执行,用于处理初始化逻辑或打印日志信息。例如:
server.listen(3000, '127.0.0.1', () => { console.log('服务器已成功开启,运行于 127.0.0.1:3000 🚀'); })
-
回调函数不会处理错误情况,需要通过
server.on('error', callback)
捕获启动错误(如端口被占用)。server.on('error', (err) => { console.log(err) })
-
req
import http from 'node:http'
const server = http.createServer((req, res) => {
// node是服务器脚本,运行于服务器,所以输出在终端,不在浏览器
console.log('请求的 URL:', req.url);
console.log('请求的方法:', req.method);
console.log('请求头信息:', req.headers);
res.end('Request received');
});
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀');
});
输出结果
假设客户端发送了一个 GET 请求,路径为 /home
,控制台可能会输出如下内容:
请求的 URL: /home
请求的方法: GET
请求头信息: {
host: 'localhost:8000',
connection: 'keep-alive',
'user-agent': 'PostmanRuntime/7.29.0',
accept: '*/*'
# ...
}
当使用
createServer
创建一个 HTTP 服务器时,传入的回调函数会在接收到任何 URL 请求时触发。因此,当我们访问首页
/
时,浏览器可能会额外发送对/favicon.ico
的请求,这意味着该回调函数可能会被触发多次。而具体是否会被调用多次,是由浏览器的行为自行决定的
区分请求路径和方法
const server = http.createServer((req, res) => {
const url = req.url;
const method = req.method;
// 默认情况下,node没有指定编码,客户端会认为是ASCII编码,所以会导致中文乱码
// 所以需要设置响应头,指定内容类型和编码, 告诉客户端返回的内容是utf-8编码的html
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
if (url === '/login' && method === 'POST') {
res.end('登录成功');
} else if (url === '/login') {
res.end('不支持的请求方式,请使用 POST');
} else if (url === '/product') {
res.end('商品列表');
} else {
res.end('不存在的URL,请检查路径地址');
}
});
解析请求参数
import http from 'node:http'
import url from 'node:url'
import querystring from 'node:querystring'
// 假设请求的URL为:http://localhost:3000/home?offset=10&size=20
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 解析 URL => url内置模块使用和浏览器的URL使用是一致的
const urlInfo = url.parse(req.url); // 解析 URL
// 解析查询字符串 => 将查询字符串转换为对象 「 值为字符串类型值 」
const query = querystring.parse(urlInfo.query); // 解析查询字符串
// 注意:req.url 是 path + query,不是完整的URL
console.log(req.url) // /home?offset=10&size=20
console.log('Path:', urlInfo); // 输出路径
console.log('Query:', query); // 输出查询字符串解析结果
if (urlInfo.pathname === '/home') {
const offset = query.offset ?? 0;
const size = query.size ?? 10;
res.end(`偏移量: ${offset}, 数据大小: ${size}`);
} else {
res.end('未知路径');
}
});
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀');
});
自 Node.js v10.0.0 起,官方开始推荐使用原生的 URL
和 URLSearchParams
接口来解析和处理 URL,而不是使用传统的 url
和 querystring
模块。
尽管这些旧模块仍然可以使用,但它们已经被标记为 Legacy(遗留模块),不推荐在新的代码中使用。
import http from 'node:http'
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 构建完整URL
const urlInfo = new URL(req.url, `http://${req.headers.host}`);
const params = urlInfo.searchParams;
console.log('Path:', urlInfo.pathname); // 输出路径
// searchParams 是 URLSearchParams 对象,是一个可迭代方法
// 通过 Object.fromEntries 将可迭代对象转换为对象
console.log('Query:', Object.fromEntries(params)); // 输出查询字符串解析结果
if (urlInfo.pathname === '/home') {
const offset = params.get('offset') || 0;
const size = params.get('size') || 10;
res.end(`偏移量: ${offset}, 数据大小: ${size}`);
} else {
res.end('未知路径');
}
});
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀');
});
解析请求体
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// method 不区分大小写,内部会自动全部转大写
if (req.url === '/login' && req.method === 'POST') {
let body = ''
// req 是一个可读流,body 是流中的数据
req.on('data', (chunk) => {
// chunk是buffer类型数据,在字符串拼接时会自动转字符串类型值
// 也可以通过req.setEncoding('utf8')或手动调用toString方法转字符串
body += chunk
})
req.on('end', () => {
// body是JSON格式字符串类型值,解析为对象
const info = JSON.parse(body)
if (info.username === 'admin' && info.password === '123456') {
res.end('登录成功')
} else {
res.end('登录失败')
}
})
} else {
res.end('Not Found')
}
})
常见请求头
-
Content-Type:
表示请求数据的类型,例如:-
application/x-www-form-urlencoded
-
application/json
-
text/plain
-
text/html
-
multipart/form-data
(用于二进制数据上传 和 表单上传)
-
-
Content-Length:
表示请求数据的长度。对于文件上传,数据可能较大,无法一次性通过data
事件读取完毕,需要多次监听data
事件。当读取的数据长度达到Content-Length
的值时,说明数据已经读取完毕。浏览器会自动计算
Content-Length
的值 -
Connection: Keep-Alive:
在 HTTP/1.1 中,默认所有连接都是Keep-Alive
,不需要额外设置。在 Node.js 中,连接默认保持 5 秒钟。如果在此期间发送多次请求,使用的是同一个连接,从而提高访问性能。
-
Accept-Encoding:
指定客户端可以接受的压缩格式,例如:gzip
deflate
br
如果服务器有客户端可以接受的压缩格式类型文件,服务器会返回压缩后的文件,客户端会自动解压,提高传输效率。如果不存在,则返回非压缩格式文件
-
Accept:
指定客户端接受的文件格式。服务器可以根据此信息设置响应数据的格式,例如 UTF-8 编码来正确解析中文。 -
User-Agent:
包含客户端的信息,例如浏览器或 Postman 的版本。
res
import http from 'node:http'
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// res本质是可写流,所以可以通过write和end方法返回数据
// 响应方式一:使用 write 方法写入数据
res.write('Hello World');
res.write('哈哈');
// 响应方式二:使用 end 方法结束写入
// + res进行了特殊处理,不能通过close关闭写入流,只能通过end方法进行关闭
// + 如果我们没有调用 `end` 方法,客户端会一直等待响应结束
// + 这也就是为什么推荐客户端设置请求超时时间,避免一直等待
res.end('本次写入已结束');
});
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀');
});
此时浏览器会显示 => Hello World哈哈本次写入已结束
状态码
响应状态码,称为 HTTP 状态码。它是用来表示这次响应状态的一个数字代码。
HTTP 状态码有很多种,我们可以根据不同的情况,向客户端返回不同的状态码。
客户端可以根据服务器返回的状态码,判断当前请求的结果是什么。
常见的状态码及其含义:
状态码 | 状态描述 | 信息说明 |
---|---|---|
200 | OK | 客户端请求成功,服务器返回所请求的数据。 |
201 | Created | POST 请求成功,服务器创建了新的资源。 |
204 | No Content | 请求成功,但服务器没有返回任何内容。 |
206 | Partial Content | 服务器成功处理了部分请求(如分块下载)。 |
301 | Moved Permanently | 请求资源的 URL 已永久修改,响应中会给出新的 URL。 |
302 | Found | 请求资源的 URL 已临时修改,响应中会给出新的 URL。 |
304 | Not Modified | 资源未被修改,客户端可使用缓存版本。 |
307 | Temporary Redirect | 请求资源临时移动至新位置,HTTP 方法保持不变。 |
308 | Permanent Redirect | 请求资源永久移动至新位置,HTTP 方法保持不变。 |
400 | Bad Request | 客户端请求有误,服务器无法处理。 |
401 | Unauthorized | 未授权的请求,客户端需要提供认证信息。 |
403 | Forbidden | 客户端没有权限访问资源,即使已认证也被拒绝。 |
404 | Not Found | 服务器无法找到请求的资源。 |
405 | Method Not Allowed | 请求方法不被允许(如使用了不支持的 HTTP 方法)。 |
408 | Request Timeout | 请求超时,客户端未在规定时间内发送完整请求。 |
413 | Payload Too Large | 请求实体过大,服务器无法处理。 |
414 | URI Too Long | 请求的 URI 太长,服务器无法处理。 |
429 | Too Many Requests | 客户端发送的请求次数过多,触发了速率限制。 |
500 | Internal Server Error | 服务器遇到了未知错误,无法处理请求。 |
501 | Not Implemented | 服务器不支持请求的方法。 |
502 | Bad Gateway | 网关或代理服务器收到无效响应。 |
503 | Service Unavailable | 服务器暂时不可用(超载或维护)。 |
504 | Gateway Timeout | 网关或代理服务器超时,未收到上游服务器响应。 |
505 | HTTP Version Not Supported | 服务器不支持请求的 HTTP 版本。 |
网关
网关是请求的入口,接收客户端的请求并转发到不同的后端服务器,后端服务器处理请求后返回结果,网关再将结果返回给客户端。它充当一个中间层,连接客户端(外部)和服务器(内部)。
重定向
307 和 308 是 HTTP/1.1 标准中引入的,弥补了 302 和 301 在方法改变上的不明确性。
状态码 | 请求方法是否改变 | 是否永久改变 |
---|---|---|
301 | 改变 ( 新地址使用 ) | 永久重定向 客户端会缓存新URL,下次直接使用新URL进行访问 |
302 | 改变 ( 新地址使用 GET ) | 临时重定向 客户端不会缓存新URL,下次依旧使用旧URL进行访问 |
307 | 不改变(保持原方法) | 临时重定向 客户端不会缓存新URL,下次依旧使用旧URL进行访问 |
308 | 不改变(保持原方法) | 永久重定向 客户端会缓存新URL,下次直接使用新URL进行访问 |
设置状态码
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 设置状态码 和 状态消息
res.statusCode = 201
res.statusMessage = 'Created'
res.end('success');
});
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html; charset=utf-8')
// 在设置状态码的同时,定义其他的响应头信息。
// 参数一 状态码
// 参数二 状态消息 「 可以省略 」
// 参数三 响应头信息 「 可以省略 」
res.writeHead(201, 'Created', {
'Content-Type': 'text/html; charset=utf-8'
})
res.end('success');
});
响应头
当服务端返回中文内容,如果服务器没有明确指定返回结果的编码类型
- postman => 使用
utf-8
进行解析 - 浏览器 => 使用
ASCII
码进行解析
因此,在正常情况下,当我们给客户端返回结果时,需要明确告诉它返回结果的格式和编码类型
设置响应头部信息有两种方式:
-
方式一
- 我们可以通过
res.setHeader
方法来设置
res.setHeader('Content-Type', 'application/json; charset=UTF-8');
- 我们可以通过
-
方式二
- 使用
res.writeHead
方法,同时设置状态码和头部信息
res.writeHead(200, { // 此时可以设置多个响应头 'Content-Type': 'application/json; charset=UTF-8' });
- 使用
网络请求
Axios既可以在浏览器使用,也可以在node环境中使用。
- 在浏览器环境中,axios是基于XMLHttpRequest封装的
- 在Node.js中,axios是基于http模块封装的
所以,HTTP 模块除了能搭建服务器,还可以主动发送网络请求。
import http from 'node:http'
// 使用 http.get 方法发送GET请求
http.get('http://httpbin.org/get', (res) => {
let data = ''
// 回调函数中,res 是可读流,所以可以通过 on 方法监听事件
res.on('data', (chunk) => {
data += chunk
})
// 当数据流结束时,会触发 end 事件
res.on('end', () => {
console.log(data)
})
})
发送其它类型方法,必须通过request
方法。因为 HTTP 模块没有直接提供 post
等系列方法。
import http from 'node:http'
// 配置对象
const options = {
hostname: 'httpbin.org', // 主机名
path: '/post', // 请求路径
port: 80, // 端口号
method: 'POST', // 请求方法
headers: {
'Content-Type': 'application/json' // 请求头
}
};
// const 请求写入流 = http.request(配置对象, 处理响应读取流的回调)
const req = http.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
console.log('Response:', data);
});
});
// 发送请求体数据
// + JSON是通用标准,但不同编程语言对JSON的处理不尽相同
// + 所以网络传输过程中,使用的是JSON格式字符串,而不是JSON对象
req.write(JSON.stringify({ message: 'Hello, Server!' }));
// 告诉服务器,请求体数据已发送完毕
req.end();
options 可选配置项
配置项 | 作用 | 示例 |
---|---|---|
hostname | 指定目标服务器的主机名或 IP 地址。 | 'httpbin.org' |
port | 指定目标服务器的端口号。 | 80 或 443 |
path | 指定请求的路径,包括查询字符串(如需要)。 | '/post' 或 '/get?id=123' |
method | 指定 HTTP 请求方法(如 GET , POST , PUT , DELETE 等)。 | 'POST' |
headers | 指定请求头,用于传递额外的元信息。 | { 'Content-Type': 'application/json' } |
timeout | 指定请求的超时时间(以毫秒为单位)。 | 5000 |
protocol | 指定请求使用的协议(默认为 http: 或 https: )。 | 'http:' 或 'https:' |
示例 - 文件上传
multipart/form-data
是一种 HTTP 请求的编码方式,主要用于上传文件或提交表单数据。
它将数据分隔成多个部分,每部分包含自己的头信息(如 Content-Disposition
和 Content-Type
),并通过一个边界字符串(boundary
)将各部分分隔开。这种格式允许文件和表单字段同时提交。
----------------------------292794409779591642750678
Content-Disposition: form-data; name="avatar"; filename="node.png"
Content-Type: image/png
�PNG
IHDR `���.ZtEXtSoftwareAdobe ImageReadyq�e<(iTXtXML:com.adobe.xmp<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> <x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 5.6-c138 79.159824, 2016/09/14-01:09:01 "> <rdf:RDF
# ....
----------------------------292794409779591642750678--
在本例中
-
--------------------------292794409779591642750678
是自动生成的边界字符串,用于分隔数据块。- 这个边界字符串,假设叫
boundary
- 则 实际使用的分割符为
--<boundary>
- 使用
--<boundary>--
表示传输完毕
- 这个边界字符串,假设叫
-
Content-Disposition
指定了字段名(如"avatar"
)和文件名(如"node.png"
)。 -
Content-Type
表示文件的 MIME 类型(如image/png
)。 -
文件的实际内容以二进制数据的形式紧随其后。
为了存储上传的文件,必须从 multipart/form-data
格式中剔除字段名、头信息等不必要的数据,只保留文件的二进制内容。
:: code-group
import http from 'node:http'
import fsPromise from 'node:fs/promises'
import fs from 'node:fs'
import path from 'node:path'
// 将数组分割成多个子数组 「 默认每个子数组大小为 65536(64KB) 」
function splitArray(array, chunkSize = 65536) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize)); // 分割数组
}
return result;
}
const server = http.createServer(async (req, res) => {
if (req.url === '/upload' && req.method === 'POST') {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf']
const buffers = []
let writeStream = null
// 由于数据是以分段形式发送的,因此需要将每个数据块存入 Buffer 数组。
// 上传文件的时候,数据是分段发送的,每一个数据分段并不代表数据内容完整
// 数据分段内容不完整时,直接转换为字符串会导致字符编码器"猜测"字符的含义,而这种猜测可能是错误的,从而产生乱码或数据偏差。
// 这种情况下,再将字符串转换回 Buffer 时,数据可能会丢失或被破坏,从而无法正常解析。
// 虽然原则上,完整buffer转字符串,再转回buffer,是相等的 " 可以通过 buf.equals(buf2) 验证 "
req.on('data', chunk => buffers.push(chunk))
req.on('end', async () => {
try {
const contentType = req.headers['content-type']
// 解析 boundary
const boundaryMatch = contentType.match(/boundary=(.+)$/)
const boundary = Buffer.from('--' + boundaryMatch[1])
const endBoundary = Buffer.from('--' + boundaryMatch[1] + '--')
// 将所有数据块合并成一个完整的 Buffer
const bodyBuffer = Buffer.concat(buffers)
// 找到 boundary 的起始位置
// - +2 是因为boundary后边是换行符 \r\n 「 2个字节 」
// - start 是 boundary后 的起始位置 「 即boundary后的第二行 」
// - 不能使用trim方法,因为trim是字符串方法,不是buffer方法。使用时会隐式转换为字符串,从而产生乱码 「 数据误差 」
let start = bodyBuffer.indexOf(boundary) + boundary.length + 2 // 跳过 \r\n
// 找到 endBoundary 的起始位置
let end = bodyBuffer.indexOf(endBoundary)
while (start < end) {
// 找下一个 boundary
const nextBoundary = bodyBuffer.indexOf(boundary, start)
let partEnd = nextBoundary === -1 ? end : nextBoundary - 2 // -2 去掉前面的 \r\n
// 获取当前数据分段 => subarray(start, end) 基于 [srart, end) 生成一个新缓存区 " 类似于数组的 slice 方法 "
const part = Buffer.from(bodyBuffer.subarray(start, partEnd))
// 解析 header 和内容 => 根据 boundary 分割的文件,header 和 content 之间会有一段 \r\n\r\n " 4个字节 ",基于此进行分割
const headerEnd = part.indexOf('\r\n\r\n')
if (headerEnd === -1) break
const header = part.subarray(0, headerEnd).toString()
const content = part.subarray(headerEnd + 4)
const filenameMatch = header.match(/filename=\"(.+?)\"/)
const mimeMatch = header.match(/Content-Type: (.+)/)
if (filenameMatch && mimeMatch) {
const filename = filenameMatch[1]
const mimetype = mimeMatch[1].trim()
if (!allowedMimeTypes.includes(mimetype)) {
start = nextBoundary === -1 ? end : nextBoundary + boundary.length + 2
continue
}
// 确保 uploads 目录存在
try {
await fsPromise.access('./uploads')
} catch {
await fsPromise.mkdir('./uploads')
}
// 获取文件扩展名
const fileExtname = path.extname(filename)
const fileName = path.basename(filename, fileExtname)
const filePath = path.join('./uploads', `${fileName}-${Date.now()}${fileExtname}`)
// 手动分块
const chunks = splitArray(content)
writeStream = fs.createWriteStream(filePath)
// 模拟流式写入
chunks.forEach(chunk => {
writeStream.write(chunk)
})
writeStream.end()
}
start = nextBoundary === -1 ? end : nextBoundary + boundary.length + 2
}
res.end('上传成功')
} catch (error) {
res.statusCode = 500
res.end('上传失败')
}
})
} else {
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀')
})
import http from 'node:http'
import fs from 'node:fs'
function splitArray(array, chunkSize = 65536) {
const result = [];
for (let i = 0; i < array.length; i += chunkSize) {
result.push(array.slice(i, i + chunkSize));
}
return result;
}
const server = http.createServer((req, res) => {
if (req.url === '/upload' && req.method === 'POST') {
// 将req流当做二进制字符串进行解析 => 此时,一个字节转一个字符,不存在猜测问题,因此可以正常操作二进制流
req.setEncoding('binary')
const allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf']
let data = ''
req.on('data', (chunk) => {
data += chunk
})
req.on('end', () => {
const boundaryStr = req.headers['content-type'].split('=')[1].trim()
const boundary = '--' + boundaryStr
const endBoundary = boundary + '--'
const boundaryIndex = data.indexOf(boundary) + boundary.length
const endBoundaryIndex = data.indexOf(endBoundary)
const fileData = data.slice(boundaryIndex, endBoundaryIndex).trim()
const mimeType = fileData.match(/Content-Type: (.+)/)[1]
if (!allowedMimeTypes.includes(mimeType)) {
res.end('不支持的文件类型')
return
}
const [header, content] = fileData.split(`Content-Type: ${mimeType}`).map(item => item.trim())
const filename = header.match(/filename="(.+?)"/)[1]
const [file, ext] = filename.split('.')
const writeStream = fs.createWriteStream(`./uploads/${file}-${Date.now()}.${ext}`)
const chunks = splitArray(content, 65536)
for (const chunk of chunks) {
// 以二进制流的形式输入
writeStream.write(chunk, 'binary')
}
writeStream.end()
res.end('上传成功')
})
} else {
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀')
})
::
原生操作非常的麻烦,所以我们一般使用专门的库来解析 multipart/form-data
格式。
busboy
是一个轻量级的库,专门用于解析multipart/form-data
格式,并且可以直接与原生http
模块配合使用。multer
是专门为Express
和Koa
等框架设计的文件上传中间件,无法直接与原生http
模块配合使用。
具体使用,查看官方文档即可
import http from 'node:http'
import fs from 'node:fs'
// busboy => 勤杂工
import Busboy from 'busboy'
const server = http.createServer((req, res) => {
if (req.url === '/upload' && req.method === 'POST') {
const busboy = Busboy({ headers: req.headers })
// file是文件上传事件,当且仅当formData中包含文件时,才会触发
// + name => 字段名
// + file => 文件流
// + info => 文件信息 「 包含了文件名,文件大小,MIME类型等 」
// busbuy还有其余事件 如:
// + 'field' => 当formData中包含普通字段时触发
// + 'close' => 当busboy解析完毕关闭时触发
busboy.on('file', (name, file, info) => {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!info.filename || !allowedMimeTypes.includes(info.mimeType)) {
// 如果没有文件名或文件类型不支持,跳过文件
// resume <=> 恢复,继续,重新回到,重返回 | 概要,摘要
return file.resume();
}
// 创建写入流 并指定写入位置
// 1. 如果文件存在,则覆盖
// 2. 如果写入目录不存在,就报错
const writeStream = fs.createWriteStream(`./uploads/${info.filename}`)
// 在读取流和写入流之间建立一个管道
file.pipe(writeStream)
// 写入流上传完毕,就是图片上传完毕
writeStream.on('finish', () => {
res.end('上传成功')
})
})
// 将req流数据交给busboy进行处理
req.pipe(busboy)
} else {
res.end('Not Found')
}
})
server.listen(3000, () => {
console.log('服务器已成功开启,端口号:3000 🚀')
})