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 相关文章推荐
JQuery Dialog的内存泄露问题解决方法
Jun 18 Javascript
jquery 卷帘效果实现代码(不同方向)
Feb 05 Javascript
javascript禁用键盘功能键让右击及其他键无效
Oct 09 Javascript
Js实现动态添加删除Table行示例
Apr 14 Javascript
对于jQuery性能的一些优化建议
Aug 13 Javascript
谈谈我对JavaScript原型和闭包系列理解(随手笔记6)
Dec 20 Javascript
javascript制作照片墙及制作过程中出现的问题
Apr 04 Javascript
简单实现js浮动框
Dec 13 Javascript
关于iframe跨域POST提交的方法示例
Jan 15 Javascript
vue实现移动端图片裁剪上传功能
Aug 18 Javascript
解决layer弹层遮罩挡住窗体的问题
Aug 17 Javascript
node.js爬取中关村的在线电瓶车信息
Nov 13 Javascript
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中创建并处理图象
2006/10/09 PHP
《PHP边学边教》(04.编写简易的通讯录――视频教程1)
2006/12/13 PHP
win10环境PHP 7 安装配置【教程】
2016/05/09 PHP
PHP入门教程之面向对象基本概念实例分析
2016/09/11 PHP
jQuery 学习入门篇附实例代码
2010/03/16 Javascript
js indexOf()定义和用法
2012/10/21 Javascript
jquery跨域请求示例分享(jquery发送ajax请求)
2014/03/25 Javascript
js打造数组转json函数
2015/01/14 Javascript
jQuery插件实现大图全屏图片相册
2015/03/14 Javascript
js实现文字向上轮播功能
2017/01/13 Javascript
jQuery基于Ajax方式提交表单功能示例
2017/02/10 Javascript
vue-cli开发时,关于ajax跨域的解决方法(推荐)
2018/02/03 Javascript
vue的传参方式汇总和router使用技巧
2018/05/22 Javascript
小程序tab页无法传递参数的方法
2018/08/03 Javascript
Angular8 实现table表格表头固定效果
2020/01/03 Javascript
JavaScript Event Loop相关原理解析
2020/06/10 Javascript
解决Vue @submit 提交后不刷新页面问题
2020/07/18 Javascript
Python列表生成器的循环技巧分享
2015/03/06 Python
python判断windows系统是32位还是64位的方法
2015/05/11 Python
Python cv2 图像自适应灰度直方图均衡化处理方法
2018/12/07 Python
python实现整数的二进制循环移位
2019/03/08 Python
python3 小数位的四舍五入(用两种方法解决round 遇5不进)
2019/04/11 Python
PyQt QCombobox设置行高的方法
2019/06/20 Python
Python实现非正太分布的异常值检测方式
2019/12/09 Python
Python3 实现爬取网站下所有URL方式
2020/01/16 Python
学会python自动收发邮件 代替你问候女友
2020/05/20 Python
从python读取sql的实例方法
2020/07/21 Python
python利用xpath爬取网上数据并存储到django模型中
2021/02/26 Python
html5实现多图片预览上传及点击可拖拽控件
2018/03/15 HTML / CSS
采购类个人求职的自我评价
2014/02/18 职场文书
公安交警中队队长个人对照检查材料思想汇报
2014/10/05 职场文书
医院科室评语
2015/01/04 职场文书
2016元旦晚会主持词
2015/07/01 职场文书
JavaScript 反射学习技巧
2021/10/16 Javascript
SQL试题 使用窗口函数选出连续3天登录的用户
2022/04/24 Oracle
修改Nginx配置返回指定content-type的方法
2022/09/23 Servers