难度系数:⭐⭐⭐⭐
实用指数:💯💯💯💯💯
📖 开篇:一次糟糕的文件查看体验
小明在OA系统查看领导批复的文件:
点击"查看文件" → 自动下载到本地 → 找到下载文件
→ 双击打开 → "您没有安装Office" → 下载WPS
→ 安装WPS → 再次打开文件 → 终于看到了...
小明:
"查看个文件要5分钟?现在都2024年了!😭"
同一天,小红在石墨文档查看文件:
点击"预览" → 立即在浏览器打开 → 完美显示 ✨
小红:
"这才是现代化的体验!😊"
产品经理找到你:
"小王,我们也要做文件预览功能!要支持所有格式!"
你:
"所有格式?Word、Excel、PPT、PDF、图片、视频、音频、CAD图纸、PSD... 😱"
产品经理:
"对啊,有问题吗?🙄"
今天,我们就来实现一个强大的文件预览系统! 🎯
🎯 文件预览的应用场景
| 场景 | 文件类型 | 预览需求 |
|---|---|---|
| OA系统 📋 | Word/Excel/PPT/PDF | 审批文档预览 |
| 网盘系统 📦 | 图片/视频/音频/文档 | 在线查看 |
| 合同系统 📝 | PDF/扫描件 | 合同预览 |
| 设计系统 🎨 | PSD/AI/Sketch | 设计稿预览 |
| 开发平台 💻 | 代码/Markdown | 代码高亮预览 |
| 电商平台 🛒 | 商品图片 | 大图预览 |
🎨 文件预览方案全景图
┌──────────────────────────────────────────────────────────┐
│ 文件预览解决方案 │
└──────────────────────────────────────────────────────────┘
方案1: 浏览器原生支持
- 图片(JPG/PNG/GIF/WebP)
- PDF(Chrome/Edge内置)
- 视频(MP4/WebM)
- 音频(MP3/WAV)
✅ 无需转换,速度快
方案2: 前端库渲染
- Office文档 → office-viewer
- PDF → pdf.js
- Markdown → marked.js
- 代码 → highlight.js
✅ 无需后端支持
方案3: 格式转换
- Office → PDF/HTML
- CAD → 图片
- PSD → 图片
✅ 支持复杂格式
方案4: 第三方服务
- 微软Office Online
- Google Docs Viewer
- 永中Office
⚠️ 有限制,收费
方案5: 自建预览服务
- LibreOffice
- OpenOffice
- unoconv
✅ 完全可控
🖼️ 方案一:图片预览
1. 基础图片预览
<!DOCTYPE html>
<html>
<head>
<title>图片预览</title>
<style>
.image-preview {
max-width: 100%;
max-height: 600px;
cursor: zoom-in;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 9999;
}
.modal img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 90%;
max-height: 90%;
}
.close-btn {
position: absolute;
top: 20px;
right: 40px;
color: white;
font-size: 40px;
cursor: pointer;
}
</style>
</head>
<body>
<img src="/api/file/view/1" class="image-preview" onclick="openModal(this.src)">
<div id="modal" class="modal" onclick="closeModal()">
<span class="close-btn">×</span>
<img id="modal-img" src="">
</div>
<script>
function openModal(src) {
document.getElementById('modal').style.display = 'block';
document.getElementById('modal-img').src = src;
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
</script>
</body>
</html>
2. 高级图片预览(Viewer.js)
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/viewerjs@1.11.0/dist/viewer.min.css">
<script src="https://cdn.jsdelivr.net/npm/viewerjs@1.11.0/dist/viewer.min.js"></script>
</head>
<body>
<div id="images">
<img src="image1.jpg" alt="图片1">
<img src="image2.jpg" alt="图片2">
<img src="image3.jpg" alt="图片3">
</div>
<script>
const viewer = new Viewer(document.getElementById('images'), {
toolbar: true, // 显示工具栏
navbar: true, // 显示缩略图
title: true, // 显示标题
tooltip: true, // 显示缩放比例
movable: true, // 可拖动
zoomable: true, // 可缩放
rotatable: true, // 可旋转
scalable: true, // 可翻转
transition: true, // 过渡动画
fullscreen: true, // 全屏
keyboard: true // 键盘控制
});
</script>
</body>
</html>
3. 后端接口
@RestController
@RequestMapping("/api/file")
public class FileViewController {
@Autowired
private FileService fileService;
/**
* 图片预览
*/
@GetMapping("/view/{fileId}")
public void viewImage(@PathVariable Long fileId,
HttpServletResponse response) throws IOException {
// 1. 获取文件信息
FileInfo fileInfo = fileService.getById(fileId);
if (fileInfo == null) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
// 2. 检查权限
if (!hasPermission(fileId)) {
response.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
// 3. 读取文件
File file = new File(fileInfo.getFilePath());
// 4. 设置响应头
response.setContentType(fileInfo.getContentType());
response.setHeader("Content-Disposition", "inline; filename=" +
URLEncoder.encode(fileInfo.getFileName(), "UTF-8"));
// 5. 输出文件
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(fis, os);
os.flush();
}
}
/**
* 缩略图(压缩)
*/
@GetMapping("/thumbnail/{fileId}")
public void thumbnail(@PathVariable Long fileId,
@RequestParam(defaultValue = "200") int width,
@RequestParam(defaultValue = "200") int height,
HttpServletResponse response) throws IOException {
FileInfo fileInfo = fileService.getById(fileId);
File file = new File(fileInfo.getFilePath());
// 使用Thumbnailator压缩图片
response.setContentType("image/jpeg");
Thumbnails.of(file)
.size(width, height)
.outputFormat("jpg")
.toOutputStream(response.getOutputStream());
}
}
📄 方案二:PDF预览
1. Chrome内置预览
<!-- 最简单的方式:直接用iframe -->
<iframe src="/api/file/view/1.pdf"
width="100%"
height="600px">
</iframe>
<!-- 或者用embed -->
<embed src="/api/file/view/1.pdf"
type="application/pdf"
width="100%"
height="600px">
2. PDF.js预览(推荐⭐⭐⭐⭐⭐)
<!DOCTYPE html>
<html>
<head>
<title>PDF预览</title>
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.min.js"></script>
<style>
#pdf-container {
text-align: center;
}
canvas {
border: 1px solid #ccc;
margin: 10px auto;
display: block;
}
.controls {
margin: 20px;
text-align: center;
}
button {
padding: 10px 20px;
margin: 0 5px;
font-size: 16px;
}
</style>
</head>
<body>
<div class="controls">
<button onclick="prevPage()">上一页</button>
<span>第 <span id="page-num"></span> 页 / 共 <span id="page-count"></span> 页</span>
<button onclick="nextPage()">下一页</button>
<button onclick="zoomIn()">放大</button>
<button onclick="zoomOut()">缩小</button>
</div>
<div id="pdf-container"></div>
<script>
// 设置PDF.js的workerSrc
pdfjsLib.GlobalWorkerOptions.workerSrc =
'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
let pdfDoc = null;
let pageNum = 1;
let pageRendering = false;
let pageNumPending = null;
let scale = 1.5; // 缩放比例
/**
* 加载PDF
*/
async function loadPDF(url) {
try {
pdfDoc = await pdfjsLib.getDocument(url).promise;
document.getElementById('page-count').textContent = pdfDoc.numPages;
// 渲染第一页
renderPage(pageNum);
} catch (error) {
console.error('PDF加载失败:', error);
}
}
/**
* 渲染指定页
*/
async function renderPage(num) {
pageRendering = true;
try {
// 获取页面
const page = await pdfDoc.getPage(num);
// 创建canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 计算尺寸
const viewport = page.getViewport({ scale: scale });
canvas.width = viewport.width;
canvas.height = viewport.height;
// 渲染
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
await page.render(renderContext).promise;
// 显示
const container = document.getElementById('pdf-container');
container.innerHTML = '';
container.appendChild(canvas);
// 更新页码
document.getElementById('page-num').textContent = num;
pageRendering = false;
// 如果有待渲染的页面
if (pageNumPending !== null) {
renderPage(pageNumPending);
pageNumPending = null;
}
} catch (error) {
console.error('页面渲染失败:', error);
pageRendering = false;
}
}
/**
* 上一页
*/
function prevPage() {
if (pageNum <= 1) return;
pageNum--;
queueRenderPage(pageNum);
}
/**
* 下一页
*/
function nextPage() {
if (pageNum >= pdfDoc.numPages) return;
pageNum++;
queueRenderPage(pageNum);
}
/**
* 队列渲染
*/
function queueRenderPage(num) {
if (pageRendering) {
pageNumPending = num;
} else {
renderPage(num);
}
}
/**
* 放大
*/
function zoomIn() {
scale += 0.25;
renderPage(pageNum);
}
/**
* 缩小
*/
function zoomOut() {
if (scale <= 0.5) return;
scale -= 0.25;
renderPage(pageNum);
}
// 加载PDF文件
loadPDF('/api/file/view/1');
</script>
</body>
</html>
📊 方案三:Office文档预览
1. Office转PDF(推荐方案)
LibreOffice转换
@Service
public class OfficeConvertService {
@Value("${libreoffice.home}")
private String libreOfficeHome;
/**
* Office文档转PDF
*/
public File convertToPdf(File sourceFile) throws IOException, InterruptedException {
String outputDir = sourceFile.getParent();
// 构建命令
String[] cmd = {
libreOfficeHome + "/program/soffice",
"--headless", // 无界面模式
"--convert-to", "pdf", // 转换为PDF
"--outdir", outputDir, // 输出目录
sourceFile.getAbsolutePath() // 源文件
};
// 执行转换
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new RuntimeException("转换失败,exitCode=" + exitCode);
}
// 返回PDF文件
String pdfFileName = sourceFile.getName().replaceAll("\.[^.]+$", ".pdf");
File pdfFile = new File(outputDir, pdfFileName);
log.info("✅ 转换成功:{} → {}", sourceFile.getName(), pdfFile.getName());
return pdfFile;
}
/**
* Office文档转HTML
*/
public File convertToHtml(File sourceFile) throws IOException, InterruptedException {
String outputDir = sourceFile.getParent();
String[] cmd = {
libreOfficeHome + "/program/soffice",
"--headless",
"--convert-to", "html",
"--outdir", outputDir,
sourceFile.getAbsolutePath()
};
Process process = Runtime.getRuntime().exec(cmd);
process.waitFor();
String htmlFileName = sourceFile.getName().replaceAll("\.[^.]+$", ".html");
return new File(outputDir, htmlFileName);
}
}
预览接口
@RestController
@RequestMapping("/api/preview")
public class PreviewController {
@Autowired
private FileService fileService;
@Autowired
private OfficeConvertService convertService;
/**
* Office文档预览(自动转PDF)
*/
@GetMapping("/office/{fileId}")
public void previewOffice(@PathVariable Long fileId,
HttpServletResponse response) throws IOException {
// 1. 获取文件
FileInfo fileInfo = fileService.getById(fileId);
File sourceFile = new File(fileInfo.getFilePath());
// 2. 检查是否已转换
String pdfPath = fileInfo.getFilePath().replaceAll("\.[^.]+$", ".pdf");
File pdfFile = new File(pdfPath);
if (!pdfFile.exists() || pdfFile.lastModified() < sourceFile.lastModified()) {
// 转换为PDF
try {
pdfFile = convertService.convertToPdf(sourceFile);
} catch (Exception e) {
log.error("转换失败", e);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
return;
}
}
// 3. 输出PDF
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "inline");
try (FileInputStream fis = new FileInputStream(pdfFile);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(fis, os);
}
}
}
2. 前端Office预览(office-viewer)
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/@microsoft/office-js@1/dist/office.js"></script>
</head>
<body>
<div id="office-container" style="width: 100%; height: 600px;"></div>
<script>
// 使用Office Online预览(需要公网可访问的URL)
const fileUrl = 'https://example.com/files/document.docx';
const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fileUrl)}`;
const iframe = document.createElement('iframe');
iframe.src = officeUrl;
iframe.width = '100%';
iframe.height = '600px';
document.getElementById('office-container').appendChild(iframe);
</script>
</body>
</html>
🎬 方案四:视频/音频预览
1. HTML5原生播放器
<!-- 视频播放 -->
<video controls width="800">
<source src="/api/file/view/video.mp4" type="video/mp4">
<source src="/api/file/view/video.webm" type="video/webm">
您的浏览器不支持视频播放
</video>
<!-- 音频播放 -->
<audio controls>
<source src="/api/file/view/audio.mp3" type="audio/mpeg">
<source src="/api/file/view/audio.ogg" type="audio/ogg">
您的浏览器不支持音频播放
</audio>
2. Video.js播放器(推荐)
<!DOCTYPE html>
<html>
<head>
<link href="https://vjs.zencdn.net/8.6.1/video-js.css" rel="stylesheet">
<script src="https://vjs.zencdn.net/8.6.1/video.min.js"></script>
</head>
<body>
<video id="my-video" class="video-js" controls preload="auto"
width="800" height="450"
data-setup='{}'>
<source src="/api/file/view/video.mp4" type="video/mp4">
<p class="vjs-no-js">
请启用JavaScript以播放视频
</p>
</video>
<script>
const player = videojs('my-video', {
controls: true,
autoplay: false,
preload: 'auto',
playbackRates: [0.5, 1, 1.5, 2], // 倍速播放
fluid: true // 响应式
});
// 监听播放事件
player.on('play', function() {
console.log('开始播放');
});
player.on('ended', function() {
console.log('播放结束');
});
</script>
</body>
</html>
3. 后端流式传输
@RestController
@RequestMapping("/api/video")
public class VideoStreamController {
/**
* 视频流式传输(支持断点续传)
*/
@GetMapping("/stream/{fileId}")
public void streamVideo(@PathVariable Long fileId,
@RequestHeader(value = "Range", required = false) String range,
HttpServletResponse response) throws IOException {
// 1. 获取文件
FileInfo fileInfo = fileService.getById(fileId);
File file = new File(fileInfo.getFilePath());
long fileLength = file.length();
long start = 0;
long end = fileLength - 1;
// 2. 解析Range请求头
if (range != null && range.startsWith("bytes=")) {
String[] ranges = range.substring(6).split("-");
start = Long.parseLong(ranges[0]);
if (ranges.length > 1 && !ranges[1].isEmpty()) {
end = Long.parseLong(ranges[1]);
}
}
long contentLength = end - start + 1;
// 3. 设置响应头
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206
response.setContentType("video/mp4");
response.setHeader("Accept-Ranges", "bytes");
response.setHeader("Content-Range",
String.format("bytes %d-%d/%d", start, end, fileLength));
response.setContentLengthLong(contentLength);
// 4. 输出文件流
try (RandomAccessFile raf = new RandomAccessFile(file, "r");
OutputStream os = response.getOutputStream()) {
raf.seek(start);
byte[] buffer = new byte[8192];
long bytesRemaining = contentLength;
while (bytesRemaining > 0) {
int bytesToRead = (int) Math.min(buffer.length, bytesRemaining);
int bytesRead = raf.read(buffer, 0, bytesToRead);
if (bytesRead == -1) break;
os.write(buffer, 0, bytesRead);
bytesRemaining -= bytesRead;
}
os.flush();
}
}
}
💻 方案五:代码预览
1. Highlight.js代码高亮
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<style>
pre {
margin: 0;
border-radius: 5px;
}
code {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
}
</style>
</head>
<body>
<pre><code class="language-java" id="code-content"></code></pre>
<script>
// 从后端获取代码
fetch('/api/file/content/1')
.then(res => res.text())
.then(code => {
document.getElementById('code-content').textContent = code;
hljs.highlightAll();
});
</script>
</body>
</html>
2. Monaco Editor(VSCode编辑器)
<!DOCTYPE html>
<html>
<head>
<style>
#monaco-container {
width: 100%;
height: 600px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="monaco-container"></div>
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
<script>
require.config({
paths: {
'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs'
}
});
require(['vs/editor/editor.main'], function() {
// 从后端获取代码
fetch('/api/file/content/1')
.then(res => res.text())
.then(code => {
monaco.editor.create(document.getElementById('monaco-container'), {
value: code,
language: 'java',
theme: 'vs-dark',
readOnly: true, // 只读模式
automaticLayout: true,
minimap: {
enabled: true
},
scrollBeyondLastLine: false
});
});
});
</script>
</body>
</html>
🚀 完整预览系统
1. 统一预览接口
@RestController
@RequestMapping("/api/preview")
public class UnifiedPreviewController {
@Autowired
private Map<String, FilePreviewHandler> handlerMap;
/**
* 统一预览入口
*/
@GetMapping("/{fileId}")
public void preview(@PathVariable Long fileId,
HttpServletResponse response) throws IOException {
// 1. 获取文件信息
FileInfo fileInfo = fileService.getById(fileId);
if (fileInfo == null) {
response.setStatus(HttpStatus.NOT_FOUND.value());
return;
}
// 2. 根据文件类型选择处理器
String fileType = getFileType(fileInfo.getFileName());
FilePreviewHandler handler = handlerMap.get(fileType + "Handler");
if (handler == null) {
response.setStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value());
response.getWriter().write("不支持的文件类型");
return;
}
// 3. 执行预览
handler.preview(fileInfo, response);
}
private String getFileType(String fileName) {
String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
if (Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp").contains(ext)) {
return "image";
} else if ("pdf".equals(ext)) {
return "pdf";
} else if (Arrays.asList("doc", "docx", "xls", "xlsx", "ppt", "pptx").contains(ext)) {
return "office";
} else if (Arrays.asList("mp4", "avi", "mov", "webm").contains(ext)) {
return "video";
} else if (Arrays.asList("mp3", "wav", "ogg").contains(ext)) {
return "audio";
} else if (Arrays.asList("txt", "java", "py", "js", "html", "css").contains(ext)) {
return "text";
}
return "unknown";
}
}
2. 预览处理器接口
public interface FilePreviewHandler {
void preview(FileInfo fileInfo, HttpServletResponse response) throws IOException;
}
// 图片预览处理器
@Component("imageHandler")
public class ImagePreviewHandler implements FilePreviewHandler {
@Override
public void preview(FileInfo fileInfo, HttpServletResponse response) throws IOException {
File file = new File(fileInfo.getFilePath());
response.setContentType(fileInfo.getContentType());
response.setHeader("Content-Disposition", "inline");
try (FileInputStream fis = new FileInputStream(file);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(fis, os);
}
}
}
// Office预览处理器
@Component("officeHandler")
public class OfficePreviewHandler implements FilePreviewHandler {
@Autowired
private OfficeConvertService convertService;
@Override
public void preview(FileInfo fileInfo, HttpServletResponse response) throws IOException {
// 转换为PDF再预览
File sourceFile = new File(fileInfo.getFilePath());
File pdfFile = convertService.convertToPdf(sourceFile);
response.setContentType("application/pdf");
response.setHeader("Content-Disposition", "inline");
try (FileInputStream fis = new FileInputStream(pdfFile);
OutputStream os = response.getOutputStream()) {
IOUtils.copy(fis, os);
}
}
}
3. 前端统一组件
<template>
<div class="file-preview">
<!-- 图片预览 -->
<div v-if="fileType === 'image'" class="image-preview">
<img :src="previewUrl" alt="预览">
</div>
<!-- PDF预览 -->
<div v-else-if="fileType === 'pdf'" class="pdf-preview">
<iframe :src="previewUrl" frameborder="0"></iframe>
</div>
<!-- Office预览 -->
<div v-else-if="fileType === 'office'" class="office-preview">
<iframe :src="previewUrl" frameborder="0"></iframe>
</div>
<!-- 视频预览 -->
<div v-else-if="fileType === 'video'" class="video-preview">
<video controls :src="previewUrl"></video>
</div>
<!-- 音频预览 -->
<div v-else-if="fileType === 'audio'" class="audio-preview">
<audio controls :src="previewUrl"></audio>
</div>
<!-- 代码预览 -->
<div v-else-if="fileType === 'text'" class="code-preview">
<pre><code v-html="highlightedCode"></code></pre>
</div>
<!-- 不支持预览 -->
<div v-else class="unsupported">
<p>不支持在线预览此文件</p>
<el-button @click="download">下载查看</el-button>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getFileInfo } from '@/api/file'
import hljs from 'highlight.js'
const props = defineProps({
fileId: {
type: Number,
required: true
}
})
const fileInfo = ref(null)
const fileContent = ref('')
const fileType = computed(() => {
if (!fileInfo.value) return 'unknown'
const ext = fileInfo.value.fileName.split('.').pop().toLowerCase()
if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) return 'image'
if (ext === 'pdf') return 'pdf'
if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) return 'office'
if (['mp4', 'avi', 'mov'].includes(ext)) return 'video'
if (['mp3', 'wav'].includes(ext)) return 'audio'
if (['txt', 'java', 'js', 'py'].includes(ext)) return 'text'
return 'unknown'
})
const previewUrl = computed(() => {
return `/api/preview/${props.fileId}`
})
const highlightedCode = computed(() => {
if (fileType.value !== 'text') return ''
return hljs.highlightAuto(fileContent.value).value
})
onMounted(async () => {
fileInfo.value = await getFileInfo(props.fileId)
if (fileType.value === 'text') {
const res = await fetch(`/api/file/content/${props.fileId}`)
fileContent.value = await res.text()
}
})
const download = () => {
window.open(`/api/file/download/${props.fileId}`)
}
</script>
<style scoped>
.file-preview {
width: 100%;
height: 600px;
}
iframe, img, video {
width: 100%;
height: 100%;
}
.unsupported {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
</style>
🎯 性能优化
1. 缓存转换结果
@Service
public class PreviewCacheService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 获取缓存的预览文件
*/
public File getCachedPreview(Long fileId, String targetFormat) {
String cacheKey = "preview:" + fileId + ":" + targetFormat;
String cachePath = redisTemplate.opsForValue().get(cacheKey);
if (cachePath != null) {
File file = new File(cachePath);
if (file.exists()) {
return file;
}
}
return null;
}
/**
* 缓存预览文件
*/
public void cachePreview(Long fileId, String targetFormat, File previewFile) {
String cacheKey = "preview:" + fileId + ":" + targetFormat;
redisTemplate.opsForValue().set(
cacheKey,
previewFile.getAbsolutePath(),
Duration.ofDays(7)
);
}
}
2. 异步转换
@Service
public class AsyncConvertService {
@Async("previewExecutor")
public CompletableFuture<File> convertAsync(File sourceFile) {
try {
File pdfFile = officeConvertService.convertToPdf(sourceFile);
return CompletableFuture.completedFuture(pdfFile);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}
@Configuration
public class ExecutorConfig {
@Bean(name = "previewExecutor")
public Executor previewExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("preview-");
executor.initialize();
return executor;
}
}
📝 总结
文件类型支持清单
| 文件类型 | 预览方案 | 推荐方案 |
|---|---|---|
| 图片 | 原生img | Viewer.js |
| Chrome内置 | PDF.js | |
| Word/Excel/PPT | 转PDF | LibreOffice+PDF.js |
| 视频 | HTML5 video | Video.js |
| 音频 | HTML5 audio | - |
| 代码 | Highlight.js | Monaco Editor |
| Markdown | Marked.js | - |
| TXT | 直接显示 | Highlight.js |
关键要点 🎯
- 按需转换 - 首次访问时转换,缓存结果
- 异步处理 - 大文件转换不阻塞请求
- 流式传输 - 视频支持断点续传
- 权限控制 - 检查用户权限
- CDN加速 - 静态资源使用CDN
让文件预览如丝般顺滑! 🎉🎉🎉