Node.js 从字符串生成文件流的实现方法

作者:墨池时间:2026-10-23 10:19:18浏览:200

本文介绍如何在 Node.js 中不依赖物理磁盘,直接通过字符串生成具备完整元数据(如文件名、路径)的文件流。通过解析 fs.createReadStream 内部实现,剔除文件读写操作,利用 string-to-file-stream 库实现内存中的文件流模拟,适用于表单提交等避免临时文件生成的场景。

背景与问题

一.背景

在处理文件相关数据加工的场景中,经常需要面对一个核心问题:生成的物理文件应当如何处理?具体包括以下几个常见痛点:

首先,生成的文件应该存储在哪里?目标路径是否已经存在?

其次,临时文件应该在何时进行清理?如何避免命名冲突并防止文件被意外覆盖?

最后,在并发读写场景下,如何保证读写的顺序正确无误?

……

针对上述读写物理文件带来的问题,最理想的解决方案是“不写文件”。然而,在某些特定场景下,完全避免写入物理文件并非易事,例如在文件上传操作中。

二.问题

文件上传通常通过表单提交来实现,例如使用 Node.js 的 form-data 库:


var FormData = require('form-data');
var fs = require('fs');

var form = new FormData();
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));
form.submit('example.org/upload', function(err, res) {
 console.log(res.statusCode);
});

(摘自 Form-Data )

如果希望避免生成物理文件,可以采用以下替代方案:


const FormData = require('form-data');

const filename = 'my-file.txt';
const content = 'balalalalala...变身';

const formData = new FormData();
// 1.先将字符串转换成Buffer
const fileContent = Buffer.from(content);
// 2.补上文件meta信息
formData.append('file', fileContent, {
 filename,
 contentType: 'text/plain',
 knownLength: fileContent.byteLength
});

也就是说,文件流除了能够提供数据内容外,还包含一些元数据(meta information),例如文件名、文件路径等,而这些信息是普通 Stream 所不具备的。那么,是否存在一种方法可以凭空创建一个“真正的”文件流?

三.思路

要创建出“真正的”文件流,至少有正反两种思路:

第一种思路是给普通流添加文件相关的元数据信息。

第二种思路是先获取一个真正的文件流,然后修改其数据和元数据信息。

显然,第一种思路更加灵活,并且在实现上可以做到完全不依赖物理文件。

文件流的生产过程

文件流的生产过程

沿着“凭空创造”的思路,在探究 fs.createReadStream API 的内部实现后,发现生产文件流的关键过程如下:


function ReadStream(path, options) {
 // 1.打开path指定的文件
 if (typeof this.fd !== 'number')
  this.open();
}

ReadStream.prototype.open = function() {
 fs.open(this.path, this.flags, this.mode, (er, fd) => {
  // 2.拿到文件描述符并持有
  this.fd = fd;
  this.emit('open', fd);
  this.emit('ready');
  // 3.开始流式读取数据
  // read来自父类Readable,主要调用内部方法_read
  // ref: https://github.com/nodejs/node/blob/v10.16.3/lib/_stream_readable.js#L390
  this.read();
 });
};

ReadStream.prototype._read = function(n) {
 // 4.从文件中读取一个chunk
 fs.read(this.fd, pool, pool.used, toRead, this.pos, (er, bytesRead) => {
  let b = null;
  if (bytesRead > 0) {
   this.bytesRead += bytesRead;
   b = thisPool.slice(start, start + bytesRead);
  }
  // 5.(通过触发data事件)吐出一个chunk,如果还有数据,process.nextTick再次this.read,直至this.push(null)触发'end'事件
  // ref: https://github.com/nodejs/node/blob/v10.16.3/lib/_stream_readable.js#L207
  this.push(b);
 });
};

P.S.其中第 5 步相对复杂,this.push(buffer) 既能触发下一个 chunk 的读取(this.read()),也能在数据读完之后(通过 this.push(null))触发 'end' 事件,具体见 node/lib/_stream_readable.js

重新实现文件流

重新实现文件流

既然已经摸清了文件流的生产过程,下一步自然是 替换掉所有文件操作,直至文件流的实现完全不依赖文件 ,例如:


// 从文件中读取一个chunk
fs.read(this.fd, pool, pool.used, toRead, this.pos, (er, bytesRead) => {
 /* ... */
});

// 换成
this._fakeReadFile(this.fd, pool, pool.used, toRead, this.pos, (bytesRead) => {
 /* ... */
});

// 从输入字符串对应的Buffer中copy出一个chunk
ReadStream.prototype._fakeReadFile = function(_, buffer, offset, length, position, cb) {
 position = position || this.input._position;
 // fake read file async
 setTimeout(() => {
  let bytesRead = 0;
  if (position < this.input.byteLength) {
   bytesRead = this.input.copy(buffer, offset, position, position + length - 1);
   this.input._position += bytesRead;
  }
  cb(bytesRead);
 }, 0);
}

即从中剔除文件操作,用基于字符串的操作去替代它们

解决方案

四.解决方案

如此这般,就有了 ayqy/string-to-file-stream ,用来凭空创建文件流:


string2fileStream('string-content') === fs.createReadStream(/* path to a text file with content 'string-content' */)`

例如:


const string2fileStream = require('string-to-file-stream');

const input = 'Oh, my great data!';
const s = string2fileStream(input);
s.on('data', (chunk) => {
 assert.equal(chunk.toString(), input);
});
生成的流同样能够具有文件 meta 信息:

const string2fileStream = require('string-to-file-stream');

const formData = new FormData();
formData.append('file', string2fileStream('my-string-data', { path: './abc.txt' }));
form.submit('example.org/upload', function(err, res) {
 console.log(res.statusCode);
});

足够以假乱真

参考资料

fs.createReadStream(path[, options])

fs/streams.js

_stream_readable.js

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章