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 相关文章推荐
javascript 构建一个xmlhttp对象池合理创建和使用xmlhttp对象
Jan 15 Javascript
js 未结束的字符串常量错误解决方法
Jun 13 Javascript
5秒后跳转到另一个页面的js代码
Oct 12 Javascript
js实现带搜索功能的下拉框实时搜索实时匹配
Nov 05 Javascript
javascript实现存储hmtl字符串示例
Apr 25 Javascript
jQuery 获取兄弟元素的几种不错方法
May 23 Javascript
js中将String转换为number以便比较
Jul 08 Javascript
Jquery 实现table样式的设定
Jan 28 Javascript
jQuery实现Flash效果上下翻动的中英文导航菜单代码
Sep 22 Javascript
Vue开发过程中遇到的疑惑知识点总结
Jan 20 Javascript
一个简易时钟效果js实现代码
Mar 25 Javascript
用vue写一个日历
Nov 02 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
咖啡产品发展的三大浪潮
2021/03/04 咖啡文化
浅谈php中mysql与mysqli的区别分析
2013/06/10 PHP
PHP 反射(Reflection)使用实例
2015/05/12 PHP
PHP mysql事务问题实例分析
2016/01/18 PHP
PHP给文字内容中的关键字进行套红处理
2016/04/12 PHP
php递归函数怎么用才有效
2018/02/24 PHP
Js操作Select大全(取值、设置选中等等)
2013/10/29 Javascript
jQuery的each终止或跳过示例代码
2013/12/12 Javascript
jquery mobile页面跳转后样式丢失js失效的解决方法
2014/09/06 Javascript
JavaScript获取网页支持表单字符集的方法
2015/04/02 Javascript
JavaScript动态修改背景颜色的方法
2015/04/16 Javascript
smartcrop.js智能图片裁剪库
2015/10/14 Javascript
41个Web开发者必须收藏的JavaScript实用技巧
2016/07/22 Javascript
基于jQuery实现文字打印动态效果
2017/04/21 jQuery
angular第三方包开发整理(小结)
2018/04/19 Javascript
vue组件是如何解析及渲染的?
2021/01/13 Vue.js
Python实现获取网站PR及百度权重
2015/01/21 Python
python实现用户登陆邮件通知的方法
2015/07/09 Python
详解python 发送邮件实例代码
2016/12/22 Python
Python网络编程基于多线程实现多用户全双工聊天功能示例
2018/04/10 Python
django之跨表查询及添加记录的示例代码
2018/10/16 Python
在python中利用最小二乘拟合二次抛物线函数的方法
2018/12/29 Python
Python字典的基本用法实例分析【创建、增加、获取、修改、删除】
2019/03/05 Python
python+pyqt5实现图片批量缩放工具
2019/03/18 Python
详解用python实现基本的学生管理系统(文件存储版)(python3)
2019/04/25 Python
python celery分布式任务队列的使用详解
2019/07/08 Python
详解基于Jupyter notebooks采用sklearn库实现多元回归方程编程
2020/03/25 Python
Pycharm快捷键配置详细整理
2020/10/13 Python
python语言实现贪吃蛇游戏
2020/11/13 Python
Silk’n激光脱毛器官网:silkn.com
2016/10/06 全球购物
营销与策划专业毕业生求职信
2013/11/01 职场文书
口腔医学技术应届生求职信
2013/11/09 职场文书
西安兵马俑导游词
2015/02/02 职场文书
公司中层管理培训心得体会
2016/01/11 职场文书
详解PHP设计模式之依赖注入模式
2021/05/25 PHP
SQL Server数据库备份和恢复数据库的全过程
2022/06/14 SQL Server