Excel 上传解析 + 导出实战:Vue+xlsx 避坑指南|Vue生态精选

4 阅读9分钟

搞定前端 Excel 上传解析、CSV/Excel 导出全场景,避开 MIME 校验、中文乱码、大文件卡顿等 90% 常见坑

📑 文章目录


同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。

一、为什么要单独讲这个?

业务里经常有「上传 Excel 解析」和「导出 CSV/Excel」的需求。乍一看很简单,但实际容易踩到:

  • 不知道 .xlsx.xls 的区别
  • 解析出的中文乱码
  • 导出大表时浏览器卡死
  • MIME 类型校验导致上传失败

本文从「怎么选、为什么、踩坑在哪」出发,用 Vue + xlsx 做一套可直接复用的示例,顺便把相关基础捋清。

⬆ 返回目录

二、前置知识扫盲

2.1 文件上传的本质:File、FileList、FileReader

上传文件时,你实际在操作的是这三类对象:

概念说明
File表示一个文件,有 namesizetype 等属性
FileListinput[type="file"]files 属性,是 File 的集合
FileReader用来读取文件内容,可转为 ArrayBuffer、DataURL、文本等

要点:xlsx 需要的是 ArrayBuffer,所以通常用 FileReader.readAsArrayBuffer(),再传给 XLSX.read()

⬆ 返回目录

2.2 常见坑:MIME 类型与 Excel 格式

Excel 常见扩展名:

扩展名说明常见 MIME
.xlsxExcel 2007+,基于 XMLapplication/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)

常用库是 SheetJSxlsx。安装:

npm install xlsx

⬆ 返回目录

3.2 核心流程

  1. 用户选择文件 → 拿到 File 对象

  2. FileReader 读成 ArrayBuffer

  3. XLSX.read(buffer, { type: 'array' }) 得到工作簿

  4. 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_jsondefval 或直接操作 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 拿到文件并处理

关键点:dragoverdrop 里都要调用 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-tabledata

  • 导出时从 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.typeLinux 下 file.type 可能为空
解析 Excel用 xlsx + FileReader.readAsArrayBuffer日期是数字、表头不在第一行
导出 CSV加 BOM,处理逗号/引号/换行缺 BOM 导致 Excel 乱码
导出 Excelxlsx 的 aoa_to_sheet + writeFile大表在前端生成易卡顿
大文件限制大小 + Worker 解析主线程阻塞 UI
按上面流程实现,日常 90% 的 Excel 导入导出需求都能覆盖,同时避免常见坑。

⬆ 返回目录

八、附录:依赖与版本参考


{
  "dependencies": {
    "xlsx": "^0.18.5",
    "vue": "^3.x"
  }
}

SheetJS 社区版(xlsx)对常见读写已足够;需要复杂样式、加密等可考虑其付费版。

⬆ 返回目录


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~