搞定前端 Excel 上传解析、CSV/Excel 导出全场景,避开 MIME 校验、中文乱码、大文件卡顿等 90% 常见坑
📑 文章目录
- 一、为什么要单独讲这个?
- 二、前置知识扫盲
- 三、Excel 上传与解析(xlsx 库)
- 四、完整 Vue 组件示例:Excel 上传解析
- 五、导出 CSV / Excel
- 六、VXE Table 结合使用(可选拓展)
- 七、总结与规范清单
- 八、附录:依赖与版本参考
同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
一、为什么要单独讲这个?
业务里经常有「上传 Excel 解析」和「导出 CSV/Excel」的需求。乍一看很简单,但实际容易踩到:
- 不知道
.xlsx和.xls的区别 - 解析出的中文乱码
- 导出大表时浏览器卡死
- MIME 类型校验导致上传失败
本文从「怎么选、为什么、踩坑在哪」出发,用 Vue + xlsx 做一套可直接复用的示例,顺便把相关基础捋清。
二、前置知识扫盲
2.1 文件上传的本质:File、FileList、FileReader
上传文件时,你实际在操作的是这三类对象:
| 概念 | 说明 |
|---|---|
| File | 表示一个文件,有 name、size、type 等属性 |
| FileList | input[type="file"] 的 files 属性,是 File 的集合 |
| FileReader | 用来读取文件内容,可转为 ArrayBuffer、DataURL、文本等 |
要点:xlsx 需要的是 ArrayBuffer,所以通常用 FileReader.readAsArrayBuffer(),再传给 XLSX.read()。
2.2 常见坑:MIME 类型与 Excel 格式
Excel 常见扩展名:
| 扩展名 | 说明 | 常见 MIME |
|---|---|---|
.xlsx | Excel 2007+,基于 XML | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
.xls | 老格式 | application/vnd.ms-excel |
.csv | 纯文本,逗号分隔 | text/csv |
坑点:
有些环境(如 Linux)给 Excel 文件的 file.type 可能是空字符串,不能只靠 file.type 判断。更稳妥的做法是:
- 用扩展名
.xlsx、.xls、.csv辅助校验 - 或直接交给
xlsx解析,解析失败再提示用户
三、Excel 上传与解析(xlsx 库)
3.1 选库:SheetJS (xlsx)
常用库是 SheetJS 的 xlsx。安装:
npm install xlsx
3.2 核心流程
-
用户选择文件 → 拿到
File对象 -
用
FileReader读成ArrayBuffer -
XLSX.read(buffer, { type: 'array' })得到工作簿 -
XLSX.utils.sheet_to_json()转成 JSON 数组
3.3 基础示例(纯 JS)
import * as XLSX from 'xlsx';
function parseExcel(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const buffer = e.target.result;
const workbook = XLSX.read(buffer, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); // header: 1 表示数组的数组
resolve(data);
} catch (err) {
reject(err);
}
};
reader.onerror = () => reject(reader.error);
reader.readAsArrayBuffer(file);
});
}
-
header: 1:返回[[a1,b1],[a2,b2]],第一行也是数据 -
header: 'A'或不传:第一行当表头,返回[{A: 值1, B: 值2}, ...]
3.4 常见坑与处理
1)表头在第二行
有的模板第一行是标题,第二行才是表头。可以先取前几行判断,或指定 range:
const data = XLSX.utils.sheet_to_json(worksheet, {
range: 1, // 从第 2 行开始(0 基索引)
header: ['A', 'B', 'C'] // 或自定义表头
});
2)日期被转成数字
Excel 日期存的是数字,xlsx 默认不会自动转成 Date。需要自己处理:
// 判断是否为 Excel 日期数字(一般 1 ~ 2958465 之间)
function isExcelDate(num) {
return typeof num === 'number' && num > 0 && num < 2958466;
}
function excelDateToJS(num) {
return XLSX.SSF.parse_date_code(num);
}
3)空行、空列
sheet_to_json 会跳过空行。如果希望保留空行,可以用 sheet_to_json 的 defval 或直接操作 worksheet 的原始数据。
4)大文件
大文件建议:
-
限制文件大小(如 5MB)
-
用 Web Worker 在后台解析,避免阻塞主线程
四、完整 Vue 组件示例:Excel 上传解析
4.1 点击上传
下面是一个可直接复用的 Vue 3 组件(点击选择文件):
<template>
<div class="excel-upload">
<input
ref="fileInput"
type="file"
accept=".xlsx,.xls,.csv"
@change="handleFileChange"
/>
<div v-if="loading">解析中...</div>
<div v-else-if="error">{{ error }}</div>
<table v-else-if="tableData.length" border="1" style="border-collapse: collapse;">
<thead>
<tr>
<th v-for="(cell, i) in tableData[0]" :key="i">{{ cell }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in tableData.slice(1)" :key="rowIndex">
<td v-for="(cell, colIndex) in row" :key="colIndex">{{ cell ?? '-' }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref } from 'vue';
import * as XLSX from 'xlsx';
const fileInput = ref(null);
const loading = ref(false);
const error = ref('');
const tableData = ref([]);
const MAX_SIZE = 5 * 1024 * 1024; // 5MB
const ALLOWED_EXT = ['.xlsx', '.xls', '.csv'];
function getExt(filename) {
const i = filename.lastIndexOf('.');
return i > -1 ? filename.slice(i).toLowerCase() : '';
}
function handleFileChange(e) {
const file = e.target?.files?.[0];
if (!file) return;
// 1. 大小校验
if (file.size > MAX_SIZE) {
error.value = '文件大小不能超过 5MB';
return;
}
// 2. 扩展名校验
if (!ALLOWED_EXT.includes(getExt(file.name))) {
error.value = '请上传 .xlsx、.xls 或 .csv 文件';
return;
}
loading.value = true;
error.value = '';
tableData.value = [];
const reader = new FileReader();
reader.onload = (ev) => {
try {
const buffer = ev.target.result;
const workbook = XLSX.read(buffer, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
tableData.value = data;
} catch (err) {
error.value = '解析失败:' + (err.message || '请确认文件格式正确');
} finally {
loading.value = false;
if (fileInput.value) fileInput.value.value = '';
}
};
reader.onerror = () => {
error.value = '文件读取失败';
loading.value = false;
};
reader.readAsArrayBuffer(file);
}
</script>
要点:
-
用扩展名 + 大小做前端校验
-
使用
header: 1得到二维数组,第一行当表头 -
解析后清空
input.value,方便重复选同一文件 -
异常时给出明确提示
4.2 拖拽上传
除了点击选择文件,很多场景还会用到拖拽上传:用户把文件拖到指定区域后松开鼠标,即可触发解析,无需再点「选择文件」。下面说明如何在前面的基础上增加拖拽能力。
拖拽上传涉及的事件:
| 事件 | 触发时机 | 用途 |
|---|---|---|
dragenter | 文件进入拖拽区域 | 可做高亮提示 |
dragover | 文件在区域内移动 | 必须 preventDefault(),否则 drop 不会触发 |
dragleave | 文件离开区域 | 取消高亮 |
drop | 文件在区域内释放 | 从 event.dataTransfer.files 拿到文件并处理 |
关键点: 在 dragover 和 drop 里都要调用 event.preventDefault(),否则浏览器会尝试在新标签页打开文件,而不是把文件交给你处理。
在原有组件上增加拖拽区域,复用 handleFileChange 的逻辑,只需把「选中的文件」提取成统一方法即可。示例如下:
<template>
<div class="excel-upload">
<!-- 拖拽区域:可点击也可拖拽 -->
<div
class="drop-zone"
:class="{ 'is-dragover': isDragover, 'has-error': error }"
@click="fileInput?.click()"
@dragenter.prevent="isDragover = true"
@dragover.prevent
@dragleave.prevent="isDragover = false"
@drop.prevent="handleDrop"
>
<span v-if="!loading">
{{ isDragover ? '松开即可上传' : '点击或拖拽文件到此处' }}
</span>
<span v-else>解析中...</span>
</div>
<input
ref="fileInput"
type="file"
accept=".xlsx,.xls,.csv"
class="hidden"
@change="handleFileChange"
/>
<div v-if="error" class="error-msg">{{ error }}</div>
<table v-else-if="tableData.length" border="1" style="border-collapse: collapse;">
<!-- 表格结构同上,此处省略 -->
</table>
</div>
</template>
<script setup>
// ... 前面的 ref、常量、getExt 等保持不变 ...
const isDragover = ref(false);
// 统一处理文件:点击和拖拽都走这里
function processFile(file) {
if (!file) return;
if (file.size > MAX_SIZE) {
error.value = '文件大小不能超过 5MB';
return;
}
if (!ALLOWED_EXT.includes(getExt(file.name))) {
error.value = '请上传 .xlsx、.xls 或 .csv 文件';
return;
}
loading.value = true;
error.value = '';
tableData.value = [];
const reader = new FileReader();
reader.onload = (ev) => {
try {
const buffer = ev.target.result;
const workbook = XLSX.read(buffer, { type: 'array' });
const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
const data = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
tableData.value = data;
} catch (err) {
error.value = '解析失败:' + (err.message || '请确认文件格式正确');
} finally {
loading.value = false;
if (fileInput.value) fileInput.value.value = '';
}
};
reader.onerror = () => {
error.value = '文件读取失败';
loading.value = false;
};
reader.readAsArrayBuffer(file);
}
function handleFileChange(e) {
const file = e.target?.files?.[0];
processFile(file);
}
function handleDrop(e) {
isDragover.value = false;
const file = e.dataTransfer?.files?.[0];
processFile(file);
}
</script>
<style scoped>
.drop-zone {
border: 2px dashed #ccc;
padding: 40px;
text-align: center;
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.drop-zone:hover,
.drop-zone.is-dragover {
border-color: #409eff;
background: #ecf5ff;
}
.drop-zone.has-error {
border-color: #f56c6c;
}
.hidden {
display: none;
}
</style>
拖拽部分要点:
@dragover.prevent:阻止默认行为,drop才能正常触发@drop.prevent:同样阻止默认行为e.dataTransfer.files:拖拽时拿到的文件列表,结构和input.files一样isDragover:控制拖拽悬停时的样式,提升可感知性- 点击拖拽区域时用
fileInput?.click()触发原生选择框,与点击上传保持一致
这样,同一个组件既支持点击选择,也支持拖拽上传,解析逻辑共用,维护成本低。
五、导出 CSV / Excel
5.1 导出 CSV(纯前端)
CSV 是纯文本,无需第三方库,直接用 Blob + 下载即可。
function downloadCSV(data, filename = 'export.csv') {
// data: [[表头1, 表头2], [值1, 值2], ...]
const BOM = '\uFEFF'; // 解决 Excel 打开中文乱码
const csv = data
.map(row =>
row
.map(cell => {
const s = String(cell ?? '');
if (s.includes(',') || s.includes('"') || s.includes('\n')) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
})
.join(',')
)
.join('\n');
const blob = new Blob([BOM + csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
要点:
-
加
\uFEFF(BOM)解决 Excel 打开 CSV 中文乱码 -
含逗号、引号、换行的单元格用双引号包裹,内部引号要转义
5.2 导出 Excel(xlsx)
需要多 Sheet、样式时,用 xlsx 更合适。
import * as XLSX from 'xlsx';
function downloadExcel(data, filename = 'export.xlsx', sheetName = 'Sheet1') {
const ws = XLSX.utils.aoa_to_sheet(data); // aoa = array of arrays
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, sheetName);
XLSX.writeFile(wb, filename);
}
data 格式同上:[[表头1, 表头2], [值1, 值2], ...]。
5.3 大数据量导出注意点
-
几万行以上:建议服务端生成,前端只负责下载
-
必须前端导出时:
-
分批构建
data,再一次性aoa_to_sheet -
或用 Web Worker 生成,完成后通知主线程下载
-
考虑只导出当前页或勾选行,减少数据量
-
六、VXE Table 结合使用(可选拓展)
如果你用 VXE Table 做表格,可以:
-
解析后的
tableData直接作为vxe-table的data -
导出时从 VXE 的
$table.getTableData()或当前data生成 CSV/Excel
示例:
<vxe-table :data="tableData" border>
<vxe-column v-for="(col, i) in columns" :key="i" :field="col.field" :title="col.title" />
</vxe-table>
<vxe-button @click="exportExcel">导出 Excel</vxe-button>
exportExcel 中从 tableData 构造 [[...表头], [...行数据]],再调用前面的 downloadExcel 即可。
七、总结与规范清单
| 场景 | 推荐做法 | 常见坑 |
|---|---|---|
| 上传校验 | 扩展名 + 大小,不单靠 file.type | Linux 下 file.type 可能为空 |
| 解析 Excel | 用 xlsx + FileReader.readAsArrayBuffer | 日期是数字、表头不在第一行 |
| 导出 CSV | 加 BOM,处理逗号/引号/换行 | 缺 BOM 导致 Excel 乱码 |
| 导出 Excel | xlsx 的 aoa_to_sheet + writeFile | 大表在前端生成易卡顿 |
| 大文件 | 限制大小 + Worker 解析 | 主线程阻塞 UI |
| 按上面流程实现,日常 90% 的 Excel 导入导出需求都能覆盖,同时避免常见坑。 |
八、附录:依赖与版本参考
{
"dependencies": {
"xlsx": "^0.18.5",
"vue": "^3.x"
}
}
SheetJS 社区版(xlsx)对常见读写已足够;需要复杂样式、加密等可考虑其付费版。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~