关于上传图片的思考

一直以来上传图片都是通过new FormData()来建立一个form表单,然后通过server插件输出文件。
那么,上传的到底是个什么东西呢?它与base64编码后的字符串又有什么关系呢?

发个数据试试先

首先我们建立一个表单,并拦截submit,重新使用fetch发送,方便我们调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<form enctype="multipart/form-data" action="/" method="post">
<input type="file" name="file">
<input type="submit" id="submit">
</form>
<img id="server_img" src="" alt="">
<script>
document.getElementById('submit').addEventListener('click', (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('file', e.target.form[0].files[0], e.target.form[0].files[0].name);

fetch('/', {
method: 'PUT',
body: formData
})
.then(response => response.json())
.then(response => {
document.getElementById('server_img').src = response.image;
});
})
</script>

检查上次的表单发现

这似乎是一个二进制文件,那么

  1. 这个二进制文件到底是什么呢?
  2. 这与直接读取文件是否一致呢?
  3. 这和base64有什么关系呢?

从服务端对比数据

方便起见,我们使用multiparty进行form-data的读取。

首先我们根据multiparty的使用建立服务器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const srv = http
.createServer((req, res) => {
console.log(req.url, req.method);
if (req.method === 'PUT') {
let base64data = null;
const form = new multiparty.Form();
form.on('part', function (part) {
if (part.filename === null) return part.resume();
const buffers = [];
part.on('data', function (chunk) {
buffers.push(chunk);
});
part.on('end', function () {
/* code scope 1 */
});
});

form.on('close', function () {
/* code scope 2 */
});
form.parse(req);
}
})
.listen(3000, '127.0.0.1')

在stream结尾处进行判断

在上面的代码code scope 1中进行如下调试

1
2
3
const buffer = Buffer.concat(buffers);
const originBuffer = fs.readFileSync(path.resolve(__dirname, 'test.png'));
console.log('compare buffer:', originBuffer.compare(buffer)); // 0, if you select this file to upload

这时发现,从文件中直接读取的buffer数据竟然等于服务器收到的图片数据,multipart/form-data会将文件的二进制直接原样上传。

那么我们常常使用的base64的data:url是个什么东西呢?

继续在上面的代码code scope 1中追加如下调试

1
2
3
const fileType = `/${part.filename.split('.')[1]}` || ''; // 可加可不加
base64data = `data:image${fileType};base64,${buffer.toString('base64')}`;
console.log(base64data);

直接对这个buffer数据输出base64的编码格式。
并在上面的代码code scope 2中追加,让服务器直接返回这个data:url

1
2
3
res.end(JSON.stringify({
image: base64data,
}));

最后发现

看来base64的data:url只是对原文件的二进制数据进行了一次编码。

懵逼的编码

buffer到底是个啥?

当数据流过大的时候,我们会使用stream的pipe,而pipe这就是在传送buffer。

1
2
3
4
5
6
7
WriteStream.prototype._write = function(data, encoding, cb) {
if (!(data instanceof Buffer)) {
const err = new ERR_INVALID_ARG_TYPE('data', 'Buffer', data);
return this.emit('error', err);
}
...
};

那我们打个断点试试,看看buffer到底是个啥

哇哦,这么神奇,不行,得看看文档。

With TypedArray now available, the Buffer class implements the Uint8Array API in a manner that is more optimized and suitable for Node.js.

哦原来是这样。
而且Node.js还支持这些编码:

  • ‘ascii’ - 仅支持 7 位 ASCII 数据。
  • ‘utf8’ - 多字节编码的 Unicode 字符。
  • ‘utf16le’ - 2 或 4 个字节,小端序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。
  • ‘ucs2’ - ‘utf16le’ 的别名。
  • ‘base64’ - Base64 编码。
  • ‘latin1’ - 将 Buffer 编码成单字节编码的字符串。
  • ‘binary’ - ‘latin1’ 的别名。
  • ‘hex’ - 将每个字节编码成两个十六进制字符。

base64

将三字节转换成四字节(由A~Z,a~z,0~9,+,/这64个字符组成,不足三字节则补0,最后会变为=)。
具体可以参考一下阮老师的笔记:Base64笔记

结论

  1. 表单上传的是二进制的源文件数据,而在Node.js中使用了Buffer也就是Uint8Array去存储。
  2. 至于base64只是使用65个字符(包括填补符号=)进行编码的一种方式。
  3. data:url分为完整的格式为:
    data:[<mediatype>][;base64],<data>
    • mediatype 是个 MIME 类型的字符串,例如 “image/jpeg” 表示 JPEG 图像文件。如果被省略,则默认值为 text/plain;charset=US-ASCII。
    • 如果数据是文本类型,你可以直接将文本嵌入 (根据文档类型,使用合适的实体字符或转义字符)。如果是二进制数据,你可以将数据进行base64编码之后再进行嵌入。
Author: PaulHan
Link: https://www.paulhan.cn/blog/2019/03/17/关于上传图片的思考/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.