近期的项目里使用了这样一个项目架构: 前端 -> nodejs -> javajavascript
在 nodejs 的接口转发中拦截一部分接口,再对请求的方法进行区分,请求后台数据后,再进行返回。现有的接口中基本只用到了 get 和 post 两种,可是在文件上传的时候遇到了问题。css
node 层使用 eggjs ,通常的 post 的请求直接在 ctx.body 就能拿到请求的参数,可是 /upload 的接口就不行,拿到的 body 是 {} ,下面咱们来逐步分析。html
一个 Blob ( Binary Large Object ) 对象表示一个不可变的, 原始数据的相似文件对象。Blob表示的数据不必定是一个JavaScript原生格式。 File 接口基于Blob,继承 Blob 功能并将其扩展为支持用户系统上的文件。前端
前端上传文件的方式无非就是使用:一、表单自动上传;二、使用 ajax 上传。咱们能够使用如下代码建立一个 Form,并打印出 filejava
<form method="POST" id="uploadForm" enctype="multipart/form-data">
<input type="file" id="file" name="file" />
</form>
<button id="submit">submit</button>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script> $("#submit").click(function() { console.log($("#file")[0].files[0]) }); </script>
复制代码
简单地说 Blob 能够理解为 Web 中的二进制文件。 而 File 是基于 Blob 实现的一个类,新增了关于文件有关的一些信息。node
FormData 对象的做用就相似于 Jq 的 serialize() 方法,不过 FormData 是浏览器原生的,且支持二进制文件。jquery
ajax 经过 FormData 这个对象发送表单请求,不管是原生的 XMLHttpRequest 、jq 的 ajax 方法、 axios 都是在 data 里直接指定上传 formData 类型的数据,fetch api 是在 body 里上传。ios
FormData 添加数据有有两种,以下 formData 和 formData2 的区别,而 formData2 能够经过传入一个 element 的方式进行初始化,初始化以后依然能够调用 formData 的 append 方法。git
<!DOCTYPE html>
<html>
<form method="POST" id="uploadForm" name="uploadFormName" enctype="multipart/form-data">
<input type="file" id="fileImag" name="configFile" />
</form>
<div id="show"></div>
<button id="submit">submit</button>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</html>
<script> $("#submit").click(function() { const file = $("#fileImag")[0].files[0]; const formData = new FormData(); formData.append("fileImag", file); console.log(formData.getAll("fileImag")); const formData2 = new FormData(document.querySelector("#uploadForm")); // const formData2 = new FormData(document.forms.namedItem("uploadFormName");); console.log(formData2.get("configFile")); }); </script>
复制代码
console.log()
没法直接打印出 formData 的数据,能够使用 get(key)
或者 getAll(key)
github
new FormData(element)
的建立方式,上面 key
为 <input />
上的 name 字段。Buffer 和 Stream 是 node 为了让 js 在后端拥有处理二进制文件而出现的数据结构。
经过名字能够看出 buffer 是缓存的意思。存储在内存当中,因此大小有限,buffer 是 C++ 层面分配的,所得内存不在 V8 内。
stream 能够用水流形容数据的流动,在文件 I/O、网络 I/O中数据的传输均可以称之为流。流的使用方式只有一次,释放使用以后将不能被调用。
经过两个 fs 的 api 看出,readFile 不指定字符编码默认返回 buffer 类型,而 createReadStream 将文件转化为一个 stream , nodejs 中的 stream 经过 data 事件可以一点一点地拿到文件内容,直到 end 事件响应为止。
const fs = require("fs");
fs.readFile("./package.json", function(err, buffer) {
if (err) throw err;
console.log("buffer", buffer);
});
function readLines(input, func) {
var remaining = "";
input.on("data", function(data) {
remaining += data;
var index = remaining.indexOf("\n");
var last = 0;
while (index > -1) {
var line = remaining.substring(last, index);
last = index + 1;
func(line);
index = remaining.indexOf("\n", last);
}
remaining = remaining.substring(last);
});
input.on("end", function() {
if (remaining.length > 0) {
func(remaining);
}
});
}
function func(data) {
console.log("Line: " + data);
}
var input = fs.createReadStream("./package.json");
input.setEncoding("binary");
readLines(input, func);
复制代码
fs.readFile() 函数会缓冲整个文件。 为了最小化内存成本,尽量经过 fs.createReadStream() 进行流式传输。
在 http 的请求头中 Content-type 是 multipart/form-data 时,请求的内容以下:
POST / HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryoMwe4OxVN0Iuf1S4
Origin: http://localhost:3000
Referer: http://localhost:3000/upload
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36
------WebKitFormBoundaryoqBx9oYBhx4SF1YQ
Content-Disposition: form-data; name="upload"
http://localhost:3000
------WebKitFormBoundaryoMwe4OxVN0Iuf1S4
Content-Disposition: form-data; name="upload"; filename="IMG_9429.JPG"
Content-Type: image/jpeg
����JFIF��C // 文件的二进制数据
……
--------WebKitFormBoundaryoMwe4OxVN0Iuf1S4--
复制代码
根据 WebKitFormBoundaryoMwe4OxVN0Iuf1S4 能够分割出文件的二进制内容
使用原生的 node 写一个文件上传的 demo
const http = require("http");
const fs = require("fs");
const util = require("util");
const querystring = require("querystring");
//用http模块建立一个http服务端
http
.createServer(function(req, res) {
if (req.url == "/upload" && req.method.toLowerCase() === "get") {
//显示一个用于文件上传的form
res.writeHead(200, { "content-type": "text/html" });
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">' +
'<input type="file" name="upload" multiple="multiple" />' +
'<input type="submit" value="Upload" />' +
"</form>"
);
} else if (req.url == "/upload" && req.method.toLowerCase() === "post") {
if (req.headers["content-type"].indexOf("multipart/form-data") !== -1)
parseFile(req, res);
} else {
res.end("pelease upload img");
}
})
.listen(3000);
function parseFile(req, res) {
req.setEncoding("binary");
let body = ""; // 文件数据
let fileName = ""; // 文件名
// 边界字符串 ----WebKitFormBoundaryoMwe4OxVN0Iuf1S4
const boundary = req.headers["content-type"]
.split("; ")[1]
.replace("boundary=", "");
req.on("data", function(chunk) {
body += chunk;
});
req.on("end", function() {
const file = querystring.parse(body, "\r\n", ":");
// 只处理图片文件;
if (file["Content-Type"].indexOf("image") !== -1) {
//获取文件名
var fileInfo = file["Content-Disposition"].split("; ");
for (value in fileInfo) {
if (fileInfo[value].indexOf("filename=") != -1) {
fileName = fileInfo[value].substring(10, fileInfo[value].length - 1);
if (fileName.indexOf("\\") != -1) {
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
}
console.log("文件名: " + fileName);
}
}
// 获取图片类型(如:image/gif 或 image/png))
const entireData = body.toString();
const contentTypeRegex = /Content-Type: image\/.*/;
contentType = file["Content-Type"].substring(1);
//获取文件二进制数据开始位置,即contentType的结尾
const upperBoundary = entireData.indexOf(contentType) + contentType.length;
const shorterData = entireData.substring(upperBoundary);
// 替换开始位置的空格
const binaryDataAlmost = shorterData
.replace(/^\s\s*/, "")
.replace(/\s\s*$/, "");
// 去除数据末尾的额外数据,即: "--"+ boundary + "--"
const binaryData = binaryDataAlmost.substring(
0,
binaryDataAlmost.indexOf("--" + boundary + "--")
);
// console.log("binaryData", binaryData);
const bufferData = new Buffer.from(binaryData, "binary");
console.log("bufferData", bufferData);
// fs.writeFile(fileName, binaryData, "binary", function(err) {
// res.end("sucess");
// });
fs.writeFile(fileName, bufferData, function(err) {
res.end("sucess");
});
} else {
res.end("reupload");
}
});
}
复制代码
经过 req.setEncoding("binary"); 拿到图片的二进制数据。能够经过如下两种方式处理二进制数据,写入文件。
fs.writeFile(fileName, binaryData, "binary", function(err) {
res.end("sucess");
});
复制代码
fs.writeFile(fileName, bufferData, function(err) {
res.end("sucess");
});
复制代码
在 koa 中使用 koa-body 能够经过 ctx.request.files 拿到上传的 file 对象。下面是例子。
'use strict';
const Koa = require('koa');
const app = new Koa();
const router = require('koa-router')();
const koaBody = require('../index')({multipart:true});
router.post('/users', koaBody,
(ctx) => {
console.log(ctx.request.body);
// => POST body
ctx.body = JSON.stringify(ctx.request.body, null, 2);
}
);
router.get('/', (ctx) => {
ctx.set('Content-Type', 'text/html');
ctx.body = ` <!doctype html> <html> <body> <form action="/" enctype="multipart/form-data" method="post"> <input type="text" name="username" placeholder="username"><br> <input type="text" name="title" placeholder="tile of film"><br> <input type="file" name="uploads" multiple="multiple"><br> <button type="submit">Upload</button> </body> </html>`;
});
router.post('/', koaBody,
(ctx) => {
console.log('fields: ', ctx.request.body);
// => {username: ""} - if empty
console.log('files: ', ctx.request.files);
/* => {uploads: [ { "size": 748831, "path": "/tmp/f7777b4269bf6e64518f96248537c0ab.png", "name": "some-image.png", "type": "image/png", "mtime": "2014-06-17T11:08:52.816Z" }, { "size": 379749, "path": "/tmp/83b8cf0524529482d2f8b5d0852f49bf.jpeg", "name": "nodejs_rulz.jpeg", "type": "image/jpeg", "mtime": "2014-06-17T11:08:52.830Z" } ]} */
ctx.body = JSON.stringify(ctx.request.body, null, 2);
}
)
app.use(router.routes());
const port = process.env.PORT || 3333;
app.listen(port);
console.log('Koa server with `koa-body` parser start listening to port %s', port);
console.log('curl -i http://localhost:%s/users -d "user=admin"', port);
console.log('curl -i http://localhost:%s/ -F "source=@/path/to/file.png"', port);
复制代码
咱们来看一下 koa-body 的实现
const forms = require('formidable');
function requestbody(opts) {
opts = opts || {};
...
opts.multipart = 'multipart' in opts ? opts.multipart : false;
opts.formidable = 'formidable' in opts ? opts.formidable : {};
...
// @todo: next major version, opts.strict support should be removed
if (opts.strict && opts.parsedMethods) {
throw new Error('Cannot use strict and parsedMethods options at the same time.')
}
if ('strict' in opts) {
console.warn('DEPRECATED: opts.strict has been deprecated in favor of opts.parsedMethods.')
if (opts.strict) {
opts.parsedMethods = ['POST', 'PUT', 'PATCH']
} else {
opts.parsedMethods = ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE']
}
}
opts.parsedMethods = 'parsedMethods' in opts ? opts.parsedMethods : ['POST', 'PUT', 'PATCH']
opts.parsedMethods = opts.parsedMethods.map(function (method) { return method.toUpperCase() })
return function (ctx, next) {
var bodyPromise;
// only parse the body on specifically chosen methods
if (opts.parsedMethods.includes(ctx.method.toUpperCase())) {
try {
if (opts.json && ctx.is(jsonTypes)) {
bodyPromise = buddy.json(ctx, {
encoding: opts.encoding,
limit: opts.jsonLimit,
strict: opts.jsonStrict,
returnRawBody: opts.includeUnparsed
});
} else if (opts.multipart && ctx.is('multipart')) {
bodyPromise = formy(ctx, opts.formidable);
}
} catch (parsingError) {
if (typeof opts.onError === 'function') {
opts.onError(parsingError, ctx);
} else {
throw parsingError;
}
}
}
bodyPromise = bodyPromise || Promise.resolve({});
/** * Check if multipart handling is enabled and that this is a multipart request * * @param {Object} ctx * @param {Object} opts * @return {Boolean} true if request is multipart and being treated as so * @api private */
function isMultiPart(ctx, opts) {
return opts.multipart && ctx.is('multipart');
}
/** * Donable formidable * * @param {Stream} ctx * @param {Object} opts * @return {Promise} * @api private */
function formy(ctx, opts) {
return new Promise(function (resolve, reject) {
var fields = {};
var files = {};
var form = new forms.IncomingForm(opts);
form.on('end', function () {
return resolve({
fields: fields,
files: files
});
}).on('error', function (err) {
return reject(err);
}).on('field', function (field, value) {
if (fields[field]) {
if (Array.isArray(fields[field])) {
fields[field].push(value);
} else {
fields[field] = [fields[field], value];
}
} else {
fields[field] = value;
}
}).on('file', function (field, file) {
if (files[field]) {
if (Array.isArray(files[field])) {
files[field].push(file);
} else {
files[field] = [files[field], file];
}
} else {
files[field] = file;
}
});
if (opts.onFileBegin) {
form.on('fileBegin', opts.onFileBegin);
}
form.parse(ctx.req);
});
}
复制代码
代码中删除了影响有关文件上传的相关逻辑
formy
函数处理了有关上传文件时的 http 解析和保存的一系列过程,最终将 files 抛出进行统一处理。代码中依赖了 formidable
这个库,咱们其实也能够在原生 node 直接使用这个库对文件进行处理。(上面的原生 node upload 只是简单地处理了一下 img 图片)使用 eggjs 进行文件上传须要先在配置文件中开启
config.multipart = { mode: "file", fileSize: "600mb" };
复制代码
而后经过 ctx.request.files[0]
就能取到文件信息。
一千个观众眼中有一千个哈姆雷特 经过以上知识点的梳理,我相信你也有了本身的想法,下面在这里说一下我是怎么处理的。
在 egg 中我使用了 request-promise 去作接口转发,经过查看 request-promise 的相关 api 和 ctx.request.files[0] 拿到的有效信息,我作了如下处理:
if (method === "POST") {
options.body = request.body;
options.json = true;
if (url === uploadeUrl) {
delete options.body;
options.formData = {
// Like <input type="text" name="name">
name: "file",
// Like <input type="file" name="file">
file: {
value: fs.createReadStream(ctx.request.files[0].filepath),
options: {
filename: ctx.request.files[0].filename,
contentType: ctx.get("content-type")
}
}
};
}
} else {
options.qs = query;
}
复制代码