DDR爱好者之家 Design By 杰米

近期的项目里使用了这样一个项目架构: 前端 -> 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="/UploadFiles/2021-04-02/jquery.min.js">

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="/UploadFiles/2021-04-02/jquery.min.js">

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: "upload"

http://localhost:3000
------WebKitFormBoundaryoMwe4OxVN0Iuf1S4
Content-Disposition: form-data; name="upload"; filename="IMG_9429.JPG"
Content-Type: image/jpeg

"color: #ff0000">原生 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 "htmlcode">
 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.jb51.net/article/170637.htm

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

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

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

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线

暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。

艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。

《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。