excel-read-plugin
v1.0.0
Published
高性能 Excel(.xlsx) 读取插件。采用 XML 流式解析,内置 @zip.js/zip.js 解包 OOXML 部件,支持数十万行大表内存友好读取、单元格类型与日期时区精确转换、合并单元格识别、按列索引或标题映射 JSON 字段。提供与框架无关的核心类 ExcelReader,以及 Vue3 Composition API 封装。
Maintainers
Readme
excel-read-plugin
高性能 Excel(.xlsx) 与 CSV 读取插件。.xlsx 采用 SAX 流式 XML 解析 + @zip.js/zip.js 解包 OOXML 部件,支持数十万行大表内存友好读取、单元格类型与日期时区精确转换、合并单元格识别、按列索引 / 标题映射 JSON 字段,并提供一键 CSV 导入统一管线。
核心亮点
- 🚀 流式解析:
stream()异步生成器逐行产出,超大表不驻留内存 - 📄 双格式:
.xlsx与.csv统一管线,自动识别格式(sourceFormat: 'auto') - 🧩 字段映射:按
index(列索引)或header(标题)映射字段 - 🔬 复杂表头:
headerRows > 1合并多行表头为「一级/二级」组合标题 - 🔄 自定义转换:
transform为指定字段定义转换逻辑(如把YYYY-MM-DD文本转为时间戳,是|否枚举转1|0),支持异步函数 - 📍 精确校验定位:输出行携带 Excel 1 基行号
_rowIndex,meta.columnMap标注「输出字段 ↔ 原始列」映射关系 - 🔗 合并单元格:
fill/mark/none三种处理策略 - ⏱️ 日期时区:1900/1904 日期系统自动识别,
local/utc时区策略 - 🎯 实时进度:
onProgress逐行回调 + 大数据自动让出事件循环,进度条实时刷新不卡顿 - 🧱 框架无关:核心类
ExcelReader可在任意环境使用,另有 Vue3useExcelRead封装与插件全局 API
安装
npm install excel-read-plugin
vue为可选 peerDependency:仅在 Vue 项目中使用useExcelRead/ 插件全局 API 时需要,且由宿主项目提供。
浏览器 / Node 引入
// ESM(推荐)
import { ExcelReader } from 'excel-read-plugin'
// UMD(<script> 直接引入)
// 全局变量:ExcelPlugin快速开始
import { ExcelReader } from 'excel-read-plugin'
const reader = new ExcelReader()
// 全量读取第一个工作表
// type 默认 auto:自动读取表格单元格原生类型(数字→number、文本→string、日期样式→Date、布尔→boolean),无需手动指定
const res = await reader.read(file, {
columns: [
{ header: '姓名', field: 'name' },
{ header: '年龄', field: 'age' },
{ header: '生日', field: 'birthday' },
],
})
console.log(res.rows) // [{ name, age, birthday, _rowIndex }, …]
console.log(res.headers) // 标题行原始值
console.log(res.meta) // 元信息:activeSheet / columnMap / mergedCells …
console.log(res.totalTime) // 解析耗时(ms)读取自动识别类型,非导出:插件关注表格导入场景,默认按单元格原生类型输出(数字/文本/日期/布尔),无需在读取配置里手动
type。仅当确实需要强制转换时才显式指定type,例如把文本"123"强行读成number、或把文本日期YYYY-MM-DD读成Date。
// 需要强制转换时再显式指定 type(可选)
await reader.read(file, {
columns: [
{ header: '年龄', field: 'age', type: 'number' },
{ header: '生日', field: 'birthday', type: 'date', dateFormat: 'YYYY-MM-DD' },
],
})CSV 读取
.xlsx 与 .csv 走同一套映射 / 类型转换 / 行号 / 列映射管线,行为完全一致。默认 sourceFormat: 'auto' 按文件前若干字节自动识别(zip 魔数 → Excel,其余文本 → CSV);也可显式指定。
import { ExcelReader } from 'excel-read-plugin'
// 自动识别:.xlsx 或 .csv 均可
const csvRes = await new ExcelReader().read(csvFile, {
sourceFormat: 'auto',
columns: [
{ header: '姓名', field: 'name' },
{ header: '年龄', field: 'age' },
],
})
// 显式指定格式(当文件名 / 扩展名不匹配时尤其有用)
await new ExcelReader().read(blob, { sourceFormat: 'csv' })CSV 说明
- 分隔符自动检测(逗号 / 分号 / Tab / 管道),也支持带引号、转义引号与跨行字段
- 首行 BOM 自动剥离;空行自动跳过
- 网络 CSV 单元格都以文本读取,类型需依内容判断(CSV 无样式 / 日期系统 / 合并单元格);必要时可用
type/transform指定 - 超出「文件头部无法识别」的二进制(非 zip 且含 NUL)将抛
INVALID_FILE
复杂表头(headerRows)
当标题由多行组成(常见于「分组表头」)时,设置 headerRows > 1,插件会把多行标题逐列合并为「一级/二级」组合标题,随后即可按该组合标题映射字段。
// 表头两行:第1行 [成员资料, 成员资料, 状态],第2行 [姓名, 年龄, 是否在职]
const res = await new ExcelReader().read(blob, {
headerRow: 1,
headerRows: 2, // 两行合并 → 组合标题
dataStartRow: 3, // 表头占两行,数据从第 3 行起
columns: [
{ header: '成员资料/姓名', field: 'name' },
{ header: '成员资料/年龄', field: 'age', type: 'number' },
{ header: '状态/是否在职', field: 'active' },
],
})
console.log(res.headers) // ['成员资料/姓名', '成员资料/年龄', '状态/是否在职']API 总览
核心类 ExcelReader
与框架无关,可在 Vue / React / 原生 JS / Node 中使用。
| 方法 | 说明 | 返回 |
| --- | --- | --- |
| read(source, options) | 全量读取并累计为数组(带内存阈值防护) | Promise<ParseResult> |
| stream(source, options) | 流式逐行产出(AsyncGenerator) | AsyncGenerator<ExcelRow> |
| cancel() | 取消当前读取 | void |
| reset() | 重置 abort 信号(供下一次读取) | void |
输入源 source 支持:File | Blob | ArrayBuffer | Response | string
组合式函数 useExcelRead
Vue3 Composition API 封装,提供响应式状态,可直接绑定模板。
import { useExcelRead } from 'excel-read-plugin'
const { read, stream, cancel, reset, status, progress, error, rows, meta, isLoading } = useExcelRead()
await read(file, { columns: [{ header: '姓名', field: 'name' }] })| 返回值 | 类型 | 说明 |
| --- | --- | --- |
| status | Ref<ExcelReadStatus> | idle / parsing / reading / success / error / user-cancelled 等 |
| progress | Ref<ExcelProgress> | 已处理行数、预估总数、百分比、耗时 |
| error | Ref<ExcelErrorShape \| null> | 结构化错误 |
| rows | Ref<ExcelRow[]> | 读取结果行 |
| meta | Ref<ReadMeta \| null> | 元信息 |
| isLoading | Ref<boolean> | 是否正在读取 |
| read(source, options?) | 全量读取,返回 Promise<ParseResult> |
| stream(source, options?) | 流式读取,累积到 rows |
| cancel() / reset() | 取消 / 重置 |
Vue 插件全局 API
// main.js
import { createApp } from 'vue'
import ExcelPlugin from 'excel-read-plugin'
createApp(App).use(ExcelPlugin, { defaultOptions: { mergeStrategy: 'fill' } }).mount('#app')
// 任意组件内
const { proxy } = getCurrentInstance()
const api = proxy.$excel.create() // 等价 $excelRead.create(),即 useExcelRead配置项(ExcelReadOptions)
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| sheet | number \| string | 否 | 0 | 目标工作表:索引(0 基)或名称 |
| sheets | (number \| string)[] | 否 | 全部工作表 | 多工作表读取(readSheets 用):目标工作表索引/名称列表,顺序即结果顺序,重复自动去重 |
| headerRow | number | 否 | 1 | 标题行起始(1 基) |
| headerRows | number | 否 | 1 | 标题行跨越的行数(复杂表头用 >1),>1 时合并为组合标题「一级/二级」 |
| sourceFormat | 'auto' \| 'xlsx' \| 'csv' | 否 | 'auto' | 输入格式策略:auto 自动探测、xlsx 强制 Excel、csv 强制 CSV |
| dataStartRow | number | 否 | headerRow + headerRows | 数据起始行(1 基) |
| dataEndRow | number | 否 | 工作表末尾 | 数据结束行(1 基) |
| columns | ColumnMapping[] | 否 | 输出位置数组 | 字段映射配置(见下) |
| mergeStrategy | 'fill' \| 'mark' \| 'none' | 否 | 'fill' | 合并单元格处理策略 |
| includeRaw | boolean | 否 | false | 附加合并元信息 _mergedRef / _isMergedTopLeft(mark 时自动开启) |
| memoryThreshold | number | 否 | 512MB | 全量读取内存阈值,超过抛 OUT_OF_MEMORY |
| maxDecompressedBytes | number | 否 | 1GB | 单个部件解压体积上限(zip 炸弹防护),超过抛 FILE_TOO_LARGE |
| legacy1904 | boolean \| 'auto' | 否 | 'auto' | 1904 日期系统识别;true 强制 1904、false 强制 1900 |
| dateTimezone | 'local' \| 'utc' | 否 | 'local' | 日期时区策略 |
| allowEmpty | boolean | 否 | false | 空表是否抛 EMPTY_SHEET |
| transform | (row, ctx) => row \| null | 否 | — | 行级后处理 / 过滤(返回 null 跳过该行) |
| onRow | (row, ctx) => void | 否 | — | 逐行回调 |
| onProgress | (p) => void | 否 | — | 进度回调;未指定 dataEndRow 时 totalRows 恒 0、percent 恒 0(见下方说明) |
| includeRowIndex | boolean | 否 | true | 映射行是否附加 _rowIndex 行号 |
字段映射配置(ColumnMapping)
按列索引 ColumnByIndex(0 基)
{ index: 1, field: 'age', type: 'number' }按标题 ColumnByHeader
{ header: '年龄', field: 'age', type: 'number' }公共字段
| 字段 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| field | string | 是 | — | 输出 JSON 字段名 |
| type | 'string' \| 'number' \| 'boolean' \| 'date' \| 'auto' | 否 | 'auto' | 目标数据类型 |
| required | boolean | 否 | true | 必填列;header 模式找不到标题时抛 HEADER_NOT_FOUND |
| default | CellValue | 否 | — | 值为空 / 转换失败时的默认值 |
| dateFormat | string | 否 | — | 当 type = 'date' 时输出格式化的字符串(如 YYYY-MM-DD) |
| transform | FieldTransform | 否 | — | 自定义转换函数(见下) |
自定义转换函数(transform)
对指定字段在标准类型转换之后做进一步定制处理。例如把读取到的 YYYY-MM-DD 时间字符串转换为时间戳:
import { ExcelReader } from 'excel-read-plugin'
// 定义转换函数:value 为标准转换后的值,ctx 提供精确定位信息
function toTimestamp(value, ctx) {
if (value == null || value === '') return value
const ts = new Date(String(value) + 'T00:00:00Z').getTime()
return Number.isNaN(ts) ? value : ts
}
const res = await new ExcelReader().read(blob, {
columns: [
{ header: '姓名', field: 'name', type: 'string' },
{
header: '入职日期(文本)',
field: 'joinTs',
type: 'string', // 先完成标准转换
transform: toTimestamp, // 再做定制转换 → 时间戳
},
],
})
// res.rows[0].joinTs === 1705276800000transform 适用于任意数据类型——如需把中文枚举 是|否 归一化为 1/0:
const enum01 = (v) => (v === '是' ? 1 : v === '否' ? 0 : v)
const res = await new ExcelReader().read(blob, {
columns: [
{ header: '姓名', field: 'name' },
{ header: '是否在职(是|否)', field: 'isActive', transform: enum01 }, // 是 → 1,否 → 0
],
})针对「多枚举值」的可扩展写法:用工厂生成即可,例如
const gender = (v) => ({ 男: 'M', 女: 'F' })[v] ?? v,规则可集中在一个映射对象中自由扩展。
要点
- 支持返回
Promise(异步转换),可用async函数 - 转换函数抛错时,会自动包装为带定位信息的
TYPE_CONVERSION错误 - 适用于任意数据类型转换:时间字符串 → 时间戳 / 文本 → 数值 / 单位解析 / 字典映射等
FieldTransformContext 上下文
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| row | number | 数据行号(Excel 1 基) |
| col | number | 列索引(0 基) |
| ref | string | 单元格引用,如 "B3" |
| field | string | 输出 JSON 字段名 |
| header | string | 标题(header 模式有效) |
| sheet | string | 工作表名 |
| rawValue | unknown | 转换前的原始单元格值 |
| legacy1904 | boolean | 是否使用 1904 日期系统 |
| dateTimezone | DateTimezone | 日期时区策略 |
行号与字段→列映射(校验定位)
映射行默认附加 Excel 1 基行号 _rowIndex,配合 meta.columnMap 即可精确定位并标注校验错误:
const res = await new ExcelReader().read(blob, {
columns: [
{ header: '姓名', field: 'name', required: true },
{ header: '年龄', field: 'age', type: 'number' },
],
})
// 1) 每行携带行号
for (const row of res.rows) console.log(row._rowIndex, row.name, row.age)
// 2) meta.columnMap 标注输出字段 → 原始列
// [{ field:'name', col:0, header:'姓名', source:'header' }, …]
const ageCol = res.meta.columnMap.find((m) => m.field === 'age')
// 3) 校验:用 rowIndex + col 精确定位并标注
for (const row of res.rows) {
if (Number(row.age) < 18) {
console.log(`第 ${row._rowIndex} 行 · 列「${ageCol.header}」值 ${row.age} 不合法`)
}
}meta.columnMap 条目结构:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| field | string | 输出 JSON 字段名 |
| col | number | 0 基列索引 |
| header | string | 标题(header 模式)或 undefined(index 模式) |
| source | 'index' \| 'header' | 映射方式 |
合并单元格
await reader.read(blob, {
columns: [{ header: '部门', field: 'dept' }],
mergeStrategy: 'fill', // 默认:区域沿顶左值填充
})| 策略 | 说明 |
| --- | --- |
| fill(默认) | 区域内非顶左单元格沿「顶左值」填充 |
| mark | 不填充值,附加 _mergedRef / _isMergedTopLeft 元信息(可配合 includeRaw) |
| none | 不处理合并 |
合并区域详情见返回结果 meta.mergedCells。
流式读取大表
const reader = new ExcelReader({
onProgress: (p) => console.log(p.percent, p.processedRows),
})
let count = 0
for await (const row of reader.stream(bigBlob, {
columns: [{ header: '名称', field: 'name' }, { header: '分数', field: 'score', type: 'number' }],
})) {
count++ // 每产出一行即处理,内存友好
}进度说明:
progress始终提供processedRows(已处理行数)与elapsedTime(耗时)。 仅当指定了dataEndRow(已知总行数)时totalRows与percent才有意义;对未知行数的开放读取,totalRows恒为 0、percent恒为 0 是预期行为,请以processedRows + elapsedTime呈现进度。
多工作表读取(readSheets / sheets)
除默认读取首个工作表(read/stream)与指定单个工作表(sheet 选项)外,插件提供
一次读取多张工作表的 readSheets() 与「仅列出工作表」的 sheets()(只解析 workbook,
不读取任何数据)。
const reader = new ExcelReader()
// ① 仅列出工作表(不读数据),供界面先展示 Sheet 列表再勾选
const { sheets, date1904 } = await reader.sheets(xlsxFile)
console.log(sheets) // [{ name, path, index }, ...]
// ② 一次读取全部工作表
const all = await reader.readSheets(xlsxFile)
console.log(all.totalTime) // 全部工作表总耗时(毫秒)
all.sheets.forEach((s) => {
console.log(s.meta.activeSheet.name, s.rows)
})
// ③ 只读指定的多张工作表(索引 / 名称混用,顺序即结果顺序,重复自动去重)
const two = await reader.readSheets(xlsxFile, {
sheets: [0, '数据明细'], // 每张表共用同一套读取配置(headerRow/columns 等)
})
console.log(two.sheets[0].rows)返回结构
MultiSheetResult| 字段 | 类型 | 说明 | | --- | --- | --- | |
sheets|ParseResult[]| 逐张工作表的读取结果,顺序与传入的sheets(或文件内顺序)一致 | |totalTime|number| 全部工作表读取总耗时(毫秒) |其中每张工作表为完整
ParseResult(含rows/headers/meta.activeSheet/columnMap/totalTime), 可独立展示与消费。仅支持.xlsx;对 CSV 调用readSheets会抛CONFIG_ERROR。
错误处理
所有错误统一为 ExcelError,携带稳定错误码与定位信息:
import { ExcelReader, ExcelErrorCode } from 'excel-read-plugin'
try {
await new ExcelReader().read(badBlob, { columns: [{ header: '年龄', field: 'age', type: 'number' }] })
} catch (err) {
switch (err.code) {
case ExcelErrorCode.INVALID_FILE: alert('无效的 xlsx 文件'); break
case ExcelErrorCode.TYPE_CONVERSION:
alert(`第 ${err.detail.row} 行 · 列「${err.detail.header}」值 "${err.detail.value}" 无法转 number`)
break
case ExcelErrorCode.HEADER_NOT_FOUND: alert('找不到标题列:' + err.detail.header); break
case ExcelErrorCode.EMPTY_SHEET: alert('工作表无可用数据'); break
default: alert(err.message)
}
}错误码
| 错误码 | 触发场景 |
| --- | --- |
| INVALID_FILE | 非 .xlsx(OOXML)文件 |
| PARSE_ERROR | XML / zip 解包解析失败 |
| SHEET_NOT_FOUND | 指定的工作表不存在 |
| EMPTY_SHEET | 数据区间无内容(allowEmpty 未开启时) |
| HEADER_NOT_FOUND | header 模式未找到指定标题 |
| TYPE_CONVERSION | 类型转换失败(detail 含定位信息,或自定义 transform 抛错) |
| CONFIG_ERROR | 读取配置有误 |
| OUT_OF_MEMORY | 数据量超过 memoryThreshold,建议用 stream |
| USER_CANCELLED | 用户取消读取 |
输出结果结构(ParseResult)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| rows | ExcelRow[] | 行数据(columns 模式为对象数组,缺省为位置数组) |
| headers | CellValue[] | 标题行原始值 |
| meta.sheets | SheetInfo[] | 工作表列表 |
| meta.activeSheet | SheetInfo | 实际读取的工作表 |
| meta.headers | CellValue[] | 标题行原始值 |
| meta.mergedCells | MergeCellInfo[] | 合并单元格列表 |
| meta.columnMap | ColumnMapEntry[] | 输出字段 → 原始列映射 |
| meta.date1904 | boolean | 实际使用的日期系统 |
| totalTime | number | 解析耗时(ms) |
常见问题(FAQ)
Q1:为什么读取 .xls 失败?
本插件支持 .xlsx(OOXML)与 .csv 两种格式,不支持二进制旧格式 .xls。.xls 请先另存为 .xlsx 或 .csv。
Q2:大表读取会不会爆内存?
全量 read() 带内存阈值防护(默认 512MB),超限抛 OUT_OF_MEMORY。数十万行以上请改用 stream() 流式读取。大数据用 CSV / Excel 均会周期性让出事件循环,onProgress 进度可实时刷新、不阻塞 UI。
Q3:日期总是差一天?
默认 dateTimezone: 'local' 按墙上时间处理。若要与 Date.toISOString() 一致,请设置 dateTimezone: 'utc'。另可强制指定 legacy1904 应对老模板。
Q4:找不到任何一行数据?
检查 headerRow / headerRows / dataStartRow / dataEndRow 是否符合目标表格实际结构(涉及「分组表头」时需正确设置 headerRows);让空表合法可设置 allowEmpty: true。
Q5:如何关闭行号 _rowIndex?
设置 includeRowIndex: false。
Q6:自定义 transform 里如何拿到行列定位?
通过 transform 的第二个参数 ctx(FieldTransformContext),包含 row / col / ref / field / header 等。
贡献指南
欢迎提交 issue 与 PR。开发流程:
# 安装依赖
npm install
# 类型检查
npm run typecheck
# 单元测试
npm run test:run
# 构建库产物(ESM + UMD + d.ts)
npm run build
# 启动本地演示(http://localhost:5177)
npm run demo:dev提交规范
- 功能模块位于
src/core,Vue 封装位于src/composables,统一入口src/index.ts - 每个核心函数配套单元测试(
tests/*.test.ts),示例数据由tests/helpers/create-test-xlsx.ts生成 - 新增 API 需同步更新本文档参数表与示例
- 保持现有 API 向后兼容;破坏性变更需在 PR 说明
目录结构
excel-read-plugin/
├── src/
│ ├── index.ts # 统一入口(核心类 / useExcelRead / 插件 / 类型 / 常量)
│ ├── types.ts # 类型定义
│ ├── constants.ts # 常量 / 默认配置 / 错误消息
│ ├── core/ # 框架无关核心
│ │ ├── index.ts # ExcelReader 编排器(read / stream,含 CSV 分流)
│ │ ├── xlsx-zip.ts # OOXML 解包与部件流式提取
│ │ ├── csv.ts # CSV 识别 / 分隔符检测 / 分词(复用同一映射管线)
│ │ ├── xml-sax.ts # SAX XML 解析器
│ │ ├── cell-parser.ts # 单元格解析
│ │ ├── type-converter.ts # 类型 / 日期 / 时区转换
│ │ ├── mapper.ts # 字段映射 / 自定义 transform / 行号 / 列映射
│ │ ├── styles.ts # 样式与日期格式识别
│ │ ├── shared-strings.ts
│ │ ├── workbook.ts
│ │ ├── merge-cells.ts # 合并单元格解析
│ │ ├── memory-manager.ts
│ │ └── errors.ts # 结构化错误
│ └── composables/
│ └── useExcelRead.ts # Vue3 Composition API 封装
├── tests/ # 单元测试
├── demo/ # Vue3 演示项目(覆盖全部 API)
└── dist/ # 构建产物