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


Posted in Javascript onAugust 18, 2019

一.背景

在文件相关的数据加工等场景下,经常面临生成的物理文件应该如何处理的问题,比如:

生成的文件放到哪里,路径存在不存在?

临时文件何时清理,如何解决命名冲突,防止覆盖?

并发场景下的读写顺序如何保证?

……

对于读写物理文件带来的这些问题,最好的解决办法就是 不写文件 。然而,一些场景下想要不写文件可不那么容易,比如文件上传

二.问题

文件上传一般通过表单提交来实现,例如:

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 信息,如文件名、文件路径等 ,而这些信息是普通 Stream 所不具备的。那么,有没有办法凭空创建一个“真正的”文件流?

三.思路

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

给普通流添上文件相关的 meta 信息

先拿到一个真正的文件流,再改掉其数据和 meta 信息

显然,前者更灵活一些,并且实现上能够做到完全不依赖文件

文件流的生产过程

沿着凭空创造的思路,探究 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

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

Javascript 相关文章推荐
在textarea文本域中显示HTML代码的方法
Mar 06 Javascript
js切换光标示例代码
Oct 10 Javascript
简单的Jquery遮罩层代码实例
Nov 14 Javascript
JQuery文本改变触发事件如聚焦事件、失焦事件
Jan 15 Javascript
JQuery radio(单选按钮)操作方法汇总
Apr 15 Javascript
js去除浏览器默认底图的方法
Jun 08 Javascript
javascript多物体运动实现方法分析
Jan 08 Javascript
angular4 共享服务在多个组件中数据通信的示例
Mar 30 Javascript
axios简单实现小程序延时loading指示
Jul 30 Javascript
vue.js中使用echarts实现数据动态刷新功能
Apr 16 Javascript
vue使用recorder.js实现录音功能
Nov 22 Javascript
JavaScript接口实现方法实例分析
May 16 Javascript
微信公众号生成新浪短网址的实现(快速生成)
Aug 18 #Javascript
js 实现 list转换成tree的方法示例(数组到树)
Aug 18 #Javascript
详解ES6 Promise的生命周期和创建
Aug 18 #Javascript
vue-cli3配置与跨域处理方法
Aug 17 #Javascript
vue中获取滚动table的可视页面宽度调整表头与列对齐(每列宽度不都相同)
Aug 17 #Javascript
vue 使用element-ui中的Notification自定义按钮并实现关闭功能及如何处理多个通知
Aug 17 #Javascript
微信小程序开发之map地图组件定位并手动修改位置偏差
Aug 17 #Javascript
You might like
DedeCms模板安装/制作概述
2007/03/11 PHP
dedecms模板标签代码官方参考
2007/03/17 PHP
php仿discuz分页效果代码
2008/10/02 PHP
Yii实现MySQL多数据库和读写分离实例分析
2014/12/03 PHP
PHP简单读取xml文件的方法示例
2017/04/20 PHP
PHP中的输出echo、print、printf、sprintf、print_r和var_dump的示例代码
2020/12/01 PHP
js中复制行和删除行的操作实例
2013/06/25 Javascript
JavaScript中圆括号()和方括号[]的特殊用法疑问解答
2013/08/06 Javascript
使用Js让Html中特殊字符不被转义
2013/11/05 Javascript
如何正确使用Nodejs 的 c++ module 链接到 OpenSSL
2014/08/03 NodeJs
Node.js Sequelize如何实现数据库的读写分离
2016/10/23 Javascript
js操作浏览器的参数方法
2017/01/21 Javascript
基于Node.js实现压缩和解压缩的方法
2018/02/13 Javascript
使用webpack/gulp构建TypeScript项目的方法示例
2019/12/18 Javascript
微信小程序自定义弹出模态框禁止底部滚动功能
2020/03/09 Javascript
vue实现购物车列表
2020/06/30 Javascript
Vuex实现购物车小功能
2020/08/17 Javascript
React实现全选功能
2020/08/25 Javascript
[05:45]Ti4观战指南(下)
2014/07/07 DOTA
python+Django+apache的配置方法详解
2016/06/01 Python
Python的条件表达式和lambda表达式实例
2019/01/31 Python
详解python爬虫系列之初识爬虫
2019/04/06 Python
python requests.get带header
2020/05/05 Python
Python类super()及私有属性原理解析
2020/06/15 Python
DRF框架API版本管理实现方法解析
2020/08/21 Python
Pycharm2020最新激活码|永久激活(附最新激活码和插件的详细教程)
2020/09/29 Python
璀璨的珍珠、密钉和个性化珠宝:Lily & Roo
2021/01/21 全球购物
服装设计专业自荐信
2014/06/17 职场文书
关于诚信的活动方案
2014/08/18 职场文书
教师个人考察材料
2014/12/16 职场文书
教师个人总结范文
2015/02/11 职场文书
2015年仓库工作总结
2015/04/09 职场文书
检讨书范文大全
2015/05/07 职场文书
硕士毕业答辩开场白
2015/05/27 职场文书
2016年第二十届“母亲节暨幸福工程救助贫困母亲活动日”活动总结
2016/04/06 职场文书
vue实现移动端div拖动效果
2022/03/03 Vue.js