node 文件上传接口的转发的实现


Posted in Javascript onSeptember 23, 2019

近期的项目里使用了这样一个项目架构: 前端 -> nodejs -> java

  • 前端负责实现业务逻辑的展示和交互
  • nodejs 包括维护某些数据和接口转发
  • java 负责维护剩下的数据

在 nodejs 的接口转发中拦截一部分接口,再对请求的方法进行区分,请求后台数据后,再进行返回。现有的接口中基本只用到了 get 和 post 两种,但是在文件上传的时候遇到了问题。

node 层使用 eggjs ,一般的 post 的请求直接在 ctx.body 就能拿到请求的参数,但是 /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="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>

<script>
 $("#submit").click(function() {
  console.log($("#file")[0].files[0])
 });
</script>

node 文件上传接口的转发的实现

从 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="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)

  • 如果是使用 new FormData(element) 的创建方式,上面 key 为 <input /> 上的 name 字段。
  • 如果是使用 append 添加的数据,get/getAll 时 key 为 append 所指定的 key。

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");

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() 进行流式传输。

使用 nodejs 创建 uoload api

http 协议中的文件上传

在 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

使用原生的 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 中使用 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);
 });
}

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

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

 eggjs

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

config.multipart = { mode: "file", fileSize: "600mb" };

然后通过 ctx.request.files[0] 就能取到文件信息。

文件上传接口的转发

一千个观众眼中有一千个哈姆雷特,通过以上知识点的梳理,我相信你也有了自己得想法。在这里说一下我是怎么处理的。 在 egg 中我使用了 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;
 }

总结

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

参考

https://www.npmjs.com/package/formidable

https://github.com/dlau/koa-body

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
网页中实现浏览器的最大,最小化和关闭按钮
Mar 12 Javascript
IE6背景图片不缓存问题解决方案及图片使用策略多个方法小结
May 14 Javascript
Jquery遍历节点的方法小集
Jan 22 Javascript
jQuery 遍历函数详解
Jul 05 Javascript
jQuery焦点图切换特效代码分享
Sep 15 Javascript
js判断iframe中元素是否存在的实现代码
Dec 24 Javascript
JavaScript实现选中文字提示新浪微博分享效果
Jun 15 Javascript
vue路由传参三种基本方式详解
Dec 09 Javascript
Vue+Vuex实现自动登录的知识点详解
Mar 04 Javascript
Vue+Element ui 根据后台返回数据设置动态表头操作
Sep 21 Javascript
vue打开子组件弹窗都刷新功能的实现
Sep 21 Javascript
vite2.0+vue3移动端项目实战详解
Mar 03 Vue.js
layui 上传文件_批量导入数据UI的方法
Sep 23 #Javascript
Electron 调用命令行(cmd)
Sep 23 #Javascript
layui文件上传控件带更改后数据传值的方法
Sep 23 #Javascript
原生JavaScript实现日历功能代码实例(无引用Jq)
Sep 23 #Javascript
小程序实现上下移动切换位置
Sep 23 #Javascript
微信小程序分包加载代码实现方法详解
Sep 23 #Javascript
layui扩展上传组件模拟进度条的方法
Sep 23 #Javascript
You might like
php开发文档 会员收费1期
2012/08/14 PHP
JSON在PHP中的应用介绍
2012/09/08 PHP
浅析php fwrite写入txt文件的时候用 \r\n不能换行的问题
2013/08/06 PHP
使用XHProf查找PHP性能瓶颈的实例
2017/12/13 PHP
stripos函数知识点实例分享
2019/02/11 PHP
javascript学习笔记(十五) js间歇调用和超时调用
2012/06/20 Javascript
js动态为代码着色显示行号
2013/05/29 Javascript
JavaScript实现简单的时钟实例代码
2013/11/23 Javascript
qq悬浮代码(兼容各个浏览器)
2014/01/29 Javascript
在linux中使用包管理器安装node.js
2015/03/13 Javascript
JS变量中有var定义和无var定义的区别以及es6中let命令和const命令
2017/02/19 Javascript
基于ExtJs在页面上window再调用Window的事件处理方法
2017/07/26 Javascript
利用jsonp与代理服务器方案解决跨域问题
2017/09/14 Javascript
详解vue-cli中的ESlint配置文件eslintrc.js
2017/09/25 Javascript
es6函数之rest参数用法实例分析
2020/04/18 Javascript
关于Node.js中频繁修改代码重启服务器的问题
2020/10/15 Javascript
[11:57]《一刀刀一天》第十七期:TI中国军团加油!
2014/05/26 DOTA
[01:27]DOTA2电竞之夜 今夜共饮庆功酒
2014/08/02 DOTA
python with statement 进行文件操作指南
2014/08/22 Python
Java中重定向输出流实现用文件记录程序日志
2015/06/12 Python
Python利用BeautifulSoup解析Html的方法示例
2017/07/30 Python
Python生成器的使用方法和示例代码
2019/03/04 Python
pyqt5利用pyqtDesigner实现登录界面
2019/03/28 Python
pycharm显示远程图片的实现
2019/11/04 Python
将python安装信息加入注册表的示例
2019/11/20 Python
tensorflow 2.1.0 安装与实战教程(CASIA FACE v5)
2020/06/30 Python
html5 利用canvas实现超级玛丽简单动画
2013/09/06 HTML / CSS
美国在线自行车商店:Jenson USA
2018/05/22 全球购物
资产评估专业大学生求职信
2013/09/29 职场文书
艺术爱好者的自我评价分享
2013/10/08 职场文书
暑期实践思想汇报
2014/01/06 职场文书
闭幕式主持词
2014/04/02 职场文书
家长会学生演讲稿
2014/04/26 职场文书
2014年教育培训工作总结
2014/12/08 职场文书
检讨书大全
2015/01/27 职场文书
初中教师德育工作总结2015
2015/05/12 职场文书