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 相关文章推荐
网页里控制图片大小的相关代码
Jun 13 Javascript
JavaScript获取GridView选择的行内容
Apr 14 Javascript
jquery ajax传递中文参数乱码问题及解决方法说明
Feb 07 Javascript
javascript中的self和this用法小结
Feb 08 Javascript
多个checkbox被选中时如何判断是否有自己想要的
Sep 22 Javascript
3种Jquery限制文本框只能输入数字字母的方法
Dec 03 Javascript
一个简单的JavaScript Map实例(分享)
Aug 03 Javascript
jQuery实现微信长按识别二维码功能
Aug 26 Javascript
Vue.js创建Calendar日历效果
Nov 03 Javascript
vue.js整合vux中的上拉加载下拉刷新实例教程
Jan 09 Javascript
Vue获取页面元素的相对位置的方法示例
Feb 05 Javascript
Typescript3.9 常用新特性一览(推荐)
May 14 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+XML 制作简单的留言本 图文教程
2009/11/02 PHP
如何使用PHP批量去除文件UTF8 BOM信息
2013/08/05 PHP
php实现水仙花数的4个示例分享
2014/04/08 PHP
理解PHP中的stdClass类
2014/04/18 PHP
php制作动态随机验证码
2015/02/12 PHP
在Mac OS上自行编译安装Apache服务器和PHP解释器
2015/12/24 PHP
Zend Framework动作助手Url用法详解
2016/03/05 PHP
php中strlen和mb_strlen用法实例分析
2016/11/12 PHP
thinkPHP框架实现多表查询的方法
2018/06/14 PHP
动态加载js和css(外部文件)
2013/04/17 Javascript
javascript解析xml实现省市县三级联动的方法
2015/07/25 Javascript
基于insertBefore制作简单的循环插空效果
2015/09/21 Javascript
JavaScript中eval函数的问题
2016/01/31 Javascript
简单实现js倒计时功能
2017/02/13 Javascript
纯js实现页面返回顶部的动画(超简单)
2017/08/10 Javascript
vue.config.js常用配置详解
2019/11/14 Javascript
vue实现验证用户名是否可用
2021/01/20 Vue.js
python列表去重的二种方法
2014/02/14 Python
python自动登录12306并自动点击验证码完成登录的实现源代码
2018/04/25 Python
python smtplib发送带附件邮件小程序
2018/05/22 Python
python求质数的3种方法
2018/09/28 Python
Python爬虫将爬取的图片写入world文档的方法
2018/11/07 Python
Python列表与元组的异同详解
2019/07/02 Python
python实现异常信息堆栈输出到日志文件
2019/12/26 Python
python中strip(),lstrip(),rstrip()函数的使用讲解
2020/11/17 Python
html5 移动端视频video的android兼容(去除播放控件、全屏)
2020/03/26 HTML / CSS
OLEDBConnection和SQLConnection有什么区别
2013/05/31 面试题
师范大学毕业自我鉴定
2013/11/21 职场文书
新闻发布会主持词
2014/03/28 职场文书
银行先进个人事迹材料
2014/05/11 职场文书
考试作弊检讨书1000字(5篇)
2014/10/19 职场文书
2015年医务人员医德医风自我评价
2015/03/03 职场文书
2016中秋节月饼促销广告语
2016/01/28 职场文书
《风筝》教学反思
2016/02/23 职场文书
pytorch中F.avg_pool1d()和F.avg_pool2d()的使用操作
2021/05/22 Python
vue使用echarts实现折线图
2022/03/21 Vue.js