经常在项目中需要使用上传文件功能,找了不少前端上传组件,都不是很好用,今天尝试了一下DropZone,发现不错,顺便记录一下使用过程,方便后续查阅。在做开发的时候,经常需要调研一些技术,因此前后端都需要用到,为方便开发,这里采用传统的开发方式,没有做前后端分离,方便调试。前端采用HTML+Bootstrap+jQuery,后端采用SpringBoot2.6.3。
总体
新建一个SpringBoot程序,目录结构如下:
files:存放程序运行过程中的生成文件
logs:日志目录
src:源代码目录
uploads:上传文件目录
前端第三方文件放在/src/main/resources/static目录下,主要有bootstrap、dropzone和jquery,模版文件放在/src/main/resources/templates目录下,在这里注意调试时,经常需要修改HTML页面,需要快速将HTML页面编译到到运行目录/target/classes,这里使用快捷键CTRL+F9即可,前提是IDEA的自动构建项目要勾上
前端
首先从DropZone下载相关的js文件和css文件,或者直接让一些大模型给一个示例,我这里采用腾讯元宝,稍微修改一下,基本可以用,但是不少地方还是需要自己定制。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>File Upload</title><link rel="stylesheet" href="./bootstrap/css/bootstrap.min.css"><script src="./bootstrap/js/bootstrap.min.js"></script><link rel="stylesheet" href="./dropzone/dropzone.min.css"><script src="./dropzone/dropzone.min.js"></script><script src="./jquery/jquery.min.js"></script><style>.dropzone { padding: 3px 0 0 0; margin: 0 0 8px 0; text-align: center; overflow: hidden; width: 120px;min-height: 30px; height: 36px; border: #ccc solid 1px; }.dz-processing, .dz-button { display: none; }#fileList { display: flex; flex-wrap: wrap; gap: 10px; }.file-item { background-color: #f9f9f9; padding: 5px; border: 1px solid #ccc; border-radius: 0; cursor: pointer; }</style>
</head>
<body><div class="container"><div style="padding:10px 0;">参看:https://www.dropzone.dev/</div><div class="dropzone primary" id="my-dropzone">上传文件</div><div id="message"></div><div id="fileList"></div></div><script>Dropzone.options.myDropzone = {url: '/upload',paramName: 'file',maxFiles:10,//一次性上传的文件数量上限maxFilesize: 20, //MBacceptedFiles: ".jpg,.gif,.png", //上传的类型parallelUploads: 3,dictMaxFilesExceeded: "您最多只能上传10个文件!",dictResponseError: '文件上传失败!',dictInvalidFileType: "你不能上传该类型文件,文件类型只能是*.jpg,*.gif,*.png。",dictFallbackMessage:"浏览器不受支持",dictFileTooBig:"文件过大上传文件最大支持.",previewTemplate: '<div></div>',showPreviewOnDrop: false,showPreviewOnUpload: false,init: function() {this.on('success', function(file, response) {$('#message').html('<p class="text-success">' + response.msg + '</p>');let json = JSON.parse(file.xhr.response);let fileName = '<div class="file-item" οnclick="showImage(\'' + json.data + '\')">' + json.data + '</div>';$('#fileList').prepend(fileName);});this.on('error', function(file, response) {$('#message').html('<p class="text-danger">Failed to upload file.</p>');});// 自定义文件显示方式this.on('addedfile', function(file) {console.log(file);});this.on('removedfile', function(file) {console.log(file);$('#fileList .file-item[data-dz-id="' + file.id + '"]').remove();});}};function showImage(url) {window.open('image/' + url, '_blank')}$(document).ready(function() {$.get('/images', function(res) {let array = res.data;let count = array.length;for (let i = 0; i < count; i ++) {let item = '<div class="file-item" οnclick="showImage(\'' + array[i] + '\')">' + array[i] + '</div>';$('#fileList').append(item);}})});
</script>
</body>
</html>
后端
后端主要实现三个接口,上传接口、获取图片列表接口、显示单个图片接口,分别对应/upload,/images,/image/{filename}
package org.example.imgtool.controller;import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.example.imgtool.ImgtoolApplication;
import org.example.imgtool.utils.FileUtil;
import org.example.imgtool.utils.PathUtil;
import org.example.imgtool.utils.SnowflakeGenerator;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.List;@Slf4j
@RestController
public class FileUploadController {private static final String UPLOAD_DIR = "uploads/";@GetMapping("/images")public ResponseEntity<JSONObject> getImages() {String imagesPath = PathUtil.getAppPath(ImgtoolApplication.class) + "files/images.txt";List<String> list = FileUtil.readFileToList(imagesPath);JSONObject json = new JSONObject(true);json.put("code", HttpStatus.OK.value());json.put("data", list);json.put("msg", "获取数据成功");return new ResponseEntity<>(json, HttpStatus.OK);}@GetMapping(value = "/image/{filename}", produces = MediaType.IMAGE_JPEG_VALUE)public ResponseEntity<byte[]> getRemoteImage(@PathVariable String filename) throws IOException {String imagePath = PathUtil.getAppPath(ImgtoolApplication.class) + "uploads/" + filename;byte[] bytes = FileUtil.getFileByteArray(imagePath);HttpHeaders headers = new HttpHeaders();headers.setAccept(Collections.singletonList(MediaType.IMAGE_JPEG));File file = new File(imagePath);if (file.exists()) {return new ResponseEntity<>(bytes, headers, HttpStatus.OK);} else {return ResponseEntity.status(HttpStatus.NOT_FOUND).build();}}@PostMapping("/upload")public ResponseEntity<JSONObject> uploadFile(@RequestParam("file") MultipartFile file) {try {// 检查上传目录是否存在,如果不存在则创建Path uploadPath = Paths.get(UPLOAD_DIR);if (!Files.exists(uploadPath)) {Files.createDirectories(uploadPath);}// 保存文件到上传目录String fileName = file.getOriginalFilename();int index = fileName.lastIndexOf(".");String extension = fileName.substring(index);String newFileName = String.format("%d%s", SnowflakeGenerator.generatorId(), extension);log.debug(newFileName);Path filePath = uploadPath.resolve(newFileName);Files.copy(file.getInputStream(), filePath);// 文件名写入文件String imagesPath = PathUtil.getAppPath(ImgtoolApplication.class) + "files/images.txt";File f = new File(imagesPath);List<String> list = null;if (f.exists()) {list = FileUtil.readFileToList(imagesPath);} else {list = Collections.emptyList();}list.add(0, newFileName);FileUtil.writeFile(imagesPath, list);JSONObject json = new JSONObject(true);json.put("code", HttpStatus.OK.value());json.put("data", newFileName);json.put("msg", "上传成功" + newFileName);return new ResponseEntity<>(json, HttpStatus.OK);} catch (IOException e) {log.error("Upload image fail : {}", e.getMessage());JSONObject json = new JSONObject(true);json.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());json.put("msg", "上传失败");return new ResponseEntity<>(json, HttpStatus.INTERNAL_SERVER_ERROR);}}
}
配置文件如下,注意这里spring.thymeleaf.cache一定要配置为false,这样前面提到的CTRL+F9就可以实时调试,方便前端调试代码。
spring.thymeleaf.cache=false
spring.thymeleaf.check-template=true
spring.thymeleaf.check-template-location=true
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.excluded-view-names=
spring.thymeleaf.mode=HTML5
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
server.port=8080
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB
效果
最后实现效果如下
源代码
转到https://download.csdn.net/download/Angushine/89672489下载即可。