一、认识 Stream 流
1. 什么是流
流(Stream)是连续字节数据的抽象 ,数据会像水流一样分段传输。
传统 readFile/writeFile 会一次性将整个文件加载到内存,大文件(视频、图片)极易造成内存溢出 。而流支持分段读写、暂停、恢复、精准控制读取位置,是处理大文件的最优方案。
所有流都是 EventEmitter 实例,Node.js 内置 4 种基础流:
- Readable:可读流(读取数据,如读取文件)
- Writable:可写流(写入数据,如写入文件)
- Duplex:双向流(同时可读可写,如网络套接字)
- Transform:转换流(读写时修改数据,如压缩)
下面重点讲解最常用的 Readable、Writable。
2. Readable 可读流(读取文件)
使用 fs.createReadStream 创建可读流,核心参数:
start:读取起始位置(字节)end:读取结束位置(字节)highWaterMark:单次读取字节数,默认 64KB
js
const fs = require('fs');
// 创建可读流:读取 foo.txt,从第3字节到第8字节,每次读4字节
const readStream = fs.createReadStream('./foo.txt', {
start: 3,
end: 8,
highWaterMark: 4
});
// 文件打开事件
readStream.on('open', (fd) => {
console.log('文件已打开');
});
// 分段读取数据(核心事件)
readStream.on('data', (data) => {
console.log('读取到数据:', data.toString('utf-8'));
// 暂停读取 2 秒
readStream.pause();
setTimeout(() => {
readStream.resume(); // 恢复读取
}, 2000);
});
// 读取完成
readStream.on('end', () => {
console.log('文件读取完毕');
});
// 流关闭
readStream.on('close', () => {
console.log('文件流已关闭');
});
3. Writable 可写流(写入文件)
使用 fs.createWriteStream 创建可写流,核心参数:
flags:w覆盖写入、a+追加写入start:写入起始位置
注意:可写流不会自动关闭 ,必须手动调用 close() 或 end()。
js
const fs = require('fs');
// 创建可写流:追加写入,从第8字节开始写
const writeStream = fs.createWriteStream('./foo.txt', {
flags: 'a+',
start: 8
});
writeStream.on('open', () => {
console.log('写入流已打开');
});
// 写入数据
writeStream.write('你好啊', (err) => {
if (err) console.error('写入失败:', err);
else console.log('片段写入成功');
});
// finish:所有数据写入完成(end 触发)
writeStream.on('finish', () => {
console.log('全部内容写入完成');
});
// close:流真正关闭
writeStream.on('close', () => {
console.log('写入流已关闭');
});
// end:写入收尾数据 + 自动关闭流(等价 write + close)
writeStream.end(' Hello World');
4. pipe 管道(流的拷贝)
pipe 是流的核心方法,自动将可读流数据流转到可写流 ,无需手动监听 data 事件,简化文件拷贝逻辑。
js
const fs = require('fs');
// 源文件可读流、目标文件可写流
const read = fs.createReadStream('./foo.txt');
const write = fs.createWriteStream('./bar.txt');
// 一行代码完成文件拷贝
read.pipe(write);
read.on('end', () => {
console.log('文件拷贝完成');
});
二、Node.js 原生 HTTP Web 服务
Node.js 内置 http 模块,无需第三方框架,即可快速搭建 Web 服务器。
1. 基础服务搭建
http.createServer 创建服务,回调包含两个核心对象:
req(Request):请求对象,存储客户端请求信息res(Response):响应对象,用于给客户端返回数据
最简 Web 服务代码
js
const http = require('http');
const PORT = 8000;
// 创建服务
const server = http.createServer((req, res) => {
// 给客户端返回数据,end 表示响应结束
res.end('Hello World');
});
// 监听端口和主机
// host: 0.0.0.0 允许局域网访问;127.0.0.1 仅本机访问
server.listen(PORT, '0.0.0.0', () => {
console.log(`服务器启动成功,访问地址:http://localhost:${PORT}`);
});
2. 解析 Request 请求对象
通过 req 可获取 请求地址、请求方式、请求头、请求参数。
2.1 路由分发(根据 URL 处理不同接口)
js
const http = require('http');
const PORT = 8000;
const server = http.createServer((req, res) => {
const url = req.url;
// 根据路径路由
if (url === '/login') {
res.end('欢迎登录');
} else if (url === '/products') {
res.end('商品列表数据');
} else {
res.end('404 页面不存在');
}
});
server.listen(PORT, () => {
console.log(`服务运行在 ${PORT} 端口`);
});
2.2 解析 URL 拼接参数(query 参数)
使用内置 url + querystring 模块解析 ?name=xxx&age=xxx 格式参数:
js
const http = require('http');
const url = require('url');
const qs = require('querystring');
const PORT = 8000;
const server = http.createServer((req, res) => {
// 解析完整 URL
const parseUrl = url.parse(req.url);
const { pathname, query } = parseUrl;
// 解析 query 字符串为对象
const queryObj = qs.parse(query);
console.log('请求参数:', queryObj);
if (pathname === '/login') {
res.end(`用户名:${queryObj.name},密码:${queryObj.password}`);
} else {
res.end('404');
}
});
server.listen(PORT, () => {
console.log(`服务启动:http://localhost:${PORT}`);
});
访问测试:http://localhost:8000/login?name=test&password=123456
2.3 获取 POST 请求 Body 数据
POST 数据放在请求体中,需监听 data、end 事件分段接收:
js
const http = require('http');
const PORT = 8000;
const server = http.createServer((req, res) => {
// 只处理 /users 接口 + POST 请求
if (req.url === '/users' && req.method === 'POST') {
req.setEncoding('utf-8'); // 设置编码为字符串
let body = '';
// 分段接收数据
req.on('data', (chunk) => {
body += chunk;
});
// 数据接收完成
req.on('end', () => {
// 解析 JSON 格式请求体
const user = JSON.parse(body);
console.log('接收POST数据:', user);
res.end('创建用户成功');
});
} else {
res.end('请求地址错误');
}
});
server.listen(PORT, () => {
console.log(`服务启动`);
});
3. 配置 Response 响应对象
3.1 响应状态码 + 响应头
常用状态码:200(成功)、201(创建成功)、404(未找到)、500(服务异常)
设置响应头:setHeader(单个)、writeHead(批量+状态码)
js
const http = require('http');
const PORT = 8000;
const server = http.createServer((req, res) => {
// 方式1:单独设置状态码 + 单个响应头
// res.statusCode = 200;
// res.setHeader('Content-Type', 'application/json; charset=utf-8');
// 方式2:一次性设置状态码 + 所有响应头(推荐)
res.writeHead(200, {
'Content-Type': 'application/json; charset=utf-8'
});
// 分段返回数据
res.write(JSON.stringify({ code: 200, msg: '请求成功' }));
// 结束响应(必须调用,否则客户端一直等待)
res.end();
});
server.listen(PORT, () => {
console.log(`服务运行在 ${PORT}`);
});
三、Node.js 实现文件上传(原生写法)
文件上传请求头 Content-Type 为 multipart/form-data,数据包含分隔符 boundary、文件类型、二进制文件流,需要手动解析截取二进制数据。
js
const http = require('http');
const fs = require('fs');
const PORT = 8000;
const server = http.createServer((req, res) => {
if (req.url === '/upload' && req.method === 'POST') {
// 1. 重点:不设置任何编码,保留原始 Buffer 二进制数据
const contentType = req.headers['content-type'];
const totalSize = Number(req.headers['content-length']);
let currentSize = 0;
// 使用 Buffer 数组存储二进制块,代替字符串
const chunks = [];
// 分段接收二进制流
req.on('data', (chunk) => {
currentSize += chunk.length;
const progress = (currentSize / totalSize * 100).toFixed(2);
res.write(`上传进度:${progress}%\r\n`);
chunks.push(chunk);
});
req.on('end', () => {
// 合并所有二进制块为完整 Buffer
const buffer = Buffer.concat(chunks);
const body = buffer.toString('latin1'); // 仅用来解析文本格式头,不处理图片
try {
// 提取 boundary 分隔符
const boundary = contentType.split('; ').find(item => item.startsWith('boundary=')).split('=')[1];
const boundaryStart = `--${boundary}`;
const boundaryEnd = `--${boundary}--`;
// 截取两个分隔符中间的内容(文件本体)
const startIdx = body.indexOf(boundaryStart) + boundaryStart.length;
const endIdx = body.indexOf(boundaryEnd, startIdx);
const filePart = body.slice(startIdx, endIdx);
// 分割报文头部 和 真正的文件二进制(multipart 头部和文件之间有 \r\n\r\n)
const fileContent = filePart.split('\r\n\r\n')[1];
// 转回二进制 Buffer(latin1 无损互转二进制)
const fileBuffer = Buffer.from(fileContent, 'latin1');
// 写入图片文件
fs.writeFile('./upload.png', fileBuffer, (err) => {
if (err) {
res.writeHead(500);
res.end('文件写入失败');
console.error(err);
return;
}
res.end('文件上传完成!可正常打开图片');
});
} catch (err) {
res.writeHead(400);
res.end('解析上传数据失败');
console.error(err);
}
});
} else {
res.writeHead(404, { 'Content-Type': 'text/plain;charset=utf-8' });
res.end('接口不存在');
}
});
server.listen(PORT, () => {
console.log(`文件上传服务启动:http://localhost:${PORT}`);
});
测试上传
可使用 Postman/ApiFox 发起请求:
- 请求地址:
http://localhost:8000/upload - 请求方式:
POST - 请求体:选择
form-data,添加文件字段并上传图片 - 服务根目录会生成
upload.png,即为上传后的文件。
四、总结
- Stream 流 :适合大文件分段读写,核心
Readable、Writable,pipe快速拷贝文件,避免内存溢出。 - HTTP 服务 :
http.createServer搭建服务,req解析请求(URL、参数、Body),res控制响应(状态码、响应头、返回数据)。 - 文件上传 :原生需手动解析
multipart/form-data格式,截取二进制流写入本地;实际项目可使用multer等第三方库简化开发。