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 相关文章推荐
jQuery UI AutoComplete 使用说明
Jun 20 Javascript
读jQuery之九 一些瑕疵说明
Jun 21 Javascript
extjs表格文本启用选择复制功能具体实现
Oct 11 Javascript
IE下window.onresize 多次调用与死循环bug处理方法介绍
Nov 12 Javascript
字段太多jquey快速清空表单内容方法
Aug 21 Javascript
director.js实现前端路由使用实例
Feb 03 Javascript
JavaScript如何实现对数字保留两位小数一位自动补零
Dec 18 Javascript
JavaScript实现页面无操作倒计时退出
Oct 22 Javascript
使用微信小程序开发前端【快速入门】
Dec 05 Javascript
ES6中的class是如何实现的(附Babel编译的ES5代码详解)
May 17 Javascript
layui问题之渲染数据表格时,仅出现10条数据的解决方法
Sep 12 Javascript
vue:el-input输入时限制输入的类型操作
Aug 05 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
基于mysql的bbs设计(一)
2006/10/09 PHP
在普通HTTP上安全地传输密码
2007/07/21 PHP
详解PHP实现异步调用的4种方法
2016/03/14 PHP
thinkPHP框架实现生成条形码的方法示例
2018/06/06 PHP
用正则xmlHttp实现的偷(转)
2007/01/22 Javascript
IE JS编程需注意的内存释放问题
2009/06/23 Javascript
jQuery+CSS 实现的超Sexy下拉菜单
2010/01/17 Javascript
JavaScript类和继承 this属性使用说明
2010/09/03 Javascript
ajax处理php返回json数据的实例代码
2013/01/24 Javascript
详解页面滚动值scrollTop在FireFox与Chrome浏览器间的兼容问题
2015/12/03 Javascript
详解 vue.js用法和特性
2017/10/15 Javascript
Vue创建头部组件示例代码详解
2018/10/23 Javascript
优雅地使用loading(推荐)
2019/04/20 Javascript
Vue中keep-alive的两种应用方式
2020/07/15 Javascript
Python实现子类调用父类的方法
2014/11/10 Python
详解python发送各类邮件的主要方法
2016/12/22 Python
Python打印输出数组中全部元素
2018/03/13 Python
Linux系统(CentOS)下python2.7.10安装
2018/09/26 Python
详解pandas安装若干异常及解决方案总结
2019/01/10 Python
pytorch 预训练层的使用方法
2019/08/20 Python
opencv python Canny边缘提取实现过程解析
2020/02/03 Python
Python多线程多进程实例对比解析
2020/03/12 Python
Python-jenkins 获取job构建信息方式
2020/05/12 Python
浅析Python面向对象编程
2020/07/10 Python
Django和Ueditor自定义存储上传文件的文件名
2021/02/25 Python
美丽的现代设计家具:2Modern
2018/07/26 全球购物
集团公司总经理岗位职责
2013/12/20 职场文书
自我鉴定写作要点
2014/01/17 职场文书
酒店总经理助理岗位职责
2014/02/01 职场文书
《石榴》教学反思
2014/03/02 职场文书
亚运会口号
2014/06/20 职场文书
2014年企业党建工作总结
2014/12/18 职场文书
家装电话营销开场白
2015/05/29 职场文书
2016党员读书思廉心得体会
2016/01/23 职场文书
2019年最新感恩节祝福语(28句)
2019/11/27 职场文书
LayUI+Shiro实现动态菜单并记住菜单收展的示例
2021/05/06 Javascript