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 left,right,mid函数
Jun 10 Javascript
使用JS进行目录上传(相当于批量上传)
Dec 05 Javascript
java和javascript获取word文档的书签位置对比
Jun 19 Javascript
使用jquery/js获取iframe父子级、同级获取元素的方法
Aug 05 Javascript
如何实现json数据可视化详解
Nov 24 Javascript
JS判断指定dom元素是否在屏幕内的方法实例
Jan 23 Javascript
AngularJS封装$http.post()实例详解
May 06 Javascript
jQuery选择器之表单元素选择器详解
Sep 19 jQuery
vue+Element实现搜索关键字高亮功能
May 28 Javascript
JavaScript遍历数组的方法代码实例
Jan 14 Javascript
JavaScript实现拖拽功能
Feb 11 Javascript
vue-router中hash模式与history模式的区别
Jun 23 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
高分R级DC动画剧《哈莉·奎茵》第二季正式预告首发
2020/04/09 欧美动漫
php cache类代码(php数据缓存类)
2010/04/15 PHP
Zend Framework校验器Zend_Validate用法详解
2016/12/09 PHP
javascript 用原型继承来实现对象系统
2010/03/22 Javascript
IE6,IE7下js动态加载图片不显示错误
2010/07/17 Javascript
createElement与createDocumentFragment的点点区别小结
2011/12/19 Javascript
推荐17个优美新鲜的jQuery的工具提示插件
2012/09/14 Javascript
解析javascript 浏览器关闭事件
2013/07/08 Javascript
JQuery简单实现锚点链接的平滑滚动
2015/05/03 Javascript
jQuery筛选数组之grep、each、inArray、map的用法及遍历json对象
2016/06/20 Javascript
jQuery获取attr()与prop()属性值的方法及区别介绍
2016/07/06 Javascript
JavaScript用JSONP跨域请求数据实例详解
2017/01/06 Javascript
jquery.pager.js实现分页效果
2019/07/29 jQuery
vue+webpack dev本地调试全局样式引用失效的解决方案
2019/11/12 Javascript
JavaScript设计模式---单例模式详解【四种基本形式】
2020/05/16 Javascript
解决antd datepicker 获取时间默认少8个小时的问题
2020/10/29 Javascript
Python tkinter模块中类继承的三种方式分析
2017/08/08 Python
python 常用的基础函数
2018/07/10 Python
Python语言快速上手学习方法
2018/12/14 Python
关于Pycharm无法debug问题的总结
2019/01/19 Python
Python制作微信好友背景墙教程(附完整代码)
2019/07/17 Python
pytorch nn.Conv2d()中的padding以及输出大小方式
2020/01/10 Python
Python3实现个位数字和十位数字对调, 其乘积不变
2020/05/03 Python
全球地下的服装和态度:Slam Jam
2018/02/04 全球购物
英国最大的香水商店:The Fragrance Shop
2018/07/06 全球购物
英国异国风情旅游网站:Travel Talk Tours(团体旅游、探险旅游、帆船假期)
2018/07/26 全球购物
简述索引存取方法的作用和建立索引的原则
2013/03/26 面试题
八一演出活动方案
2014/02/03 职场文书
班长自荐书范文
2014/02/11 职场文书
十佳护士获奖感言
2014/02/18 职场文书
协议书模板
2014/04/23 职场文书
2014年世界艾滋病日宣传活动总结
2014/11/18 职场文书
2015年行政助理工作总结
2015/04/30 职场文书
困难补助申请报告
2015/05/19 职场文书
运动会宣传语
2015/07/13 职场文书
Redis延迟队列和分布式延迟队列的简答实现
2021/05/13 Redis