原文地址: luoliangDSGA's blog
博客地址: luoliangdsga.github.io
歡迎轉載,轉載請註明做者及出處,謝謝!javascript
以前寫過一篇SpringBoot+Vue先後端分離實現文件上傳的博客,可是那篇博客主要針對的是小文件的上傳,若是是大文件,一次性上傳,將會出現不可預期的錯誤。因此須要對大文件進行分塊,再依次上傳,這樣處理對於服務器容錯更好處理,更容易實現斷點續傳、跨瀏覽器上傳等功能。本文也會實現斷點,跨瀏覽器繼續上傳的功能。css
GIF效果預覽前端
須要準備好基礎環境vue
準備好這些以後,就能夠往下看了。java
新建一個SpringBoot項目,我這裏使用的是SpringBoot2,引入mvc,jpa,mysql相關的依賴。node
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</dependency>
</dependencies>
複製代碼
在yml中配置mvc以及數據庫鏈接等屬性mysql
server:
port: 8081
servlet:
path: /boot
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
datasource:
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jpa:
properties:
hibernate:
hbm2ddl:
auto: create-drop
show-sql: true
logging:
level:
org.boot.uploader.*: debug
prop:
upload-folder: files
複製代碼
定義文件上傳相關的類,一個是FileInfo,表明文件的基礎信息;一個是Chunk,表明文件塊。webpack
FileInfo.javaios
@Data
@Entity
public class FileInfo implements Serializable {
@Id
@GeneratedValue
private Long id;
@Column(nullable = false)
private String filename;
@Column(nullable = false)
private String identifier;
@Column(nullable = false)
private Long totalSize;
@Column(nullable = false)
private String type;
@Column(nullable = false)
private String location;
}
複製代碼
Chunk.javagit
@Data
@Entity
public class Chunk implements Serializable {
@Id
@GeneratedValue
private Long id;
/** * 當前文件塊,從1開始 */
@Column(nullable = false)
private Integer chunkNumber;
/** * 分塊大小 */
@Column(nullable = false)
private Long chunkSize;
/** * 當前分塊大小 */
@Column(nullable = false)
private Long currentChunkSize;
/** * 總大小 */
@Column(nullable = false)
private Long totalSize;
/** * 文件標識 */
@Column(nullable = false)
private String identifier;
/** * 文件名 */
@Column(nullable = false)
private String filename;
/** * 相對路徑 */
@Column(nullable = false)
private String relativePath;
/** * 總塊數 */
@Column(nullable = false)
private Integer totalChunks;
/** * 文件類型 */
@Column
private String type;
@Transient
private MultipartFile file;
}
複製代碼
編寫文件塊相關的業務操做
@Service
public class ChunkServiceImpl implements ChunkService {
@Resource
private ChunkRepository chunkRepository;
@Override
public void saveChunk(Chunk chunk) {
chunkRepository.save(chunk);
}
@Override
public boolean checkChunk(String identifier, Integer chunkNumber) {
Specification<Chunk> specification = (Specification<Chunk>) (root, criteriaQuery, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
predicates.add(criteriaBuilder.equal(root.get("identifier"), identifier));
predicates.add(criteriaBuilder.equal(root.get("chunkNumber"), chunkNumber));
return criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
};
return chunkRepository.findOne(specification).orElse(null) == null;
}
}
複製代碼
接下來就是編寫最重要的controller了
@RestController
@RequestMapping("/uploader")
@Slf4j
public class UploadController {
@Value("${prop.upload-folder}")
private String uploadFolder;
@Resource
private FileInfoService fileInfoService;
@Resource
private ChunkService chunkService;
@PostMapping("/chunk")
public String uploadChunk(Chunk chunk) {
MultipartFile file = chunk.getFile();
log.debug("file originName: {}, chunkNumber: {}", file.getOriginalFilename(), chunk.getChunkNumber());
try {
byte[] bytes = file.getBytes();
Path path = Paths.get(generatePath(uploadFolder, chunk));
//文件寫入指定路徑
Files.write(path, bytes);
log.debug("文件 {} 寫入成功, uuid:{}", chunk.getFilename(), chunk.getIdentifier());
chunkService.saveChunk(chunk);
return "文件上傳成功";
} catch (IOException e) {
e.printStackTrace();
return "後端異常...";
}
}
@GetMapping("/chunk")
public Object checkChunk(Chunk chunk, HttpServletResponse response) {
if (chunkService.checkChunk(chunk.getIdentifier(), chunk.getChunkNumber())) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
return chunk;
}
@PostMapping("/mergeFile")
public String mergeFile(FileInfo fileInfo) {
String path = uploadFolder + "/" + fileInfo.getIdentifier() + "/" + fileInfo.getFilename();
String folder = uploadFolder + "/" + fileInfo.getIdentifier();
merge(path, folder);
fileInfo.setLocation(path);
fileInfoService.addFileInfo(fileInfo);
return "合併成功";
}
}
複製代碼
public static void merge(String targetFile, String folder) {
try {
Files.createFile(Paths.get(targetFile));
Files.list(Paths.get(folder))
.filter(path -> path.getFileName().toString().contains("-"))
.sorted((o1, o2) -> {
String p1 = o1.getFileName().toString();
String p2 = o2.getFileName().toString();
int i1 = p1.lastIndexOf("-");
int i2 = p2.lastIndexOf("-");
return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
})
.forEach(path -> {
try {
//以追加的形式寫入文件
Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
//合併後刪除該塊
Files.delete(path);
} catch (IOException e) {
e.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
到這裏,後端主要的邏輯已經寫完了,下面開始編寫前端的部分。
前端我直接clone了vue-uploader,在這個代碼的基礎上進行了修改。
App.vue
<template>
<uploader :options="options" :file-status-text="statusText" class="uploader-example" ref="uploader"
@file-complete="fileComplete" @complete="complete"></uploader>
</template>
<script>
import axios from 'axios'
import qs from 'qs'
export default {
data() {
return {
options: {
target: '/boot/uploader/chunk',
testChunks: true,
simultaneousUploads: 1,
chunkSize: 10 * 1024 * 1024
},
attrs: {
accept: 'image/*'
},
statusText: {
success: '成功了',
error: '出錯了',
uploading: '上傳中',
paused: '暫停中',
waiting: '等待中'
}
}
},
methods: {
// 上傳完成
complete() {
console.log('complete', arguments)
},
// 一個根文件(文件夾)成功上傳完成。
fileComplete() {
console.log('file complete', arguments)
const file = arguments[0].file;
axios.post('/boot/uploader/mergeFile', qs.stringify({
filename: file.name,
identifier: arguments[0].uniqueIdentifier,
totalSize: file.size,
type: file.type
})).then(function (response) {
console.log(response);
}).catch(function (error) {
console.log(error);
});
}
},
mounted() {
this.$nextTick(() => {
window.uploader = this.$refs.uploader.uploader
})
}
}
</script>
...
複製代碼
配置說明:
更多說明請直接參考vue-uploader
解決跨域問題
這裏使用了http-proxy-middleware這個node中間件,能夠對前端的請求進行轉發,轉發到指定的路由。
dev: {
env: require('./dev.env'),
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: '',
assetsPublicPath: '/',
proxyTable: {
'/boot': {
target: 'http://localhost:8081',
changeOrigin: true //若是跨域,則須要配置此項
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
複製代碼
proxyTable表示代理配置表,將特定的請求代理到指定的API接口,這裏是將'localhost:8080/boot/xxx'代理到'http://localhost:8081/boot/xxx'。
如今能夠開始驗證了,分別啓動先後端的項目
npm install
npm run dev
複製代碼
運行效果就像最開始的那張圖,能夠同時上傳多個文件,上傳暫停以後更換瀏覽器,選擇同一個文件能夠實現繼續上傳的效果,你們能夠自行進行嘗試,代碼會在個人GitHub上進行更新。
整篇文章到這裏差很少就結束了,這個項目能夠做爲demo用來學習,有不少能夠擴展的地方,確定也會有不完善的地方,有更好的方法也但願能指出,共同交流學習。