SpringBoot+Vue.js先後端分離實現大文件分塊上傳

原文地址: luoliangDSGA's blog
博客地址: luoliangdsga.github.io
歡迎轉載,轉載請註明做者及出處,謝謝!javascript

SpringBoot+Vue.js先後端分離實現大文件分塊上傳

以前寫過一篇SpringBoot+Vue先後端分離實現文件上傳的博客,可是那篇博客主要針對的是小文件的上傳,若是是大文件,一次性上傳,將會出現不可預期的錯誤。因此須要對大文件進行分塊,再依次上傳,這樣處理對於服務器容錯更好處理,更容易實現斷點續傳、跨瀏覽器上傳等功能。本文也會實現斷點,跨瀏覽器繼續上傳的功能。css

開始

GIF效果預覽前端

此處用到了 這位大佬的Vue上傳組件,此圖也是引用自他的GitHub,感謝這位大佬。

須要準備好基礎環境vue

  • Java
  • Node
  • MySQL

準備好這些以後,就能夠往下看了。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;
    }

}
複製代碼
  1. checkChunk()方法會根據文件惟一標識,和當前塊數判斷是否已經上傳過這個塊。
  2. 這裏只貼了ChunkService的代碼,其餘的代碼只是jpa簡單的存取。

接下來就是編寫最重要的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 "合併成功";
    }
}
複製代碼
  1. 文章開頭就提到了先後端分離,既然是先後端分離,確定會涉及到跨域問題,在上一篇文章中是經過springMVC的@CrossOrigin註解來解決跨域問題,這裏並無使用這個註解,在下面的前端項目中會使用一個node的中間件來作代理,解決跨域的問題。
  2. 能夠看到有兩個/chunk路由,第一個是post方法,用於上傳並存儲文件塊,須要對文件塊名進行編號,再存儲在指定路徑下;第二個是get方法,前端上傳以前會先進行檢測,若是此文件塊已經上傳過,就能夠實現斷點和快傳。
  3. /mergeFile用於合併文件,在全部塊上傳完畢後,前端會調用此接口進行制定文件的合併。其中的merge方法是會遍歷指定路徑下的文件塊,而且按照文件名中的數字進行排序後,再合併成一個文件,不然合併後的文件會沒法使用,代碼以下:
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>
...
複製代碼

配置說明:

  1. target 目標上傳 URL,能夠是字符串也能夠是函數,若是是函數的話,則會傳入 Uploader.File 實例、當前塊 Uploader.Chunk 以及是不是測試模式,默認值爲 '/'。
  2. chunkSize 分塊時按照該值來分。最後一個上傳塊的大小是多是大於等於1倍的這個值可是小於兩倍的這個值大小,默認 110241024。
  3. testChunks 是否測試每一個塊是否在服務端已經上傳了,主要用來實現秒傳、跨瀏覽器上傳等,默認true。
  4. simultaneousUploads 併發上傳數,默認3。

更多說明請直接參考vue-uploader

解決跨域問題

這裏使用了http-proxy-middleware這個node中間件,能夠對前端的請求進行轉發,轉發到指定的路由。

在index.js中進行配置,以下:

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
複製代碼
  • 後端 能夠經過command line,也能夠直接運行BootUploaderApplication的main()方法

運行效果就像最開始的那張圖,能夠同時上傳多個文件,上傳暫停以後更換瀏覽器,選擇同一個文件能夠實現繼續上傳的效果,你們能夠自行進行嘗試,代碼會在個人GitHub上進行更新。

最後

整篇文章到這裏差很少就結束了,這個項目能夠做爲demo用來學習,有不少能夠擴展的地方,確定也會有不完善的地方,有更好的方法也但願能指出,共同交流學習。

相關文章
相關標籤/搜索