百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

文件上传接口的转发(node)(上传文件api接口)

bigegpt 2024-08-02 11:03 3 浏览

场景

近期的项目里使用了这样一个项目架构: 前端 -> 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>
复制代码

从 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>
复制代码

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 中写出,直接看源码是你会发现更多用法。
  • 文中的知识点很多知识稍微提及,可以进一步深入了解与他相关的知识。
  • 最后如果文中有任何错误和疑问请及时指出。

相关推荐

得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践

一、摘要在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障Web应用和移动端的可用性与性能。传统方案往往依赖代理Agent→消息队列→流计算引擎→OLAP存储...

warm-flow新春版:网关直连和流程图重构

本期主要解决了网关直连和流程图重构,可以自此之后可支持各种复杂的网关混合、多网关直连使用。-新增Ruoyi-Vue-Plus优秀开源集成案例更新日志[feat]导入、导出和保存等新增json格式支持...

扣子空间体验报告

在数字化时代,智能工具的应用正不断拓展到我们工作和生活的各个角落。从任务规划到项目执行,再到任务管理,作者深入探讨了这款工具在不同场景下的表现和潜力。通过具体的应用实例,文章展示了扣子空间如何帮助用户...

spider-flow:开源的可视化方式定义爬虫方案

spider-flow简介spider-flow是一个爬虫平台,以可视化推拽方式定义爬取流程,无需代码即可实现一个爬虫服务。spider-flow特性支持css选择器、正则提取支持JSON/XML格式...

solon-flow 你好世界!

solon-flow是一个基础级的流处理引擎(可用于业务规则、决策处理、计算编排、流程审批等......)。提供有“开放式”驱动定制支持,像jdbc有mysql或pgsql等驱动,可...

新一代开源爬虫平台:SpiderFlow

SpiderFlow:新一代爬虫平台,以图形化方式定义爬虫流程,不写代码即可完成爬虫。-精选真开源,释放新价值。概览Spider-Flow是一个开源的、面向所有用户的Web端爬虫构建平台,它使用Ja...

通过 SQL 训练机器学习模型的引擎

关注薪资待遇的同学应该知道,机器学习相关的岗位工资普遍偏高啊。同时随着各种通用机器学习框架的出现,机器学习的门槛也在逐渐降低,训练一个简单的机器学习模型变得不那么难。但是不得不承认对于一些数据相关的工...

鼠须管输入法rime for Mac

鼠须管输入法forMac是一款十分新颖的跨平台输入法软件,全名是中州韵输入法引擎,鼠须管输入法mac版不仅仅是一个输入法,而是一个输入法算法框架。Rime的基础架构十分精良,一套算法支持了拼音、...

Go语言 1.20 版本正式发布:新版详细介绍

Go1.20简介最新的Go版本1.20在Go1.19发布六个月后发布。它的大部分更改都在工具链、运行时和库的实现中。一如既往,该版本保持了Go1的兼容性承诺。我们期望几乎所...

iOS 10平台SpriteKit新特性之Tile Maps(上)

简介苹果公司在WWDC2016大会上向人们展示了一大批新的好东西。其中之一就是SpriteKitTileEditor。这款工具易于上手,而且看起来速度特别快。在本教程中,你将了解关于TileE...

程序员简历例句—范例Java、Python、C++模板

个人简介通用简介:有良好的代码风格,通过添加注释提高代码可读性,注重代码质量,研读过XXX,XXX等多个开源项目源码从而学习增强代码的健壮性与扩展性。具备良好的代码编程习惯及文档编写能力,参与多个高...

Telerik UI for iOS Q3 2015正式发布

近日,TelerikUIforiOS正式发布了Q32015。新版本新增对XCode7、Swift2.0和iOS9的支持,同时还新增了对数轴、不连续的日期时间轴等;改进TKDataPoin...

ios使用ijkplayer+nginx进行视频直播

上两节,我们讲到使用nginx和ngixn的rtmp模块搭建直播的服务器,接着我们讲解了在Android使用ijkplayer来作为我们的视频直播播放器,整个过程中,需要注意的就是ijlplayer编...

IOS技术分享|iOS快速生成开发文档(一)

前言对于开发人员而言,文档的作用不言而喻。文档不仅可以提高软件开发效率,还能便于以后的软件开发、使用和维护。本文主要讲述Objective-C快速生成开发文档工具appledoc。简介apple...

macOS下配置VS Code C++开发环境

本文介绍在苹果macOS操作系统下,配置VisualStudioCode的C/C++开发环境的过程,本环境使用Clang/LLVM编译器和调试器。一、前置条件本文默认前置条件是,您的开发设备已...