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的实现原理的模拟代码 -4 重要的扩展函数 extend
Aug 03 Javascript
autoIMG 基于jquery的图片自适应插件代码
Mar 12 Javascript
基于jquery实现ajax无刷新评论
Aug 19 Javascript
页面向下滚动ajax获取数据的实现方法(兼容手机)
May 24 Javascript
angular-ui-sortable实现可拖拽排序列表
Dec 28 Javascript
JavaScript实现打印星型金字塔功能实例分析
Sep 27 Javascript
使用原生js封装的ajax实例(兼容jsonp)
Oct 12 Javascript
浅析Vue.js中v-bind v-model的使用和区别
Dec 04 Javascript
js嵌套的数组扁平化:将多维数组变成一维数组以及push()与concat()区别的讲解
Jan 19 Javascript
Vue.js+cube-ui(Scroll组件)实现类似头条效果的横向滚动导航条
Jun 24 Javascript
在vue中实现禁止回退上一步,路由不存历史记录
Jul 22 Javascript
解决vue中provide inject的响应式监听
Apr 19 Vue.js
微信公众号生成新浪短网址的实现(快速生成)
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中读写文件实现代码
2011/10/20 PHP
PHP中使用break跳出多重循环代码实例
2015/01/21 PHP
PHP实现移除数组中为空或为某值元素的方法
2017/01/07 PHP
PHP7匿名类的用法示例
2019/04/05 PHP
php7下的filesize函数
2019/09/30 PHP
发现的以前不知道的函数
2006/09/19 Javascript
javascript 随机展示头像实现代码
2011/12/06 Javascript
用jquery实现的一个超级简单的下拉菜单
2014/05/18 Javascript
node.js中的http.response.end方法使用说明
2014/12/14 Javascript
JS选项卡动态替换banner图片路径的方法
2015/05/11 Javascript
JavaScript实现自动生成网页元素功能(按钮、文本等)
2015/11/21 Javascript
jQuery的选择器中的通配符[id^='code']或[name^='code']及jquery选择器总结
2015/12/24 Javascript
详解js私有作用域中创建特权方法
2016/01/25 Javascript
移动端日期插件Mobiscroll.js使用详解
2016/12/19 Javascript
微信小程序 websocket 实现SpringMVC+Spring+Mybatis
2017/08/04 Javascript
javascript按顺序加载运行js方法
2017/12/01 Javascript
web前端页面生成exe可执行文件的方法
2018/02/08 Javascript
详解vue 数据传递的方法
2018/04/19 Javascript
JavaScript设计模式之工厂模式和抽象工厂模式定义与用法分析
2018/07/26 Javascript
VuePress 快速踩坑小结
2019/02/14 Javascript
Vue使用.sync 实现父子组件的双向绑定数据问题
2019/04/04 Javascript
Python实现向QQ群成员自动发邮件的方法
2014/11/19 Python
用tensorflow构建线性回归模型的示例代码
2018/03/05 Python
python如何把嵌套列表转变成普通列表
2018/03/20 Python
Python3运算符常见用法分析
2020/02/14 Python
美国高端牛仔品牌:Silver Jeans
2019/12/12 全球购物
俄罗斯在线购买飞机票、火车票、巴士票网站:Tutu.ru
2020/03/16 全球购物
廉政教育心得体会
2014/01/01 职场文书
演讲稿格式范文
2014/05/19 职场文书
有限责任公司股东合作协议书
2014/12/02 职场文书
2015年世界环境日活动方案
2015/05/05 职场文书
计划生育目标责任书
2015/05/09 职场文书
2015年国培研修感言
2015/08/01 职场文书
MySQL 如何限制一张表的记录数
2021/09/14 MySQL
python文件与路径操作神器 pathlib
2022/04/01 Python
Python实现聚类K-means算法详解
2022/07/15 Python