您的位置 首页 > 数码极客

【part文件】文件上传接口的转发(node)

场景

最近的项目使用了这种项目体系结构:前端-nodejs-Java。

前端负责实现业务逻辑的展示和交互。nodejs包括一些数据维护和接口传递Java。剩下的数据从nodejs的接口传递中拦截一些接口,区分请求方法,请求后台数据,然后返回。

现有的接口中基本只用到了 get 和 post 两种,但是在文件上传的时候遇到了问题。

node 层使用 eggjs ,一般的 post 的请求直接在 c 就能拿到请求的参数,但是 /upload 的接口就不行,拿到的 body 是 {} ,下面我们来逐步分析。

js 中的文件

web 中的 Blob 、file 和 Formdate

一个 Blob ( Binary Large Object ) 对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式。 File 接口基于Blob,继承 Blob 功能并将其扩展为支持用户系统上的文件。

前端上传文件的方式无非就是使用:1、表单自动上传;2、使用 ajax 上传。我们可以使用以下代码创建一个 Form,并打印出 file

<form method="POST" id="uploadForm" enctype="multipart/form-data"> <input type="file" id="file" name="file" /> </form> <button id="submit">submit</button> <script src=";></script> <script> $("#submit").click(function() { con($("#file")[0].files[0]) }); </script> 复制代码

从 F12 中可以看出 File 原型链上是 Blob。简单地说 Blob 可以理解为 Web 中的二进制文件。 而 File 是基于 Blob 实现的一个类,新增了关于文件有关的一些信息。

FormData 对象的作用就类似于 Jq 的 serialize() 方法,不过 FormData 是浏览器原生的,且支持二进制文件。 ajax 通过 FormData 这个对象发送表单请求,无论是原生的 XMLHttprequest 、jq 的 ajax 方法、 axios 都是在 data 里直接指定上传 formData 类型的数据,fetch api 是在 body 里上传。

forData 数据有两种方式生成,如下 formData 和 formData2 的区别,而 formData2 可以通过传入一个 element 的方式进行初始化,初始化之后依然可以调用 formData 的 append 方法。

<!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=";></script> </html> <script> $("#submit").click(function() { const file = $("#fileImag")[0].files[0]; const formData = new FormData(); ("fileImag", file); con("fileImag")); const formData2 = new FormData("#uploadForm")); // const formData2 = new FormData("uploadFormName");); con("configFile")); }); </script> 复制代码

node 中的 buffer 、 Stream 、fs

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"); (".;, function(err, buffer) { if (err) throw err; con("buffer", buffer); }); function readLines(input, func) { var remaining = ""; in("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); }); in("end", function() { if > 0) { func(remaining); } }); } function func(data) { con("Line: " + data); } var input = (".;); in("binary"); readLines(input, func); 复制代码

() 函数会缓冲整个文件。 为了最小化内存成本,尽可能通过 () 进行流式传输。

使用 nodejs 创建 uoload api

http 协议中的文件上传

在 http 的请求头中 Content-type 是 multipart/form-data 时,请求的内容如下:

POST / HTTP 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 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKi (KHTML, like Gecko) Chrome Safari ------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

使用原生的 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 == "/upload" && req.me() === "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 == "/upload" && req.me() === "post") { if ["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 = query(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 ("\\") != -1) { fileName = ("\\") + 1); } con("文件名: " + fileName); } } // 获取图片类型(如:image/gif 或 image/png)) const entireData = body.toString(); const contentTypeRegex = /Content-Type: image\/.*/; contentType = file["Content-Type"].substring(1); //获取文件二进制数据开始位置,即contentType的结尾 const upperBoundary = en(contentType) + con; const shorterData = en(upperBoundary); // 替换开始位置的空格 const binaryDataAlmost = shorterData .replace(/^\s\s*/, "") .replace(/\s\s*$/, ""); // 去除数据末尾的额外数据,即: "--"+ boundary + "--" const binaryData = binaryDa( 0, binaryDa("--" + boundary + "--") ); // con("binaryData", binaryData); const bufferData = new Bu(binaryData, "binary"); con("bufferData", bufferData); // (fileName, binaryData, "binary", function(err) { // res.end("sucess"); // }); (fileName, bufferData, function(err) { res.end("sucess"); }); } else { res.end("reupload"); } }); } 复制代码
  • 通过 req.setEncoding("binary"); 拿到图片的二进制数据。可以通过以下两种方式处理二进制数据,写入文件。
(fileName, binaryData, "binary", function(err) { res.end("sucess"); }); 复制代码 (fileName, bufferData, function(err) { res.end("sucess"); }); 复制代码

koa

在 koa 中使用 koa-body 可以通过 c 拿到上传的 file 对象。下面是例子。

'use strict'; const Koa = require('koa'); const app = new Koa(); const router = require('koa-router')(); const koaBody = require('../index')({multipart:true}); rou('/users', koaBody, (ctx) => { con); // => POST body c = JSON.stringify, null, 2); } ); rou('/', (ctx) => { c('Content-Type', 'text/html'); c = ` <!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>`; }); rou('/', koaBody, (ctx) => { con('fields: ', c); // => {username: ""} - if empty con('files: ', c); /* => {uploads: [ { "size": 748831, "path": "/tm;, "name": "some-image.png", "type": "image/png", "mtime": "2014-06-17T11:08:52.816Z" }, { "size": 379749, "path": "/tm;, "name": "nodej;, "type": "image/jpeg", "mtime": "2014-06-17T11:08:52.830Z" } ]} */ c = JSON.stringify, null, 2); } ) a()); const port = || 3333; a(port); con('Koa server with `koa-body` parser start listening to port %s', port); con('curl -i http://localhost:%s/users -d "user=admin"', port); con('curl -i http://localhost:%s/ -F "source=@/path/to;', port); 复制代码

我们来看一下 koa-body 的实现

const forms = require('formidable'); function requestbody(opts) { opts = opts || {}; ... o = 'multipart' in opts ? o : false; o = 'formidable' in opts ? o : {}; ... // @todo: next major version, o support should be removed if (o && o) { throw new Error('Cannot use strict and parsedMethods options at the same time.') } if ('strict' in opts) { con('DEPRECATED: o has been deprecated in favor of o.') if (o) { o = ['POST', 'PUT', 'PATCH'] } else { o = ['POST', 'PUT', 'PATCH', 'GET', 'HEAD', 'DELETE'] } } o = 'parsedMethods' in opts ? o : ['POST', 'PUT', 'PATCH'] o = o.map(function (method) { return me() }) return function (ctx, next) { var bodyPromise; // only parse the body on specifically chosen methods if (o.includes(ctx.me())) { try { if && c(jsonTypes)) { bodyPromise = buddy.json(ctx, { encoding: o, limit: o, strict: o, returnRawBody: o }); } else if (o && c('multipart')) { bodyPromise = formy(ctx, o); } } catch (parsingError) { if (typeof o === 'function') { o(parsingError, ctx); } else { throw parsingError; } } } bodyPromise = bodyPromise || Promi({}); /** * 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 o && c('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 (opts); ('end', function () { return resolve({ fields: fields, files: files }); }).on('error', function (err) { return reject(err); }).on('field', function (field, value) { if (fields[field]) { if (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 (files[field])) { files[field].push(file); } else { files[field] = [files[field], file]; } } else { files[field] = file; } }); if ) { ('fileBegin', o); } ); }); } 复制代码

代码中删除了影响有关文件上传的相关逻辑

  • 首先 multipart 为 true 是开启文件上传的关键。
  • 然后 formy 函数处理了 http 解析和保存的一系列过程,最终将 files 抛出进行统一处理。代码中依赖了 formidable 这个库,我们其实也可以直接使用这个库对文件进行处理。(上面的原生 node upload 只是简单地处理了一下)
  • o 是 formidable 的 config 可以设置文件大小,保存的文件路径等等。

eggjs

使用 eggjs 进行文件上传需要现在配置文件中开启

con = { mode: "file", fileSize: "600mb" }; 复制代码

然后通过 c[0] 就能取到文件信息。

文件上传接口的转发

一千个观众眼中有一千个哈姆雷特,通过以上知识点的梳理,我相信你也有了自己得想法。在这里说一下我是怎么处理的。 在 egg 中我使用了 request-promise 去做接口转发,通过查看 api 和 c[0] 拿到的信息,我做了以下处理。

if (method === "POST") { o = reque; o = true; if (url === uploadeUrl) { delete o; o = { // Like <input type="text" name="name"> name: "file", // Like <input type="file" name="file"> file: { value: (c[0].filepath), options: { filename: c[0].filename, contentType: c("content-type") } } }; } } else { o = query; } 复制代码

总结

  • http 中的文件上传第一步就是设置 Content-type 为 multipart/form-data 的 header。
  • 区分好 web 端 js 和 node 端处理文件的方式有所不同。
  • 有些 npm 模块的 readme 并不是很清晰,可以直接下源码去看 example ,或者直接读源码,就比如 koa-body 中 formidable 的用法并未在他的 reademe 中写出,直接看源码是你会发现更多用法。
  • 文中的知识点很多知识稍微提及,可以进一步深入了解与他相关的知识。
  • 最后如果文中有任何错误和疑问请及时指出。

关于作者: admin

无忧经验小编鲁达,内容侵删请Email至wohenlihai#qq.com(#改为@)

热门推荐