前言
在企業應用中,我們時常會遇到需要上傳并展示 Excel 文件的需求,以實現文件內容的在線預覽。經過一番探索與嘗試,筆者最終借助 exceljs
這一庫成功實現了該功能。本文將以 Vue 3 為例,演示如何實現該功能,代碼示例可直接復制運行,希望能為大家在處理類似問題時提供新的思路和解決方案。
技術難點
基礎單元格和合并單元格的混合處理
文字樣式和背景樣式的讀取映射
富文本內容格式的處理
excel文件展示
實際實現預覽效果
核心代碼
exceljs提供了合并區域的數據,我們只需要根據合并區域去判斷什么時候該合并,就能很好的實現基礎單元格和合并單元格的混合繪制
for (let merge of merges) {const [start, end] = merge.split(":");const startCell = worksheet.getCell(start);const endCell = worksheet.getCell(end);const startRow = startCell.row,startCol = startCell.col;const endRow = endCell.row,endCol = endCell.col;if (startRow === rowIndex && startCol === colIndex) {rowspan = endRow - startRow + 1;colspan = endCol - startCol + 1;isMerged = true;let styles = handleStyles(cell);allHtml += `<td rowspan="${rowspan}" colspan="${colspan}" style="${styles}">${handleValue(startCell.value)}</td>`;break;}if (rowIndex >= startRow &&rowIndex <= endRow &&colIndex >= startCol &&colIndex <= endCol) {isMerged = true;break;}}
完整源碼
<template><div><el-uploadaction="":auto-upload="false":show-file-list="true":on-change="handleFileUpload"accept=".xlsx,.xls"><el-button type="primary"> 上傳 Excel </el-button></el-upload><!-- 渲染 Excel 生成的 HTML 表格 --><div v-html="tableHtml" /></div>
</template><script setup>
import { ref } from "vue";
import * as ExcelJS from "exceljs";const tableHtml = ref(""); // 存儲 HTML 表格內容
const themeColors = {0: "#FFFFFF", // 白色 √1: "#000000", // 黑色 √2: "#C9CDD1", // 灰色 √3: "#4874CB", // 藍色 √4: "#D9E1F4", // 淺藍 √5: "#F9CBAA", // 橙色 √6: "#F2BA02", // 淺橙 √7: "#00FF00", // 淺綠 √8: "#30C0B4", // 青色 √9: "#E54C5E", // 紅色 √10: "#FFC7CE", // 淺紅11: "#7030A0", // 紫色
};// 獲取單元格顏色
const getCellColor = (cell) => {if (cell.fill && cell.fill.fgColor) {if (cell.fill.fgColor.argb) {return `#${cell.fill.fgColor.argb.substring(2)}`; // ARGB 轉 RGB}if (cell.fill.fgColor.theme !== undefined) {return themeColors[cell.fill.fgColor.theme] || "#FFFFFF"; // 主題色轉換}}return ""; // 無顏色
};
// 獲取單元格字體顏色
const getCellFontColor = (cell) => {if (cell.font && cell.font.color && cell.font.color.argb) {return `#${cell.font.color.argb.substring(2)}`; // ARGB 轉 RGB}if (cell.font && cell.font.color && cell.font.color.theme) {return themeColors[cell.font.color.theme] || "#000"; // 主題色轉換}return "#000"; // 默認黑色
};
const handleStyles = (cell) => {let styles = [];// 讀取字體顏色styles.push(`color: ${getCellFontColor(cell)}`);// 讀取背景色styles.push(`background-color: ${getCellColor(cell)}`);// 加粗if (cell.font && cell.font.bold) {styles.push("font-weight: bold");}// 文字對齊if (cell.alignment) {if (cell.alignment.horizontal) {styles.push(`text-align: ${cell.alignment.horizontal}`);}if (cell.alignment.vertical) {styles.push(`vertical-align: ${cell.alignment.vertical}`);}}return styles.join("; ");
};// 處理上傳的 Excel 文件
const handleFileUpload = async (file) => {const excelData = await readExcel(file.raw);tableHtml.value = excelData; // 更新 HTML 表格內容
};
// 處理常規單元格內容
const handleValueSimple = (value) => {if (value && typeof value === "object" && value.richText) {const valueStr = value.richText.reduce((acc, curr) => {let colorValue = "";if (curr.font && curr.font.color && curr.font.color.theme) {colorValue = getCellFontColor(curr) || `#000`;}if (curr.font && curr.font.color && curr.font.color.argb) {colorValue = `#${curr.font.color.argb.substring(2)}`;} else {colorValue = `#000`;}return acc + `<span style="color:${colorValue}">${curr.text}</span>`;}, "");return valueStr;}return value ? value : "";
};
// 處理合并單元格內容
const handleValue = (value) => {if (value && typeof value === "object" && value.richText) {const valueArr = value.richText.reduce((acc, curr) => {let colorValue = "";if (curr.font && curr.font.color && curr.font.color.argb) {colorValue = `#${curr.font.color.argb.substring(2)}`;} else {colorValue = `#000`;}const newData = curr.text.split(/\r/).map((item) => `<p style="color:${colorValue}">${item}</p>`);return acc.concat(newData);}, []);return valueArr.join("").replace(/\n/g, "<br />");}return value ? value : "";
};let worksheetIds = [];
// 讀取 Excel 并轉換成 HTML
const readExcel = async (file) => {const workbook = new ExcelJS.Workbook();const arrayBuffer = await file.arrayBuffer();const { worksheets } = await workbook.xlsx.load(arrayBuffer);worksheetIds = worksheets.map((v) => v.id); // 獲取工作表 ID集合let allHtml = "";workbook.eachSheet(function (worksheet, sheetId) {// 處理合并單元格const merges = worksheet?.model?.merges || [];const currentSheetIndex = worksheetIds.indexOf(sheetId); // 獲取當前工作表的索引allHtml +='<table border="1" style="border-collapse: collapse;width:100%;margin-bottom: 20px;">';worksheet.eachRow((row, rowIndex) => {allHtml += "<tr>";row.eachCell((cell, colIndex) => {let cellValue = cell.value || "";// 處理合并單元格let rowspan = 1,colspan = 1;let isMerged = false;for (let merge of merges) {const [start, end] = merge.split(":");const startCell = worksheet.getCell(start);const endCell = worksheet.getCell(end);const startRow = startCell.row,startCol = startCell.col;const endRow = endCell.row,endCol = endCell.col;if (startRow === rowIndex && startCol === colIndex) {rowspan = endRow - startRow + 1;colspan = endCol - startCol + 1;isMerged = true;let styles = handleStyles(cell);allHtml += `<td rowspan="${rowspan}" colspan="${colspan}" style="${styles}">${handleValue(startCell.value)}</td>`;break;}if (rowIndex >= startRow &&rowIndex <= endRow &&colIndex >= startCol &&colIndex <= endCol) {isMerged = true;break;}}if (!isMerged) {let styles = handleStyles(cell);// 生成 HTML 單元格allHtml += `<td ${rowspan > 1 ? `rowspan="${rowspan}"` : ""} ${colspan > 1 ? `colspan="${colspan}"` : ""} style="${styles}">${handleValueSimple(cellValue)}</td>`;}});allHtml += "</tr>";});allHtml += "</table>";});return allHtml;
};
</script>
拓展
exceljs這個庫的作用是啥?
ExcelJS
是一個功能強大的庫,用于讀取、操作和寫入 Excel 文件(.xlsx 和 .csv 格式)。它允許開發者通過編程方式處理 Excel 文檔,包括創建新工作簿、添加數據、格式化單元格、插入圖表等。這個庫可以在服務器端(如 Node.js 環境)或客戶端(如在瀏覽器中使用 Webpack 或 Rollup 等工具打包的 JavaScript 應用程序)使用。
主要特性
- 創建工作簿和工作表:可以輕松地創建新的 Excel 文件或修改已有的文件。
- 豐富的樣式支持:支持字體、顏色、邊框、對齊方式等多種樣式設置。
- 數據處理:支持從各種數據源導入數據,并能將數據導出為 Excel 文件。
- 公式和計算:可以添加公式到單元格,并支持基本的 Excel 計算。
- 圖表支持:能夠在 Excel 文件中創建圖表。
- 圖片和繪圖:支持向 Excel 文件中添加圖片和繪制圖形。
使用場景
ExcelJS
廣泛應用于需要與 Excel 文件進行交互的應用程序開發中,比如:
- 數據報告生成
- 數據導入/導出功能實現
- 在線 Excel 編輯器
示例代碼片段
這里是一個簡單的示例,展示如何使用 ExcelJS
創建一個新的工作簿并添加一些數據:
const ExcelJS = require('exceljs');// 創建一個新的工作簿
let workbook = new ExcelJS.Workbook();
let worksheet = workbook.addWorksheet('測試工作表');// 添加一行數據
worksheet.addRow(['姓名', '年齡', '郵箱']);
worksheet.addRow(['張三', 28, 'zhangsan@example.com']);
worksheet.addRow(['李四', 23, 'lisi@example.com']);// 保存工作簿到文件
workbook.xlsx.writeFile('example.xlsx').then(() => {console.log('文件保存成功');});
?這個例子展示了如何創建一個新的 Excel 文件,并向其中添加一些簡單數據。ExcelJS
的靈活性和強大功能使其成為處理 Excel 文件的一個優秀選擇。
npm上的puppeteer庫是做什么的?
npm 上的?puppeteer
?是一個用于自動化控制 Chrome/Chromium 瀏覽器的 Node.js 庫,由 Google 團隊開發。它通過提供高級 API,允許你以編程方式模擬用戶在瀏覽器中的操作,適用于多種場景:
核心功能
無頭瀏覽器控制
可啟動?Headless 模式(無界面)或完整瀏覽器,執行自動化任務(如點擊、輸入、導航等)。動態內容抓取
適用于爬取 JavaScript 渲染的頁面(如 React/Vue 單頁應用),傳統爬蟲工具難以直接獲取動態內容。生成截圖與 PDF
精確截取網頁全屏、指定區域,或將頁面導出為 PDF(保留樣式)。自動化測試
模擬用戶操作,測試網頁功能(如表單提交、UI 交互),生成測試報告。性能分析
監控頁面加載速度、資源請求,優化性能。
?典型使用場景
數據抓取:爬取電商價格、社交媒體內容等動態加載的數據。
自動化操作:自動填寫表單、批量下載文件、定時簽到等。
預渲染:為 SEO 生成靜態化內容,解決 SPA 首屏加載問題。
生成報告:將數據可視化頁面導出為 PDF 或圖片存檔。
?基礎示例
const puppeteer = require('puppeteer');(async () => {// 啟動瀏覽器(默認 Headless 模式)const browser = await puppeteer.launch();const page = await browser.newPage();// 導航到目標頁面await page.goto('https://example.com');// 截圖并保存await page.screenshot({ path: 'example.png' });// 關閉瀏覽器await browser.close();
})();
安裝與依賴
?通過 npm 安裝(會自動下載匹配的 Chromium):
npm install puppeteer
優勢
?
直接操控瀏覽器:行為更接近真實用戶,繞過反爬蟲機制(需合理使用)。
功能全面:支持網絡請求攔截、Cookie 管理、模擬設備(如手機/平板)等。
社區活躍:豐富的文檔和第三方工具(如?
puppeteer-cluster
?多任務優化)。
若你需要處理復雜的網頁交互或動態內容,Puppeteer 是一個高效且靈活的選擇。
vue中為什么切換動態組件需要使用shallowRef ?
在 Vue 3 中,使用?shallowRef?來管理動態組件的切換,主要是出于性能優化和避免不必要的深度響應式追蹤的考慮。以下是詳細解釋:
1. ref vs shallowRef 的核心區別
會對其包裹的值進行深度響應式轉換(遞歸將對象屬性轉為響應式)。
僅對值的引用變化進行響應式追蹤,不會深度遞歸轉換內部屬性。
2.?動態組件的場景分析
當使用?<component :is="currentComponent">?切換組件時:
- 組件對象本身是穩定的
動態組件的核心操作是替換整個組件對象(如?currentComponent.value = NewComponent),而不是修改組件對象的內部屬性。
- 組件對象可能很大
一個組件對象(如導入的 Vue 組件)通常包含大量屬性(如?props、methods、生命周期等),如果使用?ref?深度響應式化,會帶來額外性能開銷。
- 深度響應式可能導致問題
某些組件屬性(如函數方法)被 Vue 代理后可能產生副作用(如破壞函數內部?this?綁定,或與第三方庫預期結構沖突)。3.?為什么?shallowRef?更合適?
- 性能優化
避免深度遍歷組件對象的所有屬性,減少不必要的響應式代理。
- 符合實際需求
動態組件切換只需要響應組件引用的變化(整體替換),無需關心組件內部屬性的變化。
- 規避潛在問題
防止 Vue 對組件對象內部屬性(如方法、生命周期鉤子)的深度代理導致意外行為。4.?示例對比
// 使用 ref(不推薦)import { ref } from 'vue';import HeavyComponent from './HeavyComponent.vue';const currentComponent = ref(HeavyComponent);// Vue 會深度代理 HeavyComponent 的所有屬性,但實際只需要引用變化觸發更新// 使用 shallowRef(推薦)import { shallowRef } from 'vue';const currentComponent = shallowRef(HeavyComponent);// 僅追蹤 currentComponent.value 的引用變化,高效且安全
5.?官方建議
Vue 官方文檔在動態組件示例中直接使用普通變量(非響應式),但在需要響應式時建議使用?shallowRef,明確表示:
“如果你確實需要響應性,可以使用?shallowRef。”
?在動態組件切換場景中,shallowRef?通過避免深度響應式轉換,在保證功能正確性的同時,提升了性能并規避了潛在問題。這正是它與?ref?的核心區別所在。