前言
在现代Web应用的开发过程中,实现文件的在线预览功能是一项关键需求,极大地提升了用户体验。该功能允许用户直接在浏览器界面内浏览文件内容,省去了传统上需要先将文件下载到本地再打开的步骤。本文旨在详尽阐述如何在基于SpringBoot的项目中集成kkFileView这一强大工具,从而轻松实现多种类型文件的在线预览功能。
kFileView作为一款开源的文件在线预览解决方案,专注于为Web应用提供无缝的文件预览体验。该工具建立在Spring Boot框架之上,广泛支持多种文件格式的直接预览,使得用户能够在浏览器中即时查看文件内容,无需进行繁琐的下载步骤。这种设计极大地提升了用户交互的便捷性和效率。
运行效果:
二、kkFileView 安装
在开始 SpringBoot 项目之前,我们首先需要在服务器上安装 kkFileView。以下是详细的安装步骤:
下载 kkFileView 安装包
wget https://kkfileview.keking.cn/kkFileView-4.0.0.tar.gz
2.解压安装包
tar -zxvf kkFileView-4.0.0.tar.gz
3.修改配置文件
cd kkFileView-4.0.0/config
vi application.properties
4.启动 kkFileView
cd kkFileView-4.0.0/bin
sh startup.sh
启动后,可以通过以下命令查看日志,确认是否启动成功:
sh showlog.sh
项目结构
├── src
│ ├── main
│ │ ├── java
│ │ │ └── com.icoderoad.filepreview
│ │ │ ├── controller
│ │ │ │ └── FileController.java
│ │ │ ├── config
│ │ │ │ └── WebConfig.java
│ │ │ ├── service
│ │ │ │ └── FileService.java
│ │ │ └── FilepreviewApplication.java
│ │ ├── resources
│ │ │ ├── templates
│ │ │ │ └── upload.html
│ │ │ │ └── preview.html
│ │ │ └── application.yml
│ └── pom.xml
引入依赖
<dependencies>
<!-- Spring Boot 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf 模板引擎 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 其他依赖 -->
</dependencies>
配置文件
server:
port: 8080
file-preview:
file-save-dir: /Users/icoderoad/code/java/filepreview/src/main/resources/uploadfile/ # 本地文件保存路径
base-url: http://localhost:8080/files # 文件的 HTTP 访问路径
后端代码实现
通过自定义 WebMvcConfigurer
来手动映射静态资源路径:
package com.icoderoad.filepreview.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 从配置文件中读取静态资源路径
@Value("${file-preview.file-save-dir}")
private String fileStoragePath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 映射资源路径
registry.addResourceHandler("/files/**")
.addResourceLocations("file:" + fileStoragePath);
}
}
这样就可以通过 http://localhost:8080/files/example.txt
来访问文件了。
主应用程序类 (FilepreviewApplication.java
):
package com.icoderoad.filepreview;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class FilepreviewApplication {
public static void main(String[] args) {
SpringApplication.run(FilepreviewApplication.class, args);
}
}
文件服务类 (FileService.java
):
package com.icoderoad.filepreview.service;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class FileService {
@Value("${file-preview.file-save-dir}")
private String fileSaveDir;
@Value("${file-preview.base-url}")
private String baseUrl;
public String saveFile(MultipartFile file) throws IOException {
// 创建文件保存路径
Path path = Paths.get(fileSaveDir, file.getOriginalFilename());
Files.write(path, file.getBytes());
// 生成文件的 HTTP URL
return baseUrl + "/" + file.getOriginalFilename();
}
}
文件预览控制器 (FileController.java
):
package com.icoderoad.filepreview.controller;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.icoderoad.filepreview.service.FileService;
@Controller
public class FileController {
@Autowired
private FileService fileService;
@GetMapping("/")
public String index(Model model) {
return "upload";
}
@PostMapping("/upload")
public ResponseEntity<Map<String, String>> handleFileUpload(@RequestParam("file") MultipartFile file) {
Map<String, String> response = new HashMap<>();
try {
if (file.isEmpty()) {
throw new IllegalArgumentException("上传的文件为空");
}
// 保存文件并获取 HTTP 访问路径
String fileUrl = fileService.saveFile(file);
response.put("fileUrl", fileUrl);
return ResponseEntity.ok(response);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", "文件上传失败"));
}
}
@GetMapping("/preview")
public String previewFile(@RequestParam("filePath") String filePath, Model model) {
// 将文件路径传递给前端页面
model.addAttribute("filePath", filePath);
return "preview";
}
}
前端页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文件上传</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
padding-top: 20px;
}
.container {
max-width: 900px;
}
.upload-box {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.progress {
margin-top: 10px;
}
.alert {
display: none; /* 默认隐藏提示信息 */
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">文件上传</h1>
<div class="upload-box">
<form id="uploadForm" enctype="multipart/form-data">
<div class="form-group">
<label for="fileInput">选择文件</label>
<input type="file" class="form-control-file" id="fileInput" name="file">
</div>
<button type="submit" class="btn btn-primary">上传</button>
</form>
<div class="progress mt-3">
<div id="progressBar" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div id="uploadStatus" class="alert alert-success mt-3"></div>
<div id="uploadError" class="alert alert-danger mt-3"></div>
<div id="previewSection" class="mt-3" style="display: none;">
<button id="previewButton" class="btn btn-success">查看预览</button>
</div>
</div>
</div>
</div>
</div>
<!-- Bootstrap JS and dependencies -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.5.2/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<script>
$(document).ready(function() {
$('#uploadForm').on('submit', function(event) {
event.preventDefault(); // 防止默认提交
var formData = new FormData(this);
$.ajax({
url: '/upload', // 后端处理上传的接口
type: 'POST',
data: formData,
contentType: false,
processData: false,
xhr: function() {
var xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', function(e) {
if (e.lengthComputable) {
var percentComplete = Math.round((e.loaded / e.total) * 100);
$('#progressBar').css('width', percentComplete + '%');
$('#progressBar').attr('aria-valuenow', percentComplete);
}
}, false);
return xhr;
},
success: function(response) {
// 假设响应中包含文件的 URL
$('#uploadStatus').text('文件上传成功!').show();
$('#uploadError').hide();
$('#progressBar').css('width', '100%');
// 显示预览按钮
var fileUrl = response.fileUrl; // 从响应中获取文件 URL
console.log("fileUrl:", fileUrl);
$('#previewButton').data('file-url', fileUrl);
$('#previewSection').show();
},
error: function(xhr) {
$('#uploadError').text('文件上传失败!').show();
$('#uploadStatus').hide();
}
});
// 预览按钮点击事件
$('#previewButton').on('click', function() {
var fileUrl = $(this).data('file-url');
console.log("fileUrl:", fileUrl);
if (fileUrl) {
var previewUrl = 'http://localhost:8080/preview?filePath=' + encodeURIComponent(fileUrl);
window.open(previewUrl, '_blank');
}
});
});
});
</script>
</body>
</html>
文件预览页面 (preview.html
):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>File Preview</title>
<!-- Bootstrap CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<script type="text/javascript">
function base64Encode(str) {
return btoa(unescape(encodeURIComponent(str)));
}
</script>
<style>
body {
padding-top: 20px;
}
.container {
max-width: 900px;
}
.preview-box {
border: 1px solid #ddd;
padding: 20px;
border-radius: 5px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.iframe-container {
position: relative;
padding-top: 56.25%; /* 16:9 aspect ratio */
height: 0;
overflow: hidden;
}
.iframe-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<h1 class="text-center">文件在线预览</h1>
<div class="preview-box">
<div class="iframe-container">
<!-- 使用 iframe 显示文件预览 -->
<script th:inline="javascript">
// 获取服务器传递的文件 HTTP 访问路径
var filePath = /*[[${filePath}]]*/ 'default.txt';
// 对文件路径进行 Base64 编码并进行 URL 编码
var encodedUrl =encodeURIComponent(base64Encode(filePath));
// 生成预览的 URL
var previewUrl = 'http://localhost:8012/onlinePreview?url=' + encodedUrl;
// 动态插入 iframe 用于文件预览
document.write('<iframe lay-src="' + previewUrl + '" width="100%" height="600px"></iframe>');
</script>
</div>
</div>
</div>
</div>
</div>
</body>
</html>