最近在前端实现了附件预览。原本已经实现了文件的下载功能,但是业务表示需要直接在线预览,后端没有时间,只好由前端实现了。总体方案,使用fetch请求后端服务地址,将响应内容转换为blob流,然后前端使用相关插件,解析blob流内容,将附件内容以html的方式渲染在浏览器中。
1.图片预览
- 核心代码
js
//将响应内容转成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

- 核心代码
js
//将响应内容转成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条.
- 核心代码
js
// 第一步:使用 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预览是后端实现的,直接打开一个链接就是pdf形式,借助的是pdfjs。chrome浏览器其实也可以直接打开pdf文件,其内置的也是pdfjs