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 相关文章推荐
JS中简单的实现像C#中using功能(有源码下载)
Jan 09 Javascript
精解window.setTimeout()&amp;window.setInterval()使用方式与参数传递问题!
Nov 23 Javascript
window.requestAnimationFrame是什么意思,怎么用
Jan 13 Javascript
DIV+CSS+JS不间断横向滚动实现代码
Mar 19 Javascript
仿当当网淘宝网等主流电子商务网站商品分类导航菜单
Sep 25 Javascript
JS数字抽奖游戏实现方法
May 04 Javascript
JavaScript定时器和优化的取消定时器方法
Jul 03 Javascript
jQuery实现复制到粘贴板功能
Feb 11 Javascript
javascript数据结构之串的概念与用法分析
Apr 12 Javascript
小试小程序云开发(小结)
Jun 06 Javascript
深入浅析Vue中mixin和extend的区别和使用场景
Aug 01 Javascript
微信小程序如何获取地址
Dec 24 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
用PHP和ACCESS写聊天室(四)
2006/10/09 PHP
实例(Smarty+FCKeditor新闻系统)
2007/01/02 PHP
PHP简单实现上一页下一页功能示例
2016/09/14 PHP
prototype 1.5 &amp; scriptaculous 1.6.1 学习笔记
2006/09/07 Javascript
url 编码 js url传参中文乱码解决方案
2010/04/11 Javascript
Jquery之Ajax运用 学习运用篇
2011/09/26 Javascript
JavaScript 函数参数是传值(byVal)还是传址(byRef) 分享
2013/07/02 Javascript
js 编码转换 gb2312 和 utf8 互转的2种方法
2013/08/07 Javascript
JavaScript拖动层Div代码
2017/03/01 Javascript
微信小程序tabBar底部导航中文注解api详解
2017/08/16 Javascript
nodejs操作mongodb的填删改查模块的制作及引入实例
2018/01/02 NodeJs
基于vue 动态加载图片src的解决方法
2018/02/05 Javascript
vue使用echarts图表的详细方法
2018/10/22 Javascript
JS实现可切换图片的幻灯切换效果示例
2019/05/24 Javascript
使用flow来规范javascript的变量类型
2019/09/12 Javascript
layuiAdmin循环遍历展示商品图片列表的方法
2019/09/16 Javascript
Python中使用支持向量机(SVM)算法
2017/12/26 Python
python实现xlsx文件分析详解
2018/01/02 Python
python自动12306抢票软件实现代码
2018/02/24 Python
Python远程视频监控程序的实例代码
2019/05/05 Python
PyQt5固定窗口大小的方法
2019/06/18 Python
pycharm运行scrapy过程图解
2019/11/22 Python
Python打开文件、文件读写操作、with方式、文件常用函数实例分析
2020/01/07 Python
matlab、python中矩阵的互相导入导出方式
2020/06/01 Python
Python使用正则表达式实现爬虫数据抽取
2020/08/17 Python
css3实现3D色子翻转特效
2014/12/23 HTML / CSS
税务专业毕业生自荐信
2013/11/10 职场文书
社团活动策划书范文
2014/01/09 职场文书
信访工作者先进事迹
2014/01/17 职场文书
低碳环保口号
2014/06/12 职场文书
反洗钱宣传活动总结
2014/08/26 职场文书
会计试用期自我评价怎么写
2014/09/18 职场文书
群众路线剖析材料范文
2014/10/09 职场文书
长辈生日祝福语大全(72句)
2019/08/09 职场文书
浅谈golang package中init方法的多处定义及运行顺序问题
2021/05/06 Golang
JavaScript 事件捕获冒泡与捕获详情
2021/11/11 Javascript