关于文件上传:以寡治众各个击破超大文件分片上传之构建基于Vuejs30AntdesginTornado6纯异步IO高效写入服务

原文转载自「刘悦的技术博客」https://v3u.cn/a_id_218 分治算法是一种很古老但很求实的办法。本意即便将一个较大的整体打碎分成小的部分,这样每个小的部分都不足以反抗大的整体。战国时期,秦国毁坏合纵的连横即是一种分而治之的伎俩;十九世纪,比利时殖民者霸占卢旺达, 将卢旺达的种族分为胡图族与图西族,以图进行决裂管制,莫不如是。 21世纪,人们往往会在Leetcode平台上刷分治算法题,但事实上,从工业角度上来看,算法如果不和理论业务场景相结合,算法就永远是扑朔迷离的存在,它只会呈现在开发者的某一次不经意的面试中,而实在的算法,并不是虚空的,它应该能帮忙咱们解决理论问题,是的,它应该落地成为实体。 大文件分片上传就是这样一个符合分治算法的场景,现而今,视频文件的体积越来越大,高清视频体积大略2-4g不等,但4K视频的分辨率是规范高清的四倍,须要四倍的存储空间——只需两到三分钟的未压缩4K 电影,或者电影预告片的长度,就能够达到500GB。 8K视频文件更是大得难以想象,而当初12K正在呈现,如此微小的文件,该怎么设计一套正当的数据传输计划?这里咱们以前后端拆散我的项目为例,前端应用Vue.js3.0配合ui库Ant-desgin,后端采纳并发异步框架Tornado实现大文件的分片无阻塞传输与异步IO写入服务。 前端分片首先,装置Vue3.0以上版本: npm install -g @vue/cli装置异步申请库axios: npm install axios --save随后,装置Ant-desgin: npm i --save ant-design-vue@next -SAnt-desgin尽管因为已经的圣诞节“彩蛋门”事件而身败名裂,但主观地说,它仍然是业界不可多得的优良UI框架之一。 接着在我的项目程序入口文件引入应用: import { createApp } from 'vue' import App from './App.vue' import { router } from './router/index' import axios from 'axios' import qs from 'qs' import Antd from 'ant-design-vue'; import 'ant-design-vue/dist/antd.css'; const app = createApp(App) app.config.globalProperties.axios = axios; app.config.globalProperties.upload_dir = "https://localhost/static/"; app.config.globalProperties.weburl = "http://localhost:8000"; app.use(router); app.use(Antd); app.mount('#app')随后,参照Ant-desgin官网文档:https://antdv.com/components/... 构建上传控件: ...

July 25, 2022 · 3 min · jiezi

node如何实现大文件上传

在实际开发过程中我们可能会遇到大文件上传的场景,在node中是如何实现的呢?如果还是采用将文件一次性读写到服务端,将非常耗时、耗内存,而且网络发生中断后又要重新上传,性能很低。那如何实现一个高性能的上传功能呢?本文将为您一一揭晓。1、大文件上传的基本流程前端对文件进行MD5加密,生成MD5值,这个值是文件的唯一标识,可以用来校验文件的完整性。发送请求校验文件的MD5值,检测文件是否上传,如果文件已经上传,则不用上传;如果文件上传了一部分,则把剩余文件块上传;如果没有上传过,则全部上传。确定文件块大小,切分文件,并发调用上传接口,将文件块上传到服务端。前端上传完文件分片后请求通知后端合并文件块,整合成初始文件。2、生成文件MD5值1、什么是MD5? MD5信息摘要算法(英语:MD5 Message-Digest Algorithm),一种被广泛使用的密码散列函数,可以产生出一个128位(16字节)的散列值(hash value),用于确保信息传输完整一致。2、怎样生成文件MD5值?我是用spark-md5.js库生成文件MD5值。SparkMD5是MD5算法的快速实现,非常适合浏览器使用,因为nodejs版本可能更快。 spark-md5.js库github链接:https://github.com/satazor/js-spark-md5 官网告诉我们增量MD5在散列大型数据(文件)时表现更好,首先使用FileReader和Blob以块的形式读取文件,将块追加到MD5散列中,最终会生成一个文件MD5值,同时生成过程保持了低内存使用率。 FileReader对象允许Web应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用File或Blob对象指定要读取的文件或数据。 FileReader说明链接:https://developer.mozilla.org/zh-CN/docs/Web/API/FileReader 3、校验文件MD5值MD5值作为一个存放文件块的目录,检验MD5值即使检验该目录是否存在,如果存在,则文件已经完全上传或上传了一部分;如果不存在,则该文件没有被上传。 4、分片上传 5、服务端合并文件 源码参考github链接:https://github.com/Revelation2019/node-参考文章:https://segmentfault.com/a/1190000008899001

October 16, 2019 · 1 min · jiezi

使用FormData对象上传文件

<input type="file" name="file" id='chooseFile'/>var form = document.getElementById("chooseFile");form.addEventListener('change', function(ev) {//监听file控件值的变化 var oData = new FormData(); var file=this.files[0]; //拼接参数 formData.append('file', file); // 文件 formData.append('params', '参数'); formData.append('array',JSON.stringify([1,2,3])); var xhr = new XMLHttpRequest(); xhr.open("post",url,true); //post方式,url为服务器请求地址,true 该参数规定请求是否异步处理。 xhr.upload.onloadstart = function(){//上传开始 //... }; xhr.upload.onprogress = function(evt){//显示上传进度条 var loaded = evt.loaded;//已经上传大小情况 var tot = evt.total;//附件总大小 var per = Math.floor(100 * loaded / tot); //已经上传的百分比 $('#uploadprogress').removeClass('hide').css('width',per+'%'); }; xhr.onload = function (evt) {//上传完成 //... }; xhr.onerror = function () { alert('上传失败'); }; xhr.send(oData); //开始上传,发送form数据}, false);拖拽上传 ...

May 18, 2019 · 1 min · jiezi

关于七牛云正确使用姿势探索

业务场景需求我们项目有一个文件上传需求,需要从客户端上传到七牛云的对象存储和自己的应用服务器上。这里使用七牛云主要是实现下载分发。应用服务器需要留一份是因为后续需要做文件分析(并且是上传后需要立马分析出结果展现给客户端)。另外,由于是初期项目,暂时没考虑用独立服务器来分析。所用技术栈服务器:Centos7开发语言:PHP框架:Laravel前端上传组件:百度的WebUploader解决方案准确的说我经过了三个阶段才真正完美的实现了需求(主要解决上传速度)。一期解决方案及细节初期面对需求很容易想到的思路是:客户端先上传文件到应用服务器(因为上传完成可以及时做分析),然后再上传到七牛云上。所以我的解决方案是:前端用webuploader,后端的七牛云文件处理方面使用了Laravel的一个插件:overtrue/flysystem-qiniu (https://github.com/overtrue/f…,该插件的接口很简洁好用(但是有坑,后面会说到)。然后为了解决性能问题,我还做了以下工作:1,使用分片上传2,后续上传七牛云使用异步的方式(因为文件上传到其他应用来下载这个文件,中间有许多时间来让上传任务的完成)关于分片上传这里讲下分片上传的实现思路,客户端主要是把大文件按一定size进行分片,然后上传到服务器,所以会有多个请求,并且每个请求还需带上关键的信息:当前chunk(从0开始)和chunks(总分片数)。由于我用的是webuploader组件,所以客户端不用自己做什么,只需配置下简单信息(是否分片及分片大小)。服务端处理逻辑为:客户端一个请求过来,分两种情况:1,文件总size小于要分片的size,这时候直接处理文件。2,处理分片情况。具体逻辑是判断chunk和chunks,如果相等说明为第一种情况,直接处理上传,其他走处理分片逻辑。处理分片的逻辑为:保存当前分片到临时目录(按分片命名),然后判断当所有分片完成时,就合并文件。具体逻辑是判断 chunk + 1 是否等于chunks。 合并逻辑就是循环读取临时文件,然后写入到一个新的文件(合并后的),这里可以顺便删除临时文件。所遇的坑:这里处理碎片文件时,当初图方便使用了Laravel的文件处理接口Storage::append,但是这个接口有个坑就是它自作主张的文件结尾加入换行符。导致合并后的文件还原不成原始文件。解决办法是老老实实使用php的fopen、fwrite、fclose这一套。关于PHP异步处理关于PHP的异步实现可以参考鸟哥写的文章:http://www.laruence.com/2008/…主要方法为:客户端AJAX、popen函数、curl、fsocketopen等不过这篇文章比较老了,局限性也大,现在有了协程等处理方案(现在Swoole也提供协程方案了,并且client-server task分发这种也可以用swoole的),而且往架构方面考虑可以使用队列等(感觉靠谱的还是队列)。PS: 我这里前期用的是简单粗暴的popen,后来使用的是Laravel提供的队列。一期方案的问题通过上述所说的方案,很容易就实现了一个版本。但是没高兴多久。。,在后续测试时遇到一个诡异bug,当文件过大时,任务脚本上传到七牛云失败。这里脚本是写在Laravel的artisan中的,当我把脚本命令直接在终端调试时也是没有任何异常(准确讲是看不了任何异常)。前面我说过七牛这块SDK用的是overtrue/flysystem-qiniu ,并且为了考虑性能问题用的是他的writeStream接口。 $disk = Storage::disk(‘qiniu’); $stream = fopen($localFileName, ‘r’); $disk->writeStream($fileName, $stream); if (is_resource($stream)) { fclose($stream); }代码表面上看起来很理想,用的是文件流上传(怕吃内存)。但结果证明一切只是表面上的。。当我遇到大文件无法上传到七牛云时,断点调试到$disk->writeStream这里,发现返回的是false。 继而调试到overtrue/flysystem-qiniu这个扩展的源代码。然后发现了一个大坑。。主要是两个问题:1,writeStream只是个假的流写入具体源码在扩展的QiniuAdapter.php文件中,这里贴段代码:public function writeStream($path, $resource, Config $config){ $contents = ‘’; while (!feof($resource)) { $contents .= fread($resource, 1024); } $response = $this->write($path, $contents, $config); if (false === $response) { return $response; } return compact(‘path’);}注意这里的$contents变量,最终还是等价于一个大文件内容的大小(服务器为此变量开辟的内存)。并且后续还要在方法间传递。所以这里是假的流!2,接口对调试不友好还有在write方法中,屏蔽了$error,只返回false,这样不便于我们查问题,最终我是断点打印这个$error才知道报的错误是:“invalid multipart format: multipart: message too large”,这个应该是七牛那边真正返回的,但这么重要的信息被这个扩展屏蔽了。二期解决方案知道了一期方案的具体问题所在,我就一直在思考(那个扩展就不提了。。我现在怀疑它的存在意义。。),甚至在想也许一开始整个思路就错了(通过SDK上传文件的方案)。后来还真被我找到了,七牛云官方提供一个脚本工具:Qshell(https://github.com/qiniu/qshell)。这个是命令行运行脚本,具体操作看文档就可以了。放到我的项目也是集成到七牛的任务脚本中。后来测试可以了,整个流程可以跑通。但是无意中发现二期的重要问题,这个上传走的是服务器的上行带宽!而我们平常付费买的带宽就是买的上行带宽!(下行是一般是免费的)。这还怎么搞!由于我们上传业务是商户端使用的,平时使用频次也不会太少,这会导致在上传时影响前端网站的访问速度。这里具体讲下服务器带宽问题(网上查询后整理的):首先对服务器带宽方向的描述一般是用上行和下行,上传和下载是指动作。上行是指从服务器流出的带宽,如果是在其他机器下载服务器上的文件,用的主要是服务器的上行带宽(这里说下我们平时的网页浏览,其实也是不同客户端从服务器下数据, html文件、css等然后渲染,所以网页浏览占用的也是上行带宽)。下行是指流入到服务器的带宽,如果是在其他机器上传文件到服务器,比如用FTP上传文件,用的主要是服务器的下行带宽(服务器上下载文件用的也是下行带宽)。现在的云提供商比如阿里云不限制的是下行带宽,大部分服务器的使用环境,都是上行带宽用的多,下行带宽用的少。通过对带宽的理解,再回到我们项目的上传实现思路,可以看到一开始就错了(不该用应用服务器作为中转)!三期(最终)方案当初为了节省时间,直接跳过官方文档,而使用第三方扩展。 现在看来,不得不又回到官方文档了。通过把七牛的文档过一遍,发现是有方案可以避开那个占用服务器上行带宽的问题的。主体思路是要避开应用服务器上行带宽的使用,因为上行带宽很宝贵,尽量使用下行带宽(免费、速度很快!阿里的大概60M多每秒)。具体实现是通过七牛的表单上传方案直接把客户端的文件先上传到七牛(这一步根本不关应用服务器什么事,所以避开了,而且直接上传到七牛的速度非常快,基本只取决于用户端的网速,而且对于一般需求,七牛提供了对于到我们应用服务器的回调方法)。然后由于我们应用服务器也需要文件,所以方案是直接在我们应用服务器直接下载七牛的文件(这里可以同步阻塞住,前端做个等待效果解决用户体验问题)。因为前面说到流入到服务器占用的是下行带宽。所以这里速度也会非常快(而且是免费的^_^)。这种方案基本是完美的了。总结首先是对个人的反省,前期调研不充足,但是项目初期有点紧,这里也说明投入时间的重要性。其次关于项目经验:上传第三方云存储,千万不要使用应用服务器做中转!可以直接上传到第三方云服务器,如果有后续处理逻辑的,可以使用他们的回调接口。

April 10, 2019 · 1 min · jiezi

electron 文件及文件夹上传的问题探索

不晓得现在electron最新版的dialog解决了这个问题没有 选择文件夹的时候没有返回文件夹里面所有的文件 而是返回了文件夹的路径 同样的在网页端是可以通过webkitDirectory获取到文件夹下面所有的文件我试了很多的方式 下面的方式是可以成功上传文件夹下面所有的文件(这里单文件的上传不在叙述 只讨论文件夹的上传)本人业务需要 添加了一些你可能不用的参数 所以逻辑是这个 具体就看你自己的做法第一 通过dialog获取到文件夹的路径 定义为dirpath.第二 通过node的fs读取文件夹下的所有文件路径 代码大概如下:function fileDisplay(dirpath){ fs.readdir(dirpath,(err,files)=>{ if(err){console.log(err)}else{ files.forEach((filename)=>{ var filedir = path.join(paths,filename); fs.stat(filedir,(error,stats)=>{ if(error){}else{ var isFile = stats.isFile(); var isDir = stats.isDirectory(); if(ifFile){ fs.readFile(fileDir,(eror,data)=>{ if(err) throw err; filepack(data,filedir) }) } if(ifDir){ fileDisplay(fileDir) } } }) }) } })} 第三 生成文件function filepack(data,filepath){ let = filename = filepath.split(’\’)[filepath.split(’\).length-1]; let file = new File([data],filename,{type:‘image/’+filepath.split(’.’).reverse()[0]}); let fileData = new FormData(); fileData.append(‘file’,file); fileData.append(‘filename’,filename); let fileUploadData = fileData.getAll(‘file’)[0]; post(fileUploadData)}function post(file){ //提交 ajax.post()}第三步中的 new File那步中我直接用image是可以上传任意其他格式的文件的 目前没有发现会受这个的影响 有更好的方式的朋友可以给我说哈 再次说明 直接用代码肯定是不行的 因为去掉了很多中间的不重要的步骤 但是基本的三个是上面的几个上传的代码参考iview upload组件的源码 我是通过修改它的源码来改进的我是参考这个搞出来的:https://blog.csdn.net/Wbiokr/… ...

April 1, 2019 · 1 min · jiezi

前后分离文件上传

最近在做一个基于Flutter的app,算是学习新的移动端开发技术。于是就需要一个后端api接口,其中涉及到了文件上传,特此记录下1.为什么自己写文件上传本来我计划的是,后台只做数据接口,不做文件存储,毕竟自己也没那么多的服务器资源去存储。当时想的是用免费的第三方云存储解决方案,毕竟之前已经用过了七牛云。但是问题来了,免费的云存储,老是出问题。比如七牛的,过段时间就会发现,外链访问文件会失效,而且后台文件管理面板,察看文件也看不了,很坑。然后又看了有拍云,和腾讯云,它们提供的文件存储都不收费,但是要后收费,当你的文件存储超过容量时收费。这也还好,毕竟免费的,存储图片的话5g也够用了,但是主要是每天的流量由限制,超出流量收费。你后期不得面临你的应用中的图片全部不显示,作为定位上线的应用,是不能接受这种风险的。所以你如果自己玩,完全可以使用免费的云存储,要是真的商用,就考虑付费产品。但是我又穷,所以只能自己写了。2 后端开发(使用SpringBoot)就不贴全部的项目代码了,只贴上传部分的。因为使用了静态资源访问,所以需要加入模板引擎依赖。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>因为代码比较简单,所以直接controller全部处理完:package com.mike.controller;import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;import java.nio.file.StandardCopyOption;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import com.mike.bean.ApiResult;import com.mike.bean.FileView;import com.mike.util.ApiUtils;import com.mike.util.Tools;/** * The class UploadController */@RestController@RequestMapping("/up")public class UploadController { @Value("${baseUrl}") private String baaseUrl; @CrossOrigin @PostMapping("/upload") public ApiResult uploadFile(@RequestParam(“file”) MultipartFile file){ if (file.isEmpty()) { return ApiUtils.err(“你没有选择上传文件”); } else if(file.getOriginalFilename().contains("..")||!file.getOriginalFilename().contains(".")){ return ApiUtils.err(“文件格式有误”); } else { String fileName = file.getOriginalFilename(); //为防止文件名重复覆盖 fileName = fileName.replace(".", System.currentTimeMillis()+"."); Path savePath = Paths.get(“src/main/resources/static/upload”); try { Files.copy(file.getInputStream(), savePath.resolve(fileName), StandardCopyOption.REPLACE_EXISTING); FileView view = new FileView(); view.setName(fileName); view.setSize(file.getSize()); view.setUrl(baaseUrl+fileName); view.setUploadDate(Tools.getCurrent()); return ApiUtils.success(view); } catch (IOException e) { e.printStackTrace(); return ApiUtils.err(“对不起,上传失败”); } } }}在高并发下,还是有非常小的几率出现文件重名,所以使用时间戳也不是好的解决方案。需要能够生成唯一识别符号,建议使用UUId。3 前端测试(使用jquery ajax)<!DOCTYPE html><html> <head> <meta charset=“utf-8”> <title></title> </head> <body> <input type=“file” id=“file”> <button type=“button” onclick=“up()” name=“上传”>上传</button> <script src=“https://cdnjs.cloudflare.com/ajax/libs/zui/1.8.1/lib/jquery/jquery.js"></script> <script type=“text/javascript”> function up(){ var formData = new FormData(); formData.append(“file”, $("#file”)[0].files[0]); console.log(formData); $.ajax({ type:‘POST’, url:“http://localhost:8080/up/upload”, data:formData, contentType:false, processData:false,//这个很有必要,不然不行 dataType:“json”, mimeType:“multipart/form-data”, success:function(data){ if(“00”==data.code){ console.log(data.data); }else{ console.log(“error”); } } }); } </script> </body></html>效果:访问上传后的图片url:4 填坑因为图片上传导了代码目录,不是服务器的目录,所以,新上传得图片访问会报404,需要重起才能访问。为了解决这个问题,我们需要增加文件与路径的映射关系,这样,就不会出现404public class FileConfig implements WebMvcConfigurer { @Value("${serverFilePath}") private String serverFilePath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { WebMvcConfigurer.super.addResourceHandlers(registry); registry.addResourceHandler("/upload/**") .addResourceLocations(serverFilePath); }}serverFilePath 在properties中配置:baseUrl=http://localhost:8080/upload/serverFilePath=file:C:/code/mike-todo/src/main/resources/static/upload/上线的话,换成服务器上的路径就好。5 总结文件存储,远远不像我写的这么简单。只是应付一般小项目足够了,如果是大型的项目,就需要专门的文件存储系统以及服务器端的优化。如果你有什么问题,可以留言要论。或是关注我的公众号:mike啥都想搞,还有其他教学视频免费领取。 ...

March 19, 2019 · 1 min · jiezi

Express文件表单解析中间件 Multer简介

前言Express中最常使用的form解析中间件就是body-parser了,但是它明确表示不会支持multipart/form-data类型的表单.所以在body-parser官方文档中提供了如下的几个支持multipart/form-data类型的中间件的链接,或者只支持multipart/form-data解析的中间件链接.名称&地址周下载量starsbusboy426,2781448multipart240,921993formidable1,390,3614735multer284,9265860统计截止到2018年12月26日multer依赖busboy所以所以busboy的实际直接下载数量应该要减少28万????.什么是multipart/form-data类型的表单?最直观的解释就是支持上传文件的form表单,如果不使用JavaScript中创建的话,显式的html声明如下:<form action="/profile" method=“post” enctype=“multipart/form-data”> <input type=“file” name=“file”/> <input type=“submit” value=“submit”></form>上例子中的<input type=“file” name=“file” >在页面中的显示就是为一个按钮点击后可以进行文件选择.其次input可以添加multiple=“multiple"属性,这个时候打开的文件选择框会允许多选文件.总结一下有如下几种关系:一个name对应多个文件一个name对应一个文件多个name对应多个单个文件多个name对应多个文件顺便说一句multer有中文文档.正文特点multer只会解析form设置为enctype=“multipart/form-data"表单.multer可以定制存储引擎multer会将上传的信息以及内容挂载到request对象上request.body 保存文本内容request.file 保存单个文件信息以及对应内容(内存存储模式)request.files 保存多个文件信息以及对应的内容(内存存储模式)基本工作流程创建一个multer实例使用该实例上提供的不同方法获取不同功能的中间件放入到对应的路由中上传单个文件实例引入multer和Expressconst express = require(’express’), multer = require(‘multer’), app = express();传入配置参数const upload = multer({dest:’/uploads’});注意:dest参数指定了文件输出的位置,可以详细指定文件输出以及储存后面会将.使用multer中间件传递单个文件app.get(’/’,(request,response)=>{ console.log(‘get.request.body’,request.body); console.log(‘get.request.file’,request.file); console.log(‘get.request.files’,request.files); response.send(’<form action=”/” enctype=“multipart/form-data” method=“post”>’+ ‘<input type=“text” name=“title”><br>’+ ‘<input type=“file” name=“upload” multiple=“multiple”><br>’+ ‘<input type=“submit” value=“Upload”>’+ ‘</form>’)});app.post(’/’,upload.single(‘upload’),(request,response)=>{ console.log(‘post.request.body’,request.body); console.log(‘post.request.file’,request.file); console.log(‘post.request.files’,request.files); response.redirect(’/’);});app.listen(8888,()=>{ console.log(’express正在监听8888端口’);});这个例子中我们监听了根路径,分别处理两种不同的请求方式,针对get我们响应表单,针对post我们接受上传的内容.注意:upload.single(‘upload’)意思是高速multer只接收name是upload的单个文件.注意:这个例子中input是可以进行多选的,也就是说后端指定了文件数量为1但是页面依然上传了多个,这个时候multer会报错.注意:dest指定的路径为upload/会将文件保存到根路径下的upload文件夹中,对于windows系统来说是在运行这个应用对应的盘符下例如F:\uploads\这个例子中我填写了一个文本内容,同时上传了一个文件,输出结果如下:post.request.body { title: ‘hello world’ }post.request.file { fieldname: ‘upload’, originalname: ‘硬盘坏道扫描及修复工具Victoria.7z’, encoding: ‘7bit’, mimetype: ‘application/octet-stream’, destination: ‘/uploads’, filename: ‘6bdfc0df998d72e6232d60f790f47ef8’, path: ‘\uploads\6bdfc0df998d72e6232d60f790f47ef8’, size: 1033375 }上传多个文件实例const express = require(’express’), multer = require(‘multer’), app = express();const upload = multer({dest:’/uploads’});app.get(’/’,(request,response)=>{ console.log(‘get.request.body’,request.body); console.log(‘get.request.file’,request.file); console.log(‘get.request.files’,request.files); response.send(’<form action="/" enctype=“multipart/form-data” method=“post”>’+ ‘<input type=“text” name=“title”><br>’+ ‘<input type=“file” name=“upload”><br>’+ // 此处有两个相同name的input ‘<input type=“file” name=“upload”><br>’+ ‘<input type=“submit” value=“Upload”>’+ ‘</form>’)});app.post(’/’,upload.array(‘upload’),(request,response)=>{ // 注意此处使用的中间件和上例中不同 console.log(‘post.request.body’,request.body); console.log(‘post.request.file’,request.file); console.log(‘post.request.files’,request.files); response.redirect(’/’);});app.listen(8888,()=>{ console.log(’express正在监听8888端口’);});在这个例子的表单中有两个同名的name都是文件类型,这次使用array的方式来进行接受,控制台输出内容如下:post.request.body { title: ‘hello world’ }post.request.file undefinedpost.request.files [ { fieldname: ‘upload’, originalname: ‘硬盘坏道扫描及修复工具Victoria.7z’, encoding: ‘7bit’, mimetype: ‘application/octet-stream’, destination: ‘/uploads’, filename: ‘71ed2ac4299d43a30f5c13892f33e51b’, path: ‘\uploads\71ed2ac4299d43a30f5c13892f33e51b’, size: 1033375 }, { fieldname: ‘upload’, originalname: ‘新建文本文档.txt’, encoding: ‘7bit’, mimetype: ’text/plain’, destination: ‘/uploads’, filename: ‘190bde8fcdd08d57648ffb243607ed9d’, path: ‘\uploads\190bde8fcdd08d57648ffb243607ed9d’, size: 218 } ]在上面的例子中删除掉一个input,将剩余的input添加multiple属性用于多选,页面中在选择文件框中选择多个文件也是可以顺利通过.其余的方法除了上方提到的multer.single方法外还有其他的几种方法.array(name:string,maxcount?:number) 根据name限制上传文件的最大个数fields(fields:object) 自定义限制规则none() 只保留文本信息any() 允许任意类型通过,文件数组将保存在 req.files实际上它们都可以视为fields方法的包装,源码如下:Multer.prototype.single = function (name) { return this._makeMiddleware([{ name: name, maxCount: 1 }], ‘VALUE’)}Multer.prototype.array = function (name, maxCount) { return this._makeMiddleware([{ name: name, maxCount: maxCount }], ‘ARRAY’)}Multer.prototype.fields = function (fields) { return this._makeMiddleware(fields, ‘OBJECT’)}Multer.prototype.none = function () { return this._makeMiddleware([], ‘NONE’)}Multer.prototype.any = function () { function setup () { return { limits: this.limits, preservePath: this.preservePath, storage: this.storage, fileFilter: this.fileFilter, fileStrategy: ‘ARRAY’ } } return makeMiddleware(setup.bind(this))}常用API一览来自中文文档:multer(opts)Multer 接受一个 options 对象,其中最基本的是 dest 属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。为了避免命名冲突,Multer 会修改上传的文件名。这个重命名功能可以根据您的需要定制。以下是可以传递给 Multer 的选项。KeyDescriptiondest or storage在哪里存储文件fileFilter文件过滤器,控制哪些文件可以被接受limits限制上传的数据preservePath保存包含文件名的完整文件路径fileFilter设置一个函数来控制什么文件可以上传以及什么文件应该跳过,这个函数应该看起来像这样:function fileFilter (req, file, cb) { // 这个函数应该调用 cb 用boolean值来 // 指示是否应接受该文件 // 拒绝这个文件,使用false,像这样: cb(null, false) // 接受这个文件,使用true,像这样: cb(null, true) // 如果有问题,你可以总是这样发送一个错误: cb(new Error(‘I don't have a clue!’))}错误处理机制当遇到一个错误,multer 将会把错误发送给 express。你可以使用一个比较好的错误展示页 (express标准方式)。如果你想捕捉 multer 发出的错误,你可以自己调用中间件程序。如果你想捕捉 Multer 错误,你可以使用 multer 对象下的 MulterError 类 (即 err instanceof multer.MulterError)。var multer = require(‘multer’)var upload = multer().single(‘avatar’)app.post(’/profile’, function (req, res) { upload(req, res, function (err) { if (err instanceof multer.MulterError) { // 发生错误 } else if (err) { // 发生错误 } // 一切都好 })})磁盘存储引擎 (DiskStorage)磁盘存储引擎可以让你控制文件的存储。var storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, ‘/tmp/my-uploads’) }, filename: function (req, file, cb) { cb(null, file.fieldname + ‘-’ + Date.now()) }})var upload = multer({ storage: storage })有两个选项可用,destination 和 filename。他们都是用来确定文件存储位置的函数。destination 是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string (例如 ‘/tmp/uploads’)。如果没有设置 destination,则使用操作系统默认的临时文件夹。注意: 如果你提供的 destination 是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。filename 用于确定文件夹中的文件名的确定。 如果没有设置 filename,每个文件将设置为一个随机文件名,并且是没有扩展名的。注意: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。每个函数都传递了请求对象 (req) 和一些关于这个文件的信息 (file),有助于你的决定。注意 req.body 可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。内存存储引擎 (MemoryStorage)内存存储引擎将文件存储在内存中的 Buffer 对象,它没有任何选项。var storage = multer.memoryStorage()var upload = multer({ storage: storage })当使用内存存储引擎,文件信息将包含一个 buffer 字段,里面包含了整个文件数据。警告: 当你使用内存存储,上传非常大的文件,或者非常多的小文件,会导致你的应用程序内存溢出。filefilter和filename还有路由触发的顺序const express = require(’express’), multer = require(‘multer’), app = express();const storage = multer.diskStorage({ destination:__dirname, // 保存到当前目录 filename(request,file,callback){ console.log(‘filename:’,file); callback(null,’newfilename’);// 修改上传的文件名称 }});const upload = multer({ dest:’/uploads’, fileFilter(request,file,cb){ console.log(‘fileFilter:’,file); cb(null,true); }, limits:{ fileSize:100000 // 限制上传文件大小为100000字节 }, storage // 使用默认的储存器});最后触发的顺序为:filefilterfilename我们定义的路由 ...

December 26, 2018 · 2 min · jiezi