前端实现图片、Word、Excel与PDF文件在线预览
作者:xiaobaoyu
最近在前端实现了附件预览。原本已经实现了文件的下载功能,但是业务表示需要直接在线预览,后端没有时间,只好由前端实现了。总体方案,使用fetch请求后端服务地址,将响应内容转换为blob流,然后前端使用相关插件,解析blob流内容,将附件内容以html的方式渲染在浏览器中。
1.图片预览
核心代码
//将响应内容转成blob
response.blob()
//创建对象地址
const imgUrl = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = imgUrl;
img.onload = () => {
URL.revokeObjectURL(imgUrl);
};
img.onerror = () => {
console.error('图片加载失败');
};
document.body.appendChild(img);2.word预览
利用了docx-preview插件和jszip插件,但是只能实现docx后缀文档的预览,需要后端将doc转成docx 下面是DeepSeek给的方案,最终选择了docx-preview

核心代码
//将响应内容转成blob
response.blob()
// 渲染 Word 文档到预览容器
docx.renderAsync(blob, document.getElementById('word-preview'))
.then(() => {
console.log('Word document rendered successfully!');
});3.excel预览
excel预览相对比较复杂了,借助xlsx进行表格数据的解析和handsontable进行表格样式的渲染。涉及到数据量大,多sheet的情况,比如几千条数据,在前端解析并渲染,响应非常慢,很卡顿,于是做了分页,每页100条.
核心代码
// 第一步:使用 fetch 获取文件流,并使用XLSX解析文件流数据
async function previewExcel() {
try {
const response = await fetch(excelUrl, {
responseType: 'arraybuffer', // 关键:指定响应类型
});
const arrayBuffer = await response.arrayBuffer();
// 解析Excel
const workbook = XLSX.read(arrayBuffer, { type: 'array',cellStyles: true });
// 遍历所有 Sheet 页
workbook.SheetNames.forEach((sheetName) => {
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 'A', defval: '' ,blankrows:true,cellStyles: true });
const mergeCells = getMergeCells(worksheet); // 获取合并单元格信息
const cellStyles = getCellStyles(worksheet); // 获取单元格样式信息
sheetsData[sheetName] = {
data: jsonData,
mergeCells,
cellStyles,
};
});
// 默认渲染第一个 Sheet 页
currentSheetName = workbook.SheetNames[0];
const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
renderTable(data, mergeCells, cellStyles);
// 渲染 Sheet 页切换菜单
renderSheetTabs(workbook.SheetNames);
} catch (error) {
console.error('预览失败:', error);
}
}
//获取合并单元格的数据
function getMergeCells(worksheet) {
const merges = worksheet['!merges'] || [];
return merges.map(merge => ({
row: merge.s.r,
col: merge.s.c,
rowspan: merge.e.r - merge.s.r + 1,
colspan: merge.e.c - merge.s.c + 1,
}));
}
function convertHexToCssHex(hex) {
// 检查输入是否为有效的十六进制颜色值(6 位字符)
if (/^[0-9A-Fa-f]{6}$/.test(hex)) {
return "#" + hex;
} else {
return hex;
}
}
//获取单元格的样式
function getCellStyles(worksheet) {
const styles = [];
// 防止空sheet页整个excel都不渲染
if (!worksheet) {
return styles;
}
if (!worksheet['!ref']) {
return styles;
}
const range = XLSX.utils.decode_range(worksheet['!ref']); // 获取工作表范围
for (let row = range.s.r; row <= range.e.r; row++) {
for (let col = range.s.c; col <= range.e.c; col++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: col });
const cell = worksheet[cellAddress];
if (cell && cell.s) {
// XLSX 返回的颜色格式为“FFFFFF”,需要在前面加"#"才能正常渲染颜色
// XLSX未能解析出字体样式
const fgColor = cell.s.fgColor?.rgb // "FFFF00"
const background = convertHexToCssHex(fgColor) ?? 'white'
styles.push({
row,
col,
style: {
background,
color: cell.s.font?.color?.rgb || 'black', // 字体颜色
fontWeight: cell.s.font?.bold ? 'bold' : 'normal', // 字体加粗
},
});
}
}
}
return styles;
}
// 自定义样式渲染器(通用)
const styleRenderer = (hotInstance, TD, row, col, prop, value, cellProperties) => {
// 调用默认文本渲染器
Handsontable.renderers.TextRenderer(hotInstance, TD, row, col, prop, value, cellProperties);
// 获取当前单元格的样式配置
const { styles } = hotInstance.getSettings();
const cell = styles.find(s => s.row === row && s.col === col);
// 应用样式
if (cell) {
TD.style.background = cell.style.background || '';
TD.style.color = cell.style.color || '';
TD.style.fontWeight = cell.style.fontWeight || '';
}
};
// 注册渲染器
Handsontable.renderers.registerRenderer('styleRenderer', styleRenderer);
// 渲染表格到页面
let hot;
function renderTable(data,mergeCells, cellStyles) {
const container = document.getElementById('excel-preview');
if (!container) {
console.error('容器元素未找到!');
return;
}
container.innerHTML = ''; // 清空容器
// 分页数据
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
const pageData = data.slice(start, end);
// 生成动态 cell 配置
const cellConfig = cellStyles.map(style => ({
row: style.row,
col: style.col,
renderer: 'styleRenderer', // 使用注册的自定义渲染器
}));
hot = new Handsontable(container, {
data: pageData, // 表格数据
rowHeaders: true, // 显示行号
colHeaders: true, // 显示列号
mergeCells: mergeCells, // 设置合并单元格
styles:cellStyles,
cell: cellConfig, // 动态样式配置
contextMenu: true, // 启用右键菜单
manualRowResize: true, // 允许调整行高
manualColumnResize: true, // 允许调整列宽
stretchH: 'all', // 拉伸列宽以适应容器
height: 'auto', // 自动高度
licenseKey: 'non-commercial-and-evaluation', // 免费版 License
});
// 渲染分页导航
renderPagination(data.length);
}
function renderPagination(totalRows) {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = '';
const totalPages = Math.ceil(totalRows / pageSize);
// 上一页按钮
const prevButton = document.createElement('button');
prevButton.textContent = '上一页';
prevButton.disabled = currentPage === 1;
prevButton.addEventListener('click', () => {
currentPage--;
const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
renderTable(data, mergeCells, cellStyles);
});
paginationContainer.appendChild(prevButton);
// 页码显示
const pageInfo = document.createElement('span');
pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
paginationContainer.appendChild(pageInfo);
// 下一页按钮
const nextButton = document.createElement('button');
nextButton.textContent = '下一页';
nextButton.disabled = currentPage === totalPages;
nextButton.addEventListener('click', () => {
currentPage++;
const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
renderTable(data, mergeCells, cellStyles);
});
paginationContainer.appendChild(nextButton);
}
function renderSheetTabs(sheetNames) {
const tabsContainer = document.getElementById('sheet-tabs');
tabsContainer.innerHTML = ''; // 清空旧内容
sheetNames.forEach((sheetName) => {
const tab = document.createElement('button');
tab.textContent = sheetName;
tab.className = sheetName === currentSheetName ? 'active' : ''; // 高亮当前 Sheet 页
tab.addEventListener('click', () => {
currentSheetName = sheetName;
currentPage = 1; // 切换 Sheet 页时重置页码
const { data, mergeCells, cellStyles } = sheetsData[currentSheetName];
renderTable(data, mergeCells, cellStyles);
updateActiveTab(tab); // 更新高亮状态
});
tabsContainer.appendChild(tab);
});
}
function updateActiveTab(activeTab) {
const tabs = document.querySelectorAll('#sheet-tabs button');
tabs.forEach((tab) => tab.classList.remove('active'));
activeTab.classList.add('active');
}
document.addEventListener('DOMContentLoaded', function () {
previewExcel()
});以上实现了多sheet,分页,合并单元格,单元格背景色,非标准的表头和表体类表格的渲染,借助了AI和官方文档实现了对应功能,供参考!
4.pdf预览
使用 PDF.js 实现 PDF 预览是前端开发中的常见需求。PDF.js 是 Mozilla 开发的开源库,无需任何本地支持就能在所有主流浏览器上渲染 PDF 文档。
下面提供一个可直接运行的 HTML 示例,并附上详细说明。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>PDF.js 基础预览示例</title>
<style>
/* 简单美观的页面样式 */
body { font-family: sans-serif; text-align: center; }
#pdf-container {
width: 600px;
height: 800px;
margin: 20px auto;
border: 1px solid #ccc;
overflow: auto; /* 防止内容溢出 */
}
#pdf-container canvas {
width: 100%;
height: auto;
}
#controls { margin-top: 10px; }
button { padding: 8px 16px; margin: 0 5px; cursor: pointer; }
#page-num { margin: 0 10px; }
</style>
</head>
<body>
<h1>PDF.js 预览</h1>
<!-- PDF 渲染容器 -->
<div id="pdf-container"></div>
<!-- 翻页控制 -->
<div id="controls">
<button id="prev-page">上一页</button>
<span id="page-num">1 / 1</span>
<button id="next-page">下一页</button>
</div>
<!-- 使用 type="module" 来使用 ES Module 方式引入 PDF.js -->
<script type="module">
// 1. 从 CDN 导入 PDF.js 核心库和 Worker
import * as pdfjsLib from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.269/+esm';
import { GlobalWorkerOptions } from 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.269/+esm';
// 2. 配置 Worker(告诉 PDF.js 在哪里寻找解析引擎)
GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.269/build/pdf.worker.min.mjs';
// 3. 获取页面元素
const container = document.getElementById('pdf-container');
const prevBtn = document.getElementById('prev-page');
const nextBtn = document.getElementById('next-page');
const pageNumSpan = document.getElementById('page-num');
// --- 状态变量 ---
let pdfDoc = null; // PDF 文档对象
let currentPage = 1; // 当前页码
let totalPages = 0; // 总页数
// --- 核心函数:渲染指定页面 ---
async function renderPage(num) {
// 获取页面对象
const page = await pdfDoc.getPage(num);
// 设置渲染尺寸(根据容器宽度自适应)
const viewport = page.getViewport({ scale: 1.5 }); // 缩放比例
const containerWidth = container.clientWidth;
const scale = containerWidth / viewport.width;
const scaledViewport = page.getViewport({ scale });
// 创建 Canvas 元素
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.height = scaledViewport.height;
canvas.width = scaledViewport.width;
// 清空容器并添加新的 Canvas
container.innerHTML = '';
container.appendChild(canvas);
// 渲染 PDF 页面到 Canvas
const renderContext = {
canvasContext: context,
viewport: scaledViewport
};
await page.render(renderContext).promise;
// 更新页码显示
pageNumSpan.textContent = `${num} / ${totalPages}`;
currentPage = num;
}
// --- 加载 PDF 文档 ---
// 使用一个公开的 PDF 文件进行演示
const url = 'https://raw.githubusercontent.com/mozilla/pdf.js/ba2edeae/web/compressed.tracemonkey-pldi-09.pdf';
// 使用 PDF.js 的 API 加载文档
const loadingTask = pdfjsLib.getDocument(url);
loadingTask.promise.then((doc) => {
pdfDoc = doc;
totalPages = doc.numPages;
// 加载成功后,渲染第一页
renderPage(1);
}).catch((err) => {
console.error('PDF 加载失败:', err);
container.innerHTML = '<p>PDF 加载失败,请检查文件路径或网络。</p>';
});
// --- 绑定翻页按钮事件 ---
prevBtn.addEventListener('click', () => {
if (currentPage <= 1) return;
renderPage(currentPage - 1);
});
nextBtn.addEventListener('click', () => {
if (currentPage >= totalPages) return;
renderPage(currentPage + 1);
});
</script>
</body>
</html>代码解读
引入 PDF.js:通过 <script type="module"> 方式从 CDN 导入。你可以根据需要更换版本号。
配置 Worker:GlobalWorkerOptions.workerSrc 必须设置,它指定了 PDF 解析引擎(pdf.worker)的位置。
加载文档:pdfjsLib.getDocument(url) 返回一个 Promise,用于加载 PDF 文件。url 可以是网络地址或本地路径。
渲染页面:
- 通过
pdfDoc.getPage(num)获取指定页面。 - 通过
page.getViewport({ scale })获取页面视口信息,并设置缩放比例。 - 动态创建
<canvas>元素,调用page.render()方法将页面绘制到 Canvas 上。
交互控制:通过监听按钮点击,调用 renderPage() 函数实现翻页。
以上就是前端实现图片、Word、Excel与PDF文件在线预览的详细内容,更多关于前端在线预览的资料请关注脚本之家其它相关文章!
