写这篇文章的起因是我在尝试使用 Node 的原生模块去接收表单上传的数据并分割过滤表单文件与数据时,由于一开始是对可写流拼接 buffer 转的字符串进行轮询,导致只能接收文件格式为 txt 的文本文件,在接收图片或其他文件格式时却发生了写入后无法打开的情况,而 txt 文件的中文也会乱码,这是因为 buffer 在转字符串时默认为 utf8 格式,进行写入后,无法识别中文,而其他文件格式在无法判别 encoding 的情况下,在编码的转换截取中极其容易被截漏或截错。
由于在网上根本没有一篇真正告诉你怎么用 Node 原生代码去处理表单的过程,即使有的也都是错的,我是都测试过的,因为它们的原理大都跟我一样都是对字符串进行轮询,这是错误的处理方式。
所以经过对各大中间件进行追本溯源的过程后,我找到了专门用来处理表单(MIME 为 multipart/form-data)的中间件 multiparty,遂决定看懂后一定要自己过滤一遍并写出文章给其他跟我有类似问题的童鞋参考。
文章已同步至我的 github https://github.com/YOLO0927/multiparty-source-code-analysis
下面放上解析
本中间件的构造函数 Form
与直接暴漏的过滤函数 Form.prototype.parse
都十分简单,我不在此赘述,因为这个 2 个环节非常简单,稍微有点基础的同学都应该能看懂,重要的是我下面分析的 Form.prototype._write
方法,这是一个对可写流内部传入底层写入方法的一个重写,在包内你不会找到任何一个地方有调用这个方法,所以大家一定要看 Node 文档 Stream
模块中对 writable._write
方法的描述
大家一定要注意我标记的三行解释,它充分的告诉了我们,这个方法是内部方法,只要我们利用继承,将 From
继承 Stream.Writable
即可实现,下面放上我对 Form.prototype._write
的整段分析,而整个中间件的核心也就是这个方法了,其他多为此方法的辅助函数
首先童鞋们一定要注意源码 line:50 位置的util.inherits(Form, stream.Writable)
大家看到这个方法可能会找不到此包在哪里调用,其实这关键的操作在于包一旦引入便进行了 Form 继承 写入流 stream.Writable 的操作,原本可写流的内部方法 writable._write 被 Form 原型中的 _write 先传入底层,再次实例化 Form 调用 write 时将会使用的底层函数会是我们写的 Form.prototype._write
,由此我们不但使 Form 的实例化对象具有写入流,并且还重写了写入的逻辑
达到每次 transform 流都会调用我们在这里重写的 _write 方法,在 parse 方法的最后调用 req.pipe(self) 时由管道流将 req 写入 Form 实例时触发调用
Form.prototype._write = function(buffer, encoding, cb) {
if (this.error) return;
var self = this;
var i = 0;
var len = buffer.length; // 轮询表单数据 buffer 的长度
var prevIndex = self.index;
var index = self.index;
var state = self.state; // 轮询表单数据 buffer 时的状态
var lookbehind = self.lookbehind;
var boundary = self.boundary; // 通过 parse 方法里拿到的分隔符 boundary
var boundaryChars = self.boundaryChars;
var boundaryLength = self.boundary.length;
var boundaryEnd = boundaryLength - 1;
var bufferLength = buffer.length;
var c;
var cl;
// boundary = \r\n------XXXXXXXXXXXXXX
// 全过程对 buffer 进行循环,所以不存在任何文件的格式问题,如果是对 buffer.toString 后的字符串进行循环,是无法判断收集到的文件是何种格式的,这点是核心关键
// 所以我们只能对 buffer 进行循环,在循环中根据 buffer 字节码判断 : - LF RF 等符号来判断到底循环到哪个步骤,这是过滤难点
for (i = 0; i < len; i++) {
c = buffer[i];
switch (state) {
case START:
// 记住 index 永远最大只是 boundary 的长度
index = 0;
state = START_BOUNDARY;
/* falls through */
case START_BOUNDARY:
console.log(`${
i}, ${
c} === ${
boundary[index+2]}, ${
index}, ${
boundary[index]}`)
if (index === boundaryLength - 2 && c === HYPHEN) {
index = 1;
state = CLOSE_BOUNDARY;
break;
} else if (index === boundaryLength - 2) {