@senlinz/import-export
Version:
import/export excel core
252 lines (192 loc) • 10.6 kB
Markdown
# @senlinz/import-export
面向浏览器的高阶 Excel 模板生成、导出与导入 API。
[English](./README.md)
## 安装
```bash
pnpm add @senlinz/import-export
```
## WASM 加载
- `importExcel`、`importExcelDynamic`、`exportExcel`、`fromExcel`、`fromExcelDynamic`、`toExcel`、`downloadExcelTemplate`、`generateExcelTemplate` 会在首次调用时自动加载并初始化内置 WASM 资源。
- 在 Vite 和其他浏览器 ESM bundler 中,使用高阶 core 包时不再需要手动调用 `initializeWasm(...)` 或自己处理 `?url`。
- 如果你需要自行管理 WASM 模块,请直接使用 `@senlinz/import-export-wasm`,而不是这个高阶包。
```ts
import { toExcel } from '@senlinz/import-export';
const workbook = await toExcel(
{
name: 'TomAndJerry',
columns: [{ key: 'name', name: 'Name', dataType: 'text' }],
},
[{ name: 'Tom' }],
);
```
## 支持的 API
### 错误消息多语言
为了兼容现有使用方式,错误消息默认仍为英文。在 `ExcelDefinition` 或 `DynamicExcelImportOptions` 中设置 `locale: 'zh'` 或 `locale: 'zh-CN'` 后,会使用内置中文错误消息。
你也可以按稳定错误码覆盖消息,支持字符串模板或函数。函数会收到错误码、规范化后的语言、默认消息、原始 cause 以及结构化参数。
```ts
import { fromExcel, ImportExportError } from '@senlinz/import-export';
const definition = {
name: 'LocalizedImport',
locale: 'zh-CN',
columns: [{ key: 'name', name: '姓名', dataType: 'text' }],
errorMessages: {
HEADER_MISMATCH: ({ params }) => `表头错误:${params.cell} 需要 ${params.expected},实际是 ${params.actual}`,
INVALID_DATA_TYPE: '列 {columnKey} 的类型 {dataType} 不支持,可选值:{supportedDataTypes}',
},
};
try {
await fromExcel(definition, workbookBytes);
} catch (error) {
if (error instanceof ImportExportError) {
console.log(error.code, error.params, error.message);
}
}
```
包内导出 `ImportExportError`、`ValidationError`、`ImportError`、`ExportError`、`WasmInitError`。每个错误都包含 `code`、`params`、`locale` 和 `cause`。
### 稳定的 definition 字段
- `name`:模板 / 导出下载时使用的文件名
- `sheetName`:模板 / 导出时使用的工作表名称;schema 导入会读取第一个工作表
- `columns`:表头、校验和行映射使用的稳定列定义
- `author`:可选的工作簿作者元数据
- `createTime`:可选的工作簿创建时间,支持 `Date` 或日期字符串
- `maxFileSizeBytes`:浏览器上传的字节大小上限(默认 25 MiB),在读取前先校验
- `title`、`titleHeight`、`titleFormat`:可选的合并标题行及其高度、样式
- `defaultRowHeight`、`headerRowHeight`:导出时数据行和表头行的行高
- `dx`、`dy`:表头写入前的横向、纵向偏移量
- `isHeaderFreeze`:冻结表头区域,滚动时仍保持列标题可见
- `progressCallback`:长时间导入 / 导出过程中的进度回调
- `imageFetcher`:导出 `image` 列时必需的图片数据解析回调
- `escapeFormulas`:导出时自动转义类似公式的文本,默认开启,可显式关闭以允许公式
- `locale`:内置错误消息语言(`en`、`zh` 或 `zh-CN`,默认 `en`)
- `errorMessages` / `messages`:按稳定错误码自定义错误消息
### 稳定的列字段
- `key`:唯一的程序字段名
- `name`:工作表中显示的列表头
- `width`:导出列宽
- `note`:附加在表头单元格上的备注 / 注释
- `dataType`:标量单元格类型
- `allowedValues`:单元格允许值的校验列表
- `parent`:多级表头时的父级表头 key
- `format`:列的基础单元格格式
- `valueFormat`:导出值的直接或条件格式覆盖
- `dataGroup`:嵌套导出数据使用的逻辑分组标识
- `dataGroupParent`:嵌套导出数据使用的父级分组标识
### 支持的 `dataType`
- `text`:默认文本类型
- `number`:有限数值
- `date`:导出时支持 `Date` 实例或日期字符串
- `image`:通过 `imageFetcher` 解析图片 URL 或标识
## 导入 / 导出流程
```ts
import { downloadExcelTemplate, exportExcel, importExcel } from '@senlinz/import-export';
const definition = {
name: 'TomAndJerry',
sheetName: 'sheet1',
columns: [
{ key: 'name', name: 'Name', dataType: 'text', width: 20, note: 'required' },
{ key: 'age', name: 'Age', dataType: 'number', width: 10 },
{ key: 'birthday', name: 'Birthday', dataType: 'date', width: 18 },
{ key: 'category', name: 'Category', allowedValues: ['Cat', 'Mouse'] },
{ key: 'image', name: 'Image', dataType: 'image', width: 10 },
],
imageFetcher: async (url: string) => new Uint8Array(await (await fetch(url)).arrayBuffer()),
};
await downloadExcelTemplate(definition);
await exportExcel(definition, [
{ name: 'Tom', age: 12, birthday: '2024-11-01 00:00:00', category: 'Cat', image: '/Tom.jpg' },
{ name: 'Jerry', age: null, birthday: null, category: 'Mouse', image: '/Jerry.png' },
]);
const rows = await importExcel(definition);
```
## 动态导入
当你不想预先定义 `columns`,并希望直接使用浏览器文件选择框时,可使用 `importExcelDynamic(options?)`。
```ts
import { importExcelDynamic } from '@senlinz/import-export';
const result = await importExcelDynamic({
sheetName: 'sheet1',
headerRow: 1,
});
```
当你已经拿到工作簿字节数据时,可继续使用 `fromExcelDynamic(buffer, options?)`。
```ts
import { fromExcelDynamic } from '@senlinz/import-export';
const result = await fromExcelDynamic(fileBytes, {
sheetName: 'sheet1',
headerRow: 1,
});
```
- `importExcelDynamic(...)` 依赖 DOM / 浏览器文件上传 API。
- `sheetName` 可选,未提供或不存在时会回退到第一个工作表。
- `headerRow` 可选,使用从 `1` 开始的行号,默认会自动选择首个非空行。
- `maxFileSizeBytes` 可选,与 schema 导入相同,默认 25 MiB。
- `locale` 与 `errorMessages` 可用于自定义动态导入错误。
- 返回值结构为 `{ sheetName, headers, rows }`。
- 动态导入要求选中的表头行中表头名称非空且唯一。
## 导入行为
- 表头必须与 `columns[].name` 完全一致。
- Schema 导入会读取工作簿中的第一个工作表。
- `number` 列导入后返回数字。
- 空的 `number` / `date` 单元格导入后返回 `null`。
- `date` 列导入后返回格式化字符串。
- 非法或不完整的 schema 会在读取工作簿前立即失败。
- 浏览器上传会在读取前按 `maxFileSizeBytes` 校验文件大小(默认 25 MiB)。
- 无 schema 导入时,可使用 `importExcelDynamic(...)`(浏览器上传)或 `fromExcelDynamic(...)`(字节缓冲)。
## 导出行为
- `null` 和 `undefined` 会导出为空单元格。
- 数字列会拒绝非有限值。
- 日期列支持 `Date` 实例或日期字符串。
- 文本列默认会转义看起来像公式的值;如需导出公式,可设置 `escapeFormulas: false`。
- 分组导出对象必须使用 `{ value?, children: [...] }` 结构。
- 图片列必须提供 `imageFetcher`。
## 带 URL 校验的 imageFetcher
为 `image` 列使用 `imageFetcher` 时,应先校验 URL 再发起请求,避免访问不可信来源。下面的示例只允许 HTTPS,并检查 HTTP 响应状态:
```typescript
const imageFetcher = async (url: string): Promise<Uint8Array> => {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
throw new Error(`Refusing to fetch image from non-HTTPS URL: ${url}`);
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch image: ${response.status} ${response.statusText}`);
}
const buffer = await response.arrayBuffer();
return new Uint8Array(buffer);
};
```
在服务端场景中,还应限制可信域名,并拒绝私有或内网地址。完整的图片拉取信任模型和额外建议见 [SECURITY.md](../../SECURITY.md)。
## 已支持的高级能力
以下高级能力已纳入公开 API:
- 合并标题行
- 冻结表头
- 列备注
- 下拉候选值
- 条件 `valueFormat`
- 使用 `dataGroup` / `dataGroupParent` 的嵌套分组导出
- 通过 `imageFetcher` 导出图片
## 浏览器 / 运行时支持
- 主要面向浏览器 ESM 运行时。
- `downloadExcelTemplate`、`exportExcel`、`importExcel`、`importExcelDynamic` 依赖 DOM / 浏览器 API。
- 当运行时提供兼容的浏览器全局对象时,也可以使用 `fromExcel`、`fromExcelDynamic`、`toExcel`、`generateExcelTemplate`。
- core 包会在首次执行工作簿操作前,异步初始化构建产物中的内置 WASM 资源。
## 示例
- [基础浏览器示例](./examples/basic-browser.html)
- [分组导出示例](./examples/grouped-export.html)
- [Definition 校验示例](./examples/definition-errors.html)
这些示例由 [`./tests/import-export.spec.ts`](./tests/import-export.spec.ts) 中的 Playwright 用例覆盖。
## 测试工具
包内导出 `testUtils` 命名空间,供消费者测试套件使用:
```ts
import { testUtils } from '@senlinz/import-export';
```
> **警告:** `testUtils` 仅用于测试。这些 helper 是内部实现细节,**不提供稳定性保证**,可能在任意版本中变更或移除。
| Helper | 说明 |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `normalizeDefinition(definition)` | 规范化并校验 `ExcelDefinition`,包括裁剪字符串和应用默认值。 |
| `normalizeDynamicImportOptions(options?)` | 规范化并校验动态导入选项,例如 `headerRow`、`maxFileSizeBytes`。 |
| `sanitizeTextCellValue(value, escapeFormulas?)` | 清洗文本单元格值以避免公式注入;当 `escapeFormulas` 为 `true`(默认)时,会给类似公式的值加单引号前缀。 |
| `defaultMaxFileSizeBytes` | 默认最大文件大小,单位为字节(25 MB)。 |
## 已知限制
- 父级表头和数据分组必须先于依赖它们的列声明。
- `date` 导入结果会规范化为字符串,而不是 `Date` 实例。
- 当前一次只校验一个工作表。