javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > 前端在线预览

前端实现图片、Word、Excel与PDF文件在线预览

作者:xiaobaoyu

本文将手把手教你用纯前端方案实现图片、Word、Excel、PDF的在线预览,借助docx-preview、xlsx和Handsontable等插件,轻松解析blob流,解决大文件卡顿问题,让预览又快又稳

最近在前端实现了附件预览。原本已经实现了文件的下载功能,但是业务表示需要直接在线预览,后端没有时间,只好由前端实现了。总体方案,使用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 导入。你可以根据需要更换版本号。

配置 WorkerGlobalWorkerOptions.workerSrc 必须设置,它指定了 PDF 解析引擎(pdf.worker)的位置。

加载文档pdfjsLib.getDocument(url) 返回一个 Promise,用于加载 PDF 文件。url 可以是网络地址或本地路径。

渲染页面

交互控制:通过监听按钮点击,调用 renderPage() 函数实现翻页。

以上就是前端实现图片、Word、Excel与PDF文件在线预览的详细内容,更多关于前端在线预览的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文