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的ajax jsonp的使用解惑
Oct 09 Javascript
javascript实现俄罗斯方块游戏的思路和方法
Apr 27 Javascript
jQuery事件绑定方法学习总结(推荐)
Nov 21 Javascript
获取jqGrid中选择的行的数据
Nov 30 Javascript
原生和jQuery的ajax用法详解
Jan 23 Javascript
jQuery插件HighCharts绘制简单2D柱状图效果示例【附demo源码】
Mar 21 jQuery
详解ElementUI之表单验证、数据绑定、路由跳转
Jun 21 Javascript
vue+iview写个弹框的示例代码
Dec 05 Javascript
详解Angular6.0使用路由步骤(共7步)
Jun 29 Javascript
JS实现轮播图效果
Jan 11 Javascript
javascript设计模式 ? 迭代器模式原理与用法实例分析
Apr 17 Javascript
浅谈react useEffect闭包的坑
Jun 08 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记录用户通过搜索引擎进网站的关键词
2014/02/13 PHP
PhpStorm terminal无法输入命令的解决方法
2016/10/09 PHP
Laravel框架实现修改登录和注册接口数据返回格式的方法
2018/08/17 PHP
AlertBox 弹出层信息提示框效果实现步骤
2010/10/11 Javascript
兼容IE和FF的js脚本代码小结(比较常用)
2010/12/06 Javascript
JQuery判断子iframe何时加载完成解决方案
2013/08/20 Javascript
javascript设计模式之解释器模式详解
2014/06/05 Javascript
jQuery侧边栏实现代码
2016/05/06 Javascript
NodeJS与HTML5相结合实现拖拽多个文件上传到服务器的实现方法
2016/07/26 NodeJs
vue.js中$watch的用法示例
2016/10/04 Javascript
JQueryEasyUI框架下的combobox的取值和绑定的方法
2017/01/22 Javascript
基于vue2.x的电商图片放大镜插件的使用
2018/01/22 Javascript
Vue 引入AMap高德地图的实现代码
2019/04/29 Javascript
快速解决layui弹窗按enter键不停弹窗的问题
2019/09/18 Javascript
Vue生命周期activated之返回上一页不重新请求数据操作
2020/07/26 Javascript
原生js实现滑块区间组件
2021/01/20 Javascript
原生js 实现表单验证功能
2021/02/08 Javascript
Python+matplotlib实现计算两个信号的交叉谱密度实例
2018/01/08 Python
用python生成与调用cntk模型代码演示方法
2019/08/26 Python
Python守护进程实现过程详解
2020/02/10 Python
Python3创建Django项目的几种方法(3种)
2020/06/03 Python
python如何变换环境
2020/07/21 Python
html5贪吃蛇游戏使用63行代码完美实现
2013/06/25 HTML / CSS
酒店管理专业毕业生推荐信
2013/11/10 职场文书
公司年会晚宴演讲稿
2014/01/06 职场文书
运动会解说词100字
2014/01/31 职场文书
领班岗位职责范文
2014/02/06 职场文书
园林设计专业毕业生求职信
2014/03/23 职场文书
煤矿安全演讲稿
2014/05/09 职场文书
物流专业自荐信
2014/05/23 职场文书
环境监测与治理技术专业求职信
2014/07/06 职场文书
党的群众路线教育实践活动个人对照检查材料(校长)
2014/11/05 职场文书
英文感谢信格式
2015/01/21 职场文书
2015年教师党员公开承诺书
2015/01/22 职场文书
浅谈如何提高PHP代码质量之端到端集成测试
2021/05/28 PHP