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提交的内容无法换行的解决办法
Apr 09 Javascript
jquery实现页面图片等比例放大缩小功能
Feb 12 Javascript
采用call方式实现js继承
May 20 Javascript
基于jQuery仿淘宝产品图片放大镜特效
Oct 19 Javascript
JavaScript类的写法
Sep 17 Javascript
浅谈jquery页面初始化的4种方式
Nov 27 Javascript
实例讲解javascript实现异步图片上传方法
Dec 05 Javascript
jQuery实现带右侧索引功能的通讯录示例【附源码下载】
Apr 17 jQuery
Vue.js中关于侦听器(watch)的高级用法示例
May 02 Javascript
MVVM 双向绑定的实现代码
Jun 21 Javascript
React组件内事件传参实现tab切换的示例代码
Jul 04 Javascript
angular2组件中定时刷新并清除定时器的实例讲解
Aug 31 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引用地址改变变量值的问题
2012/03/23 PHP
php中JSON的使用与转换
2015/01/14 PHP
在Mac上编译安装PHP7的开发环境
2015/07/28 PHP
Zend Framework教程之Zend_Registry对象用法分析
2016/03/22 PHP
php使用strip_tags()去除html标签仍有空白的解决方法
2016/07/28 PHP
ajax更新数据后,jquery、jq失效问题
2011/03/16 Javascript
用jquery的方法制作一个简单的导航栏
2014/06/23 Javascript
深入理解JavaScript系列(35):设计模式之迭代器模式详解
2015/03/03 Javascript
window.open()实现post传递参数
2015/03/12 Javascript
在JavaScript中使用开平方根的sqrt()方法
2015/06/15 Javascript
跟我学习javascript的全局变量
2015/11/16 Javascript
JavaScript中的冒泡排序法
2016/08/03 Javascript
JavaScript实现DOM对象选择器
2016/09/24 Javascript
JavaScript中return用法示例
2016/11/29 Javascript
jquery 判断div show的状态实例
2016/12/03 Javascript
yarn的使用与升级Node.js的方法详解
2017/06/04 Javascript
原生js实现html手机端城市列表索引选择城市
2020/06/24 Javascript
js异步接口并发数量控制的方法示例
2020/11/22 Javascript
Python基础之函数用法实例详解
2014/09/10 Python
Python简单实现enum功能的方法
2016/04/25 Python
python使用循环打印所有三位数水仙花数的实例
2018/11/13 Python
Python功能点实现:函数级/代码块级计时器
2019/01/02 Python
python-opencv 将连续图片写成视频格式的方法
2019/01/08 Python
python3的数据类型及数据类型转换实例详解
2019/08/20 Python
Python+OpenCV实现实时眼动追踪的示例代码
2019/11/11 Python
wxPython修改文本框颜色过程解析
2020/02/14 Python
总经理助理的八要求
2013/11/12 职场文书
《分一分》教学反思
2014/04/13 职场文书
房地产营销活动策划方案
2014/09/15 职场文书
小学三八妇女节活动总结
2015/02/06 职场文书
部队个人年终总结
2015/03/02 职场文书
2015年药店店长工作总结
2015/04/29 职场文书
CSS3实现的3D隧道效果
2021/04/27 HTML / CSS
浅析Redis Sentinel 与 Redis Cluster
2021/06/24 Redis
深入讲解数据库中Decimal类型的使用以及实现方法
2022/02/15 MySQL
Golang gRPC HTTP协议转换示例
2022/06/16 Golang