以下是根据解析库 form-data.js
的结果。
这里不说使用,而是说 multipart/form-data
的传输的格式。
1. 简单使用 form-data 上传文件
假如我使用的是如下的操作:
var form = new FormData();
form.append('fileName', '不知道');
form.append('my_file', fs.createReadStream('./test.txt'));
那么在传输中的格式如下:
----------------------------887941523663108122966319
Content-Disposition: form-data; name="fileName"
test.txt
----------------------------887941523663108122966319
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
这是测试文件传输。
----------------------------887941523663108122966319--
2. 聊聊 form-data 格式
其中每个属性,也就是定义的 name="fileName"
两边都有 -- 。也就是上面可以换成弄成下面这样:
-- 这是开头的 --
--------------------------887941523663108122966319
Content-Disposition: form-data; name="fileName"
test.txt
-- 这是结尾的 --
每个属性都包含分隔符,也就是 --------------------------887941523663108122966319
。当然也可以换成其他的,比如:test
,这样也是可以的,由于这个是可以自定义的,那对于接收端来说,怎样知道分隔符是什么样子的嘛,首先我们得知道分隔符的名称叫 boundary
。而在发送端在指定 Content-Type
的时候我们需要同时指定 boundary
。也就是填写 Content-Type
的时候像下面这样:
multipart/form-data; boundary=--------------------------887941523663108122966319
除了上面的,中间的包含头部和内容,其中头部就是:
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
其中 Content-Disposition
第一个固定值为 form-data
; name
代表属性名, filename
代表文件名称。 Content-Type
代表这个文件的类型。像我给出的这个是文件,下面是文件内容:
这是测试文件传输。
现在我们写出 form-data
格式的模板了:
--${boundary}
Content-Disposition: form-data;name="${attrName}"; ${other}
${contentType}
${content}
--
...
--${boundary}--
在最后还有 ${boundary}--
。 对应的值说明如下:
${boundary}
代表 boundary 的取值。${attrName}
代表定义的属性名。${other}
如果是文件的话,这里还会有文件名 filename 等属性。${contentType}
代表这个属性对应值的 Content-Type 。${content}
代表内容,如果是普通值,那么代表属性名称;如果是文件则是文件中的内容。
3. 大文件说明
一般文件会特别的大,比如图片,里面有大量的二进制数据,不可能一次性就全部放入内容中,因为文件太大的话,内存不一定够用,这里的内存并不是硬盘的内存,而是处理器的内存,所以大的文件我们不可能先全部加载,然后再一起装入目标中,说白了我们计算机不一定有那么大的桶来装给定的水。
这个时候就需要使用到流 Stream
。 那么流是什么意思呢,就相当于水管,或者是一个小桶,先将大桶里面的水倒入小桶里面,小桶装满后再将小桶的水倒入目标中。也可以理解在目标桶和源桶之间用一根管子相连,这样水就可以源源不断的流入目标中。
4. form-data 源码分析
通过 form-data.js
源码得知,其继承关系如下:
FormData -> CombinedStream -> Stream
发现最终继承至 Stream
。 所以当我们进行 form.pipe(req)
时,也就是将我们定义好的数据流入到 req 中,其中 req 的继承关系如下:
http.ClientRequest -> Stream
所以这行代码也就是从 form -> req 中 。所以我们的数据即便很大,也是会慢慢流入到 req 中,并不是一次性全部装入 req 中。
4.1 append 函数分析
FormData.prototype.append = function(field, value, options) {
// 如果没有传入 options,默认为空对象
options = options || {};
// options 可以传入字符串,如果传入字符串就当做属性 filename 的值
if (typeof options == 'string') {
options = {filename: options};
}
// 使用 CombinedStream.prototype.append 方法
var append = CombinedStream.prototype.append.bind(this);
// 如果传入的 value 是 number ,那么将其转换成字符串
if (typeof value == 'number') {
value = '' + value;
}
// 不支持数组
if (util.isArray(value)) {
this._error(new Error('Arrays are not supported.'));
return;
}
/*
处理每一个属性的头部的,包含文件的类型,路径啥的信息,像:
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
*/
var header = this._multiPartHeader(field, value, options);
var footer = this._multiPartFooter();
// 每一个属性都是由三部分组成
// header value footer
append(header);
append(value);
append(footer);
this._trackLength(header, value, options);
};
我们发现每一个属性都由三部分组成: header , value 和 footer 。
4.2 _multiPartHeader 函数
header :
/*
处理每个属性的头部,也就是像这样的:
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
*/
FormData.prototype._multiPartHeader = function(field, value, options) {
if (typeof options.header == 'string') {
return options.header;
}
// 得到每一个属性的 Content-Disposition 值
var contentDisposition = this._getContentDisposition(value, options);
/*
得到每一个属性的 Content-Type 值,如果自己要自定义,那么需要在 options 中定义
contentType 的属性。
1. 对于普通文本,也就是 value 是普通字符串,如:测试,那么得到的 contentType 就是 options 中的
contentType 属性,如果 contentType 也没有设置,那么返回 [];
2. 对于文件来说,由于传入的是 fs.createReadStream(path) ,所以通过阅读下面的几个属性:
- name
- path
- readable
- filepath
- filename
使用 fs.createReadStream 就可以达到想要的目的。
*/
var contentType = this._getContentType(value, options);
var contents = '';
var headers = {
'Content-Disposition': ['form-data', 'name="' + field + '"'].concat(contentDisposition || []),
'Content-Type': [].concat(contentType || [])
};
// allow custom headers. 允许自定义头部
// 也就是如果 options 传入了 header ,那么合并两个头部
if (typeof options.header == 'object') {
// 合并原则是以 options.header 为遍历对象,
// 如果 headers 中有的属性将就这个
// 其余的采用 options.header
populate(headers, options.header);
}
var header;
for (var prop in headers) {
if (!headers.hasOwnProperty(prop)) continue;
header = headers[prop];
if (header == null) {
continue;
}
if (!Array.isArray(header)) {
header = [header];
}
if (header.length) {
contents += prop + ': ' + header.join('; ') + FormData.LINE_BREAK;
}
}
return '--' + this.getBoundary() + FormData.LINE_BREAK + contents + FormData.LINE_BREAK;
};
我们发现这里的头部包含分隔符在内。
value :也就是传入的值。
footer :在这里就是 \n\r
。
5. 传输
我们知道只要涉及到传输不管数据原本是什么样子,到达物理层都会变成二进制,所以只要是没有经过处理的数据,接收到的时候都是二进制。而二进制数据到底是什么内容,就要看拿到数据的那一端怎么处理数据了,假如把图片的数据处理成文本,那么拿到的文本也都是乱码,至少是我们不能识别的内容。
那接收端怎样才能拿到正确的数据呢,这就需要双方的协议了,发送端发送内容的同时会告诉接收端,自己发送的内容是什么格式的,所以发送端都要指定 Content-Type
,来告诉接收端发送的是啥。
所以我们上传图片什么的,其实可以直接上传图片,假如接收端不额外发送端的其他信息的话;接收端接收到信息以后就信息放在一个文件中,然后使用图片解析工具去解析,看到的就是发送端发送的图片了。