Vue3使用exceljs將excel文件轉(zhuǎn)化為html預(yù)覽最佳方案
前言
在企業(yè)應(yīng)用中,我們時(shí)常會遇到需要上傳并展示 Excel 文件的需求,以實(shí)現(xiàn)文件內(nèi)容的在線預(yù)覽。經(jīng)過一番探索與嘗試,筆者最終借助 exceljs 這一庫成功實(shí)現(xiàn)了該功能。本文將以 Vue 3 為例,演示如何實(shí)現(xiàn)該功能,代碼示例可直接復(fù)制運(yùn)行,希望能為大家在處理類似問題時(shí)提供新的思路和解決方案。
技術(shù)難點(diǎn)
基礎(chǔ)單元格和合并單元格的混合處理
文字樣式和背景樣式的讀取映射
富文本內(nèi)容格式的處理
excel文件展示

實(shí)際實(shí)現(xiàn)預(yù)覽效果

核心代碼
exceljs提供了合并區(qū)域的數(shù)據(jù),我們只需要根據(jù)合并區(qū)域去判斷什么時(shí)候該合并,就能很好的實(shí)現(xiàn)基礎(chǔ)單元格和合并單元格的混合繪制
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-upload
action=""
: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 表格內(nèi)容
const themeColors = {
0: "#FFFFFF", // 白色 √
1: "#000000", // 黑色 √
2: "#C9CDD1", // 灰色 √
3: "#4874CB", // 藍(lán)色 √
4: "#D9E1F4", // 淺藍(lán) √
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 轉(zhuǎn) RGB
}
if (cell.fill.fgColor.theme !== undefined) {
return themeColors[cell.fill.fgColor.theme] || "#FFFFFF"; // 主題色轉(zhuǎn)換
}
}
return ""; // 無顏色
};
// 獲取單元格字體顏色
const getCellFontColor = (cell) => {
if (cell.font && cell.font.color && cell.font.color.argb) {
return `#${cell.font.color.argb.substring(2)}`; // ARGB 轉(zhuǎn) RGB
}
if (cell.font && cell.font.color && cell.font.color.theme) {
return themeColors[cell.font.color.theme] || "#000"; // 主題色轉(zhuǎn)換
}
return "#000"; // 默認(rèn)黑色
};
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 表格內(nèi)容
};
// 處理常規(guī)單元格內(nèi)容
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 : "";
};
// 處理合并單元格內(nèi)容
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 并轉(zhuǎn)換成 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); // 獲取當(dāng)前工作表的索引
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這個(gè)庫的作用是啥?
ExcelJS 是一個(gè)功能強(qiáng)大的庫,用于讀取、操作和寫入 Excel 文件(.xlsx 和 .csv 格式)。它允許開發(fā)者通過編程方式處理 Excel 文檔,包括創(chuàng)建新工作簿、添加數(shù)據(jù)、格式化單元格、插入圖表等。這個(gè)庫可以在服務(wù)器端(如 Node.js 環(huán)境)或客戶端(如在瀏覽器中使用 Webpack 或 Rollup 等工具打包的 JavaScript 應(yīng)用程序)使用。
主要特性
- 創(chuàng)建工作簿和工作表:可以輕松地創(chuàng)建新的 Excel 文件或修改已有的文件。
- 豐富的樣式支持:支持字體、顏色、邊框、對齊方式等多種樣式設(shè)置。
- 數(shù)據(jù)處理:支持從各種數(shù)據(jù)源導(dǎo)入數(shù)據(jù),并能將數(shù)據(jù)導(dǎo)出為 Excel 文件。
- 公式和計(jì)算:可以添加公式到單元格,并支持基本的 Excel 計(jì)算。
- 圖表支持:能夠在 Excel 文件中創(chuàng)建圖表。
- 圖片和繪圖:支持向 Excel 文件中添加圖片和繪制圖形。
使用場景
ExcelJS 廣泛應(yīng)用于需要與 Excel 文件進(jìn)行交互的應(yīng)用程序開發(fā)中,比如:
- 數(shù)據(jù)報(bào)告生成
- 數(shù)據(jù)導(dǎo)入/導(dǎo)出功能實(shí)現(xiàn)
- 在線 Excel 編輯器
示例代碼片段
這里是一個(gè)簡單的示例,展示如何使用 ExcelJS 創(chuàng)建一個(gè)新的工作簿并添加一些數(shù)據(jù):
const ExcelJS = require('exceljs');
// 創(chuàng)建一個(gè)新的工作簿
let workbook = new ExcelJS.Workbook();
let worksheet = workbook.addWorksheet('測試工作表');
// 添加一行數(shù)據(jù)
worksheet.addRow(['姓名', '年齡', '郵箱']);
worksheet.addRow(['張三', 28, 'zhangsan@example.com']);
worksheet.addRow(['李四', 23, 'lisi@example.com']);
// 保存工作簿到文件
workbook.xlsx.writeFile('example.xlsx')
.then(() => {
console.log('文件保存成功');
}); 這個(gè)例子展示了如何創(chuàng)建一個(gè)新的 Excel 文件,并向其中添加一些簡單數(shù)據(jù)。ExcelJS 的靈活性和強(qiáng)大功能使其成為處理 Excel 文件的一個(gè)優(yōu)秀選擇。
npm上的puppeteer庫是做什么的?
npm 上的 puppeteer 是一個(gè)用于自動化控制 Chrome/Chromium 瀏覽器的 Node.js 庫,由 Google 團(tuán)隊(duì)開發(fā)。它通過提供高級 API,允許你以編程方式模擬用戶在瀏覽器中的操作,適用于多種場景:
核心功能
無頭瀏覽器控制
可啟動 Headless 模式(無界面)或完整瀏覽器,執(zhí)行自動化任務(wù)(如點(diǎn)擊、輸入、導(dǎo)航等)。動態(tài)內(nèi)容抓取
適用于爬取 JavaScript 渲染的頁面(如 React/Vue 單頁應(yīng)用),傳統(tǒng)爬蟲工具難以直接獲取動態(tài)內(nèi)容。生成截圖與 PDF
精確截取網(wǎng)頁全屏、指定區(qū)域,或?qū)㈨撁鎸?dǎo)出為 PDF(保留樣式)。自動化測試
模擬用戶操作,測試網(wǎng)頁功能(如表單提交、UI 交互),生成測試報(bào)告。性能分析
監(jiān)控頁面加載速度、資源請求,優(yōu)化性能。
典型使用場景
數(shù)據(jù)抓取:爬取電商價(jià)格、社交媒體內(nèi)容等動態(tài)加載的數(shù)據(jù)。
自動化操作:自動填寫表單、批量下載文件、定時(shí)簽到等。
預(yù)渲染:為 SEO 生成靜態(tài)化內(nèi)容,解決 SPA 首屏加載問題。
生成報(bào)告:將數(shù)據(jù)可視化頁面導(dǎo)出為 PDF 或圖片存檔。
基礎(chǔ)示例
const puppeteer = require('puppeteer');
(async () => {
// 啟動瀏覽器(默認(rèn) Headless 模式)
const browser = await puppeteer.launch();
const page = await browser.newPage();
// 導(dǎo)航到目標(biāo)頁面
await page.goto('https://example.com');
// 截圖并保存
await page.screenshot({ path: 'example.png' });
// 關(guān)閉瀏覽器
await browser.close();
})();安裝與依賴
通過 npm 安裝(會自動下載匹配的 Chromium):
npm install puppeteer
優(yōu)勢
直接操控瀏覽器:行為更接近真實(shí)用戶,繞過反爬蟲機(jī)制(需合理使用)。
功能全面:支持網(wǎng)絡(luò)請求攔截、Cookie 管理、模擬設(shè)備(如手機(jī)/平板)等。
社區(qū)活躍:豐富的文檔和第三方工具(如
puppeteer-cluster多任務(wù)優(yōu)化)。
若你需要處理復(fù)雜的網(wǎng)頁交互或動態(tài)內(nèi)容,Puppeteer 是一個(gè)高效且靈活的選擇。
vue中為什么切換動態(tài)組件需要使用shallowRef ?
在 Vue 3 中,使用 shallowRef 來管理動態(tài)組件的切換,主要是出于性能優(yōu)化和避免不必要的深度響應(yīng)式追蹤的考慮。以下是詳細(xì)解釋:
1. ref vs shallowRef 的核心區(qū)別
會對其包裹的值進(jìn)行深度響應(yīng)式轉(zhuǎn)換(遞歸將對象屬性轉(zhuǎn)為響應(yīng)式)。
僅對值的引用變化進(jìn)行響應(yīng)式追蹤,不會深度遞歸轉(zhuǎn)換內(nèi)部屬性。
2. 動態(tài)組件的場景分析
當(dāng)使用 <component :is="currentComponent"> 切換組件時(shí):
- 組件對象本身是穩(wěn)定的
動態(tài)組件的核心操作是替換整個(gè)組件對象(如 currentComponent.value = NewComponent),而不是修改組件對象的內(nèi)部屬性。
- 組件對象可能很大
一個(gè)組件對象(如導(dǎo)入的 Vue 組件)通常包含大量屬性(如 props、methods、生命周期等),如果使用 ref 深度響應(yīng)式化,會帶來額外性能開銷。
- 深度響應(yīng)式可能導(dǎo)致問題
某些組件屬性(如函數(shù)方法)被 Vue 代理后可能產(chǎn)生副作用(如破壞函數(shù)內(nèi)部 this 綁定,或與第三方庫預(yù)期結(jié)構(gòu)沖突)。
3. 為什么 shallowRef 更合適?
- 性能優(yōu)化
避免深度遍歷組件對象的所有屬性,減少不必要的響應(yīng)式代理。
- 符合實(shí)際需求
動態(tài)組件切換只需要響應(yīng)組件引用的變化(整體替換),無需關(guān)心組件內(nèi)部屬性的變化。
- 規(guī)避潛在問題
防止 Vue 對組件對象內(nèi)部屬性(如方法、生命周期鉤子)的深度代理導(dǎo)致意外行為。
4. 示例對比
// 使用 ref(不推薦)
import { ref } from 'vue';
import HeavyComponent from './HeavyComponent.vue';
const currentComponent = ref(HeavyComponent);
// Vue 會深度代理 HeavyComponent 的所有屬性,但實(shí)際只需要引用變化觸發(fā)更新
// 使用 shallowRef(推薦)
import { shallowRef } from 'vue';
const currentComponent = shallowRef(HeavyComponent);
// 僅追蹤 currentComponent.value 的引用變化,高效且安全5. 官方建議
Vue 官方文檔在動態(tài)組件示例中直接使用普通變量(非響應(yīng)式),但在需要響應(yīng)式時(shí)建議使用 shallowRef,明確表示:
“如果你確實(shí)需要響應(yīng)性,可以使用 shallowRef。”
在動態(tài)組件切換場景中,shallowRef 通過避免深度響應(yīng)式轉(zhuǎn)換,在保證功能正確性的同時(shí),提升了性能并規(guī)避了潛在問題。這正是它與 ref 的核心區(qū)別所在。
以上就是Vue3使用exceljs將excel文件轉(zhuǎn)化為html預(yù)覽最佳方案的詳細(xì)內(nèi)容,更多關(guān)于Vue3 exceljs將excel轉(zhuǎn)html預(yù)覽的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue2.0 實(shí)現(xiàn)頁面緩存和不緩存的方式
今天小編就為大家分享一篇Vue2.0 實(shí)現(xiàn)頁面緩存和不緩存的方式,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11
解決vue無法加載文件D:\Program Files\nodejs\node_global\vue.ps1,
這篇文章主要給大家介紹了關(guān)于解決vue無法加載文件D:\Program Files\nodejs\node_global\vue.ps1,因?yàn)樵诖讼到y(tǒng)上禁止運(yùn)行腳本的相關(guān)資料,這個(gè)報(bào)錯是由于在系統(tǒng)上禁止運(yùn)行腳本導(dǎo)致的,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
基于Vue3+西瓜視頻播放器(xgplayer)移動端直播解決方案
在現(xiàn)代前端開發(fā)中,視頻播放功能越來越常見,從在線教育到娛樂平臺都離不開高質(zhì)量的視頻播放器,這篇文章主要介紹了基于Vue3+西瓜視頻播放器(xgplayer)移動端直播的相關(guān)資料,需要的朋友可以參考下2026-02-02
Vue.js中集成Socket.IO實(shí)現(xiàn)實(shí)時(shí)聊天功能
隨著 Web 技術(shù)的發(fā)展,實(shí)時(shí)通信成為現(xiàn)代 Web 應(yīng)用的重要組成部分,Socket.IO 是一個(gè)流行的庫,支持及時(shí)、雙向與基于事件的通信,適用于各種平臺和設(shè)備,本文將介紹如何在 Vue.js 項(xiàng)目中集成 Socket.IO,實(shí)現(xiàn)一個(gè)簡單的實(shí)時(shí)聊天應(yīng)用,需要的朋友可以參考下2024-12-12

