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

gbxlxs

v0.1.6

Published

Standalone spreadsheet (XLSX) engine with a self-contained HTML editor: read/write xlsx, workbook model ops, CSV/HTML import-export and a full spreadsheet UI.

Readme

gbxlxs

独立电子表格引擎:读写真正的 .xlsx、工作簿模型操作、CSV / HTML 导入导出,外加一个自包含的 HTML 表格编辑器。

它不是某个在线表格的封装,而是一层可以直接嵌进任何前端的表格内核:解析 OOXML 得到一个纯对象模型,用同步算子改这个模型,再序列化回 .xlsx,并在往返中尽量保住原文件的边角内容(图表、图片、透视表、条件格式、打印分节等)。ZIP 与 XML 这两个底层难题全部交给 gbooxml。

四层结构

| 层 | 入口 | 职责 | | --- | --- | --- | | 引擎 | gbxlxs/workbook | .xlsx 读写、CSV 导出、HTML 导入、数字格式、行列插删 | | 算子 | gbxlxs/ops | 对工作簿/分节对象做同步就地修改:取值、写值、公式、样式、合并、排序、筛选、冻结、分节增删改名 | | 会话 | gbxlxs/session | 托管一份文档:脏标记、自动序列化、失败上报、修订号握手 | | 界面 | web/ | 原生 HTML 表格编辑器:菜单、工具带、公式栏、窗口化网格、页签、右键菜单 |

根入口 gbxlxs 把引擎/算子/会话的 91 个名字一次性全部导出,日常用 import { openXlsx, setValue } from 'gbxlxs' 就够了。

安装

npm install gbxlxs

界面

web/ 是一套不打包、不依赖框架的完整表格编辑器,引擎仍走本包:

npm start                 # 等价于 node web/serve.mjs
# http://localhost:5275/web/

包含打开 / 保存 / 另存为 .xlsx、新建、CSV 导出、自动保存(浏览器文件句柄)、深色主题、缩放(工具栏、Ctrl+滚轮以指针为锚)、冻结窗格、筛选、合并、排序、数字格式、条件格式、数据验证、批注、超链接、复选框、图片预览、拖放打开、撤销重做与剪贴板;公式在本地实时求值,输入函数名时弹出候选列表(↑↓ 选择、Enter/Tab 插入、Esc 收起),函数参数处显示签名与必填/可选说明;提供自动求和、可搜索的插入函数对话框、函数分类菜单,列宽/行高支持拖拽与双击自适应,填充柄支持复制、数字/日期序列与公式相对引用平移。index.html 通过 import map 把 gbooxml / guabiutils 指到 /vendor/*,serve.mjs(薄封装,本体在 guabiutils/dev-server)负责静态供给,无需构建。

也可以把编辑器嵌进现有前端(不依赖 Vue,导出路径为 gbxlxs/web/*):

import { SheetEditor } from 'gbxlxs/web/app/sheets-app.js';

const editor = new SheetEditor(mountEl, {
  embedded: true,                 // 文件级动作交给宿主
  host: {
    getTitle: () => '销售.xlsx',
    getPath: () => '文档/表格',
    getStatus: () => ({ text: '已保存', kind: '' }),
    getAutoSave: () => true,
    setAutoSave: (on) => {},
    canSave: () => true,
    save: async () => true,       // 宿主负责把 serializeNow() 的字节写回
    saveAs: async () => true,
    open: () => {},
    create: () => {},
    recentList: () => [],
    openRecent: (doc) => {},
    onChange: ({ dirty, error }) => {},
  },
});

await editor.openBytes(bytes);    // 或 await editor.createBlank()
const out = await editor.serializeNow();
editor.dispose();

不传 host / embedded 时即独立模式,编辑器自己处理文件选择、保存与未保存确认。

导入路径

exports 声明了四个引擎入口与一个 web/ 通配;src/ 深层路径仍会被 Node 直接拒掉:

import { openXlsx } from 'gbxlxs';                 // 根入口,全部 91 个名字
import { openXlsx } from 'gbxlxs/workbook';        // 只要引擎层
import { setValue } from 'gbxlxs/ops';             // 只要算子层
import { createSheetSession } from 'gbxlxs/session';
import { SheetEditor } from 'gbxlxs/web/app/sheets-app.js';
import 'gbxlxs/web/styles/sheets.css';
import version from 'gbxlxs/package.json' with { type: 'json' };

import ... from 'gbxlxs/src/ops.js' 这种写法会报 ERR_PACKAGE_PATH_NOT_EXPORTED(仓库内的测试能这么写,是因为走的是文件系统路径,不是包解析)。

快速开始

读 → 改 → 存

import { openXlsx, saveXlsx, setValue } from 'gbxlxs';

const wb = await openXlsx(bytes, { readStyles: true });   // 注意:readStyles 默认是 false
const sheet = wb.sheets[0];

setValue(sheet, 0, 0, '新值');                             // 同步、就地改
const out = await saveXlsx(wb);                            // Promise<Uint8Array>

readStyles: true 是这个 API 最容易漏的一个开关:不传就不解析样式,写回去等于把原格式抹平。

从零写一个 .xlsx

import { writeXlsx } from 'gbxlxs';

const bytes = await writeXlsx({
  sheets: [{
    name: '销售',
    rows: [
      [{ value: '品名', style: { font: { bold: true } } }, { value: '金额' }],
      ['苹果', 12.5],
      [{ value: 0.25, style: { numFmt: '0.00%' } }, { value: '注', comment: { text: '备注', author: '我' } }],
    ],
  }],
  defaultFont: { name: '微软雅黑', size: 11 },
});

rows 里的单元格里联对象会被自动提升进 cells:{ value, style, formula, richText, comment, hyperlink, checkbox } 都可以直接写在格子上(超链接对象本身除外,它靠 value 上的 hyperlink 字段识别)。

saveXlsx 和 writeXlsx 是两条不同的路:saveXlsx 走保真往返,只接受 openXlsx 产出的工作簿;writeXlsx 从裸模型重新生成全部 OOXML。把一个手工拼出来的对象交给 saveXlsx 会抛 InvalidArgumentError。

导出一张表(writeTableXlsx)

只是要把「表头 + 行」导成 Excel 时不用拼工作簿模型:列定义、表头样式、冻结首行、自动筛选与列宽一次配好。

import { writeTableXlsx } from 'gbxlxs';

const bytes = await writeTableXlsx({
  name: '任务',
  columns: [
    { title: '名称', key: 'name', width: 40 },
    { title: '负责人', key: 'owner' },
    { title: '截止', key: 'end' },
    { title: '数量', key: 'count', numFmt: '#,##0', align: 'right' },
  ],
  rows: [
    { name: '写文档', owner: '李娜', end: '2026-11-01', count: 3 },
    ['评审', '王强', '2026-11-02', 1],            // 也可以给数组(按列序)
  ],
});

// 多张表:{ sheets: [{ name, columns, rows }, …], defaultFont, dateSystem }
  • 列:title 必填(表头文案,也是对象行的默认取值键);key / width / numFmt / align(center / right)/ style 可选。numFmt 与 align 落到该列每个数据格上。
  • 行:数组按列序、对象按 key 取值;null / undefined / "" 落成空格;数字 / 布尔 / Date 原样保留类型;{ value, style, hyperlink } 内联单元格对象透传。不做类型推断("00123" 仍是文本)。
  • 版式:header(默认 true)出表头行(加粗 + 浅灰底 + 居中 + 下细线,headerStyle 可整份替换),freezeHeader(默认 true)冻结首行,autoFilter(默认 true)给「表头 + 数据」加自动筛选,autoWidth(默认 true)按内容算列宽(8–60 字符;给了 width 的列以 width 为准,autoWidth: false 则只写显式宽度)。
  • 顶层另接受 defaultFont / dateSystem / stringMode / activeSheet,原样传给 writeXlsx。
  • 参数不合法(空列、空标题、行里塞数组或普通对象、一行给的值比列多、sheets 与 columns 混用)一律抛 InvalidArgumentError,不会静默写出半张表。

公式

import { setFormula, getFormula, recalculateWorkbook, insertRows } from 'gbxlxs';

setFormula(sheet, 1, 1, 'SUM(A2:A5)');        // 公式不带前导 =;第 4 个参数可传缓存值
recalculateWorkbook(wb);                       // 本地求值,写回每格的 formulaResult
getFormula(sheet, 1, 1);                       // 'SUM(A2:A5)'
sheet.rows[1][1];                              // 计算结果

insertRows(sheet, 1, 1);                       // 在第 2 行插一行,引用自动下移

引擎自带公式解析与求值:支持单元格/区域/跨表引用、定义名称、共享公式、错误值与循环检测,内置 230+ 个函数(数学、统计、文本、日期、逻辑、查找、财务、工程、信息、数据库)。recalculateWorkbook(wb, { keepCached: true }) 会在打开文件时保留无法识别函数的原缓存值,编辑时再暴露 #NAME?。行列插删、隐藏、移动这类结构算子会自动重映射 cells 的键、公式引用、合并区、图片锚点、冻结线和分页符 —— 前提是先调用过 linkWorkbookSheets(wb)(openXlsx 与 addSheet 已经替你做这一步),否则跨表引用不会被同步。

样式与数字格式

import { setStyle, setFont, applyNumfmt, getDisplayText } from 'gbxlxs';

setFont(sheet, 0, 0, { bold: true, color: '#c00000' });
setStyle(sheet, 0, 0, { alignment: { wrapText: true } });
applyNumfmt(sheet, { startRow: 1, startCol: 1, endRow: 3, endCol: 1 }, '0.00');
getDisplayText(sheet, 1, 1);                    // 按 numFmt 渲染出来的文本

样式对象在格子之间是共享引用,绝不要就地改它;setStyle / replaceStyle / cloneCellStyle 都会先克隆再替换。

合并、排序、筛选、冻结

import { mergeRegion, sortRegion, toggleFilter, applyFilter, setFreeze, normalizeRegion } from 'gbxlxs';

const region = normalizeRegion(0, 0, 4, 3);      // → { startRow, startCol, endRow, endCol }
mergeRegion(sheet, region);                      // 1×1 返回 false;与已有合并相交时抛错
sortRegion(sheet, region, 1, false);             // 按第 2 列升序;含跨行合并时抛错
toggleFilter(sheet, region);                     // 建/撤自动筛选
applyFilter(sheet, 1, ['苹果', '梨']);            // 按显示文本筛某一列
setFreeze(sheet, 1, 0);                          // 冻结首行

排序的首行识别规则:该行是非空字符串,且同列数据区里存在非字符串值,或者该格 style.font.bold === true。混合类型排序为 数字/日期 < 文本 < 布尔,空值永远排最后(即使 desc)。

高级筛选(条件 / 空白 / 颜色 / 列间且或)

import { setFilterCriteria, setFilterLogic, filterCriteria, filterColorOptions, rowMatchesFilter } from 'gbxlxs';

setFilterCriteria(sheet, 1, { customFilters: { and: true, customFilters: [
  { operator: 'greaterThan', value: 100 },
  { operator: 'lessThanOrEqual', value: 500 },
] } });                                          // 100 < x <= 500
setFilterCriteria(sheet, 2, { filters: ['北京'], blank: true });   // 按值 + 带空白
setFilterCriteria(sheet, 3, { colorFilters: { cellFillColors: ['FFCC00'] } });
setFilterCriteria(sheet, 1, null);               // 清除该列条件
setFilterLogic(sheet, 'or');                     // 列间关系:任一条件满足
rowMatchesFilter(sheet, 5);                      // 单行是否通过(标题行恒为 true)

operator 取 equal / notEqual / greaterThan / greaterThanOrEqual / lessThan / lessThanOrEqual / beginsWith / endsWith / contains / notContains;等于/不等于支持 * 与 ? 通配符。筛选隐藏的行由引擎单独记账,取消筛选不会把用户手动隐藏的行放出来。条件随 xlsx 的 customFilters / colorFilter 往返。

查找与替换

import { findInSheet, findInWorkbook, replaceInSheet, replaceInWorkbook, replaceMatch } from 'gbxlxs';

const hits = findInWorkbook(wb, '订单-\\d+', { useRegex: true, matchCase: false, inFormulas: true });
// → [{ sheetIndex, sheetName, row, col, start, end, text, cellText, inFormula }]
replaceInWorkbook(wb, '旧值', '新值', { matchEntireCell: true });   // → { count, changes }
replaceMatch(sheet, hits[0], '新值');            // 单个命中(UI「替换」按钮口径)

选项:matchCase、matchEntireCell(同义 wholeCell)、useRegex、inFormulas(同义 lookin: 'formulas')、byFormat + formatStyle、range、sheetIndex。replaceIn* 是「全部替换」口径(格内多处出现一次换净),replaceMatch 只换命中处;返回的 changes 可直接交给历史栈撤销。无效的正则会抛「正则表达式无效」。

超级表结构化引用与动态数组

import { setFormula, recalculateWorkbook } from 'gbxlxs';

setFormula(sheet, 9, 1, 'SUM(销售表[金额])');      // 列、[列]:[列]、[#全部]、[#数据]
setFormula(sheet, 9, 2, '销售表[[#标题],[金额]]');  // [#标题] / [#总计]
setFormula(sheet, 2, 5, '@单价 * @数量');         // @ 与 [#此行] 取当前行
setFormula(sheet, 0, 5, 'SORT(A2:A10)');          // 数组结果自动溢出,冲突返回 #SPILL!
setFormula(sheet, 0, 8, 'LET(x, 10, x * 2)');     // LET / LAMBDA 已可求值
recalculateWorkbook(wb);

表名或列名对不上返回 #NAME?,节不合法(如无标题行却要 [#标题])返回 #REF!。溢出区的值写在 spillParent 标记的格子上,重算前会被清掉,不会污染用户数据。

打印分页、线程评论与形状

import { paginateSheet, normalizePrintSetup, addThread, replyThread, threadGroups } from 'gbxlxs';

const g = { colWidths: [...], rowHeights: [...] };           // 未缩放的 96dpi 像素尺寸
const { pages, setup } = paginateSheet(sheet, g);             // 按打印区域/标题/分页符/缩放切页
normalizePrintSetup(sheet);                                   // 页面设置 → 像素与选项

addThread(sheet, wb, 0, 0, '这里需要复核', '张三');            // 线程评论(legacy comment 仍是备注)
replyThread(sheet, wb, rootId, '已改');
threadGroups(sheet, wb, 0, 0);                                // → [{ author, replies, done }]

sheet.shapes / sheet.textBoxes 随 xlsx 往返,插删行列时锚点自动同步。paginateSheet 只算分页,渲染交给 UI(Web 编辑器的打印预览即基于此,走浏览器打印对话框另存 PDF)。

CSV 导出与 HTML 导入

import { writeCsv, fromHtml } from 'gbxlxs';

const text = writeCsv(sheet.rows, { delimiter: ',', bom: true, dateFormat: 'yyyy-mm-dd' });  // 同步返回字符串

const sheet2 = fromHtml('<table><tr><td>1</td></tr></table>');   // 返回的是「分节」,不是工作簿
const bytes = await writeXlsx({ sheets: [sheet2] });

托管一份文档(会话)

import { createSheetSession, setValue } from 'gbxlxs';

const session = createSheetSession({
  onChange: () => render(session.getSheets()),
  onSerializeFailed: (err) => toast(err.message),
});

await session.open(bytes);            // 或 await session.createBlank()
setValue(session.getSheet(0), 0, 0, 'v1');
session.markChanged();                // 触发一次去抖序列化
await session.serializeNow();         // 立即拿当前 Uint8Array

会话的 markChanged() 只是递增修订号并后台序列化;真正落盘后要 session.ackFlushed(revision) 才会清脏标记。序列化失败不会丢数据 —— getLatestBytes() 继续返回上一份可用字节,getSerializeError() 保留错误,等你修好分节名再 markChanged() 自愈。

数据模型

一个工作簿长这样(字段均为真实名称):

{
  sheets: [{
    name: 'Sheet1',
    rows: [['a', 1], ['b', 2]],          // 稠密矩形,Array<Array<value>>
    cells: new Map([['0,1', { value: 1, type: 'number', style: {…} }]]),
    columns: [{ width: 20 }, undefined],
    rowDefs: new Map([[0, { height: 24 }]]),
    merges: [{ startRow: 0, startCol: 0, endRow: 0, endCol: 1 }],
    freezePane: { rows: 1 },
    autoFilter: { range: 'A1:D9', columns: [{ colIndex: 0, filters: ['a'] }] },
    threadedComments: [{ id, personId, ref: 'A1', date, text, parentId?, done? }],
    shapes: [{ type: 'rect', text, anchor: { from: { row, col }, to: { row, col } }, width, height, style }],
    // 还可能有:dataValidations, conditionalRules, images, charts, tables,
    // textBoxes, pageSetup, headerFooter, protection, sparklines, …
  }],
  persons: [{ id, displayName }],
  dateSystem: '1900',
  activeSheet: 0,
  namedRanges: [{ name, range, scope }],
  properties: { title, creator, created, modified },
}

单元格的 value 可能是 null、number、string、boolean、Date,或错误文本字符串(如 #REF!)。日期序列在读入时就被转成真正的 Date;公式格存的是缓存结果而不是公式文本(公式文本在 cells 条目的 formula 上)。

rows 与 cells 的双写规则

rows 是稠密的值矩形,cells 是稀疏的细节条目表(样式、公式、富文本、批注、超链接)。写出时 cells 里的值覆盖 rows,所以:

  • 读值请走 getValue / getDisplayText,它们会先查 cells 再回落 rows;
  • 写值请走 setValue / setFormula,它们会同时更新两侧并清理无关字段;
  • 永远不要直接 sheet.rows[r][c] = x —— 旧的 cells 条目会把你新写的值盖回去。

API 速查

| 组 | 名字 | | --- | --- | | 打开/保存 | openXlsx、saveXlsx、writeXlsx、writeTableXlsx、fromHtml、writeCsv | | 读格 | getEntry、getValue、getDisplayText、getFormula、getStyle、keyOf、isProtected | | 写格 | setValue、setFormula、setStyle、setFont、setCellExtra、replaceStyle、clearStyle、parseInput | | 快照/撤销 | captureCell、applyCell、captureRegion | | 行列结构 | insertRows、deleteRows、hideRows、insertColumns、deleteColumns、hideColumns、unhideAll、setFreeze | | 分节 | addSheet、removeSheetAt、renameSheetAt、linkWorkbookSheets | | 区域 | normalizeRegion、regionA1、cellHasContent、currentRegion、regionsOverlap、mergeAt、mergeRegion、unmergeRegion | | 排序/筛选 | compareKey、sortRegion、toggleFilter、applyFilter、clearFilter、filterOptions、filterColorOptions、filterCriteria、setFilterCriteria、setFilterLogic、filterLogic、rowMatchesFilter、hasFilter | | 查找替换 | findInSheet、findInWorkbook、replaceMatch、replaceInSheet、replaceInWorkbook | | 打印分页 | paginateSheet、normalizePrintSetup | | 格式 | formatValue、inferType、cloneCellStyle、shiftedPattern、applyNumfmt、currentNumfmt、applyBorder | | 引用工具 | parseCellRef、cellRef、colToLetter、letterToCol、parseRange、isInRange、replaceA1Refs | | 校验常量 | FORBIDDEN_SHEET_NAME_CHARS、MAX_SHEET_NAME_LENGTH、RESERVED_SHEET_NAME |

openXlsx 的完整选项面:sheets(数组按序号/名称挑,或 ({ name, index, hidden, veryHidden }, i) => boolean)、readStyles、range、maxRows、maxTotalCells、sparse、dateSystem、password、maxSpinCount、maxInputBytes、maxDecompressedBytes、onWarning。onWarning 收到 { code, message, sheet, row, col },实际出现过的 code 有 malformed-cell-ref、unresolved-shared-string、unresolved-style、unresolved-hyperlink。

校验与上限

分节名由 workbook/validate.js 统一把关,全部抛 InvalidArgumentError:非空、≤ 31 字符、不含 [ ] : * ? / \、不以撇号 ' 开头或结尾、不等于保留名 history(大小写不敏感)、同簿内不重名(大小写不敏感)。renameSheetAt 用的是同一套规则、中文错误文案。

硬上限:行索引 ≤ 1048575、列索引 ≤ 16383(即 A1:XFD1048576)、单元格总数 2×10⁷、cells Map 条目 2²⁴、数字格式串长度 ≤ 255。

环境说明

浏览器可直接用,src/ 内没有任何 node: 导入,也不用 Buffer、fs、process。Node 侧 engines: >=18 只是为了 structuredClone 与 WebCrypto(敏捷加密),以及 node --test 跑测试。

错误类不可导入:WorkbookError / InvalidArgumentError / EncryptedFileError / DecryptionError / ParseError 都不在导出面上,只能 try/catch 后按 err.name 与 err.message 判断。引擎层(validate.js)文案是英文,算子层(ops.js)文案是中文。

已知坑

  1. readStyles 默认 false,忘传就丢样式。
  2. saveXlsx 依赖 openXlsx 挂在工作簿上的一个不可枚举、不可写、不可配置的 Symbol 往返状态;展开或克隆工作簿会丢掉它,之后只能改用 writeXlsx。
  3. xl/calcchain.xml 在保存时被丢弃(Excel 会自行重算),这是刻意的。
  4. hideColumns 会用 {} 懒填充 sheet.columns,所以 columns 可能很长且带空洞。
  5. 筛选隐藏行记录在模块级 WeakMap 里而不是模型上;unhideAll() 会清掉它,因此先 unhideAll() 再 clearFilter(),否则手动隐藏会被当成筛选隐藏。
  6. 用 options.sheets 过滤打开会真的删掉分节,于是 activeSheet 下标与跨表位置相对原文件发生偏移。
  7. 文本→类型推断请走 inferType,别自己 Number();日期串退化成文本后,General 格式会把它显示成带时间的 ISO 串。
  8. 富文本在写出时优先于公式,所以 setFormula 会主动删掉 richText;反过来只删 formula 而不调 clearFormulaMeta,会让下一条公式背上 array/shared/dynamic 标记。
  9. 想输入公式请用 parseInput(text):=1+1 返回 { formula: '1+1', value: null },其余走 inferType。