npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

excel-read-plugin

v1.0.0

Published

高性能 Excel(.xlsx) 读取插件。采用 XML 流式解析,内置 @zip.js/zip.js 解包 OOXML 部件,支持数十万行大表内存友好读取、单元格类型与日期时区精确转换、合并单元格识别、按列索引或标题映射 JSON 字段。提供与框架无关的核心类 ExcelReader,以及 Vue3 Composition API 封装。

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 可在任意环境使用,另有 Vue3 useExcelRead 封装与插件全局 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 === 1705276800000

transform 适用于任意数据类型——如需把中文枚举 是|否 归一化为 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/                   # 构建产物

许可证

MIT