前言
在企业应用中,我们时常会遇到需要上传并展示 Excel 文件的需求,以实现文件内容的在线预览。经过一番探索与尝试,笔者最终借助 exceljs
这一库成功实现了该功能。本文将以 Vue 3 为例,演示如何实现该功能,代码示例可直接复制运行,希望能为大家在处理类似问题时提供新的思路和解决方案。
技术难点
基础单元格和合并单元格的混合处理
文字样式和背景样式的读取映射
富文本内容格式的处理
excel文件展示

实际实现预览效果

核心代码
exceljs提供了合并区域的数据,我们只需要根据合并区域去判断什么时候该合并,就能很好的实现基础单元格和合并单元格的混合绘制
javascript
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;
}
}
完整源码
javascript
<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 表格内容
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
创建一个新的工作簿并添加一些数据:
javascript
const ExcelJS = require('exceljs');
// 创建一个新的工作簿
let workbook = new ExcelJS.Workbook();
let worksheet = workbook.addWorksheet('测试工作表');
// 添加一行数据
worksheet.addRow(['姓名', '年龄', '邮箱']);
worksheet.addRow(['张三', 28, '[email protected]']);
worksheet.addRow(['李四', 23, '[email protected]']);
// 保存工作簿到文件
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 或图片存档。
基础示例
javascript
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):
bash
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. 示例对比
javascript// 使用 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 的核心区别所在。