PAYATHON 2026

同时支持 Browser 和 Node.js 的 TypeScript npm 包如何设计文件读写接口?

数据小许

建议保留 loadBNFromBytes 和 saveBNToBytes 作为核心接口,再按需要提供独立的 Node.js 和浏览器便捷函数。两类接口可以并存:初学者使用封装好的文件操作,熟练用户直接处理字节数据。

核心接口负责模型与字节之间的转换

现有设计适合作为跨环境的公共基础:

interface BNByteIO {
  loadBNFromBytes(bytes: ArrayBuffer | ArrayBufferView): Promise<void>;
  saveBNToBytes(): Promise<Uint8Array>;
}

这两个方法负责 BIFXML 的解析、校验和序列化。数据来自本地文件、网络请求还是内存,以及导出后如何保存,都由调用方或适配函数决定。

这样的边界也便于测试。直接传入有效或损坏的字节数据,就可以验证解析行为,无需先准备文件系统或浏览器界面。

环境专用函数作为可选入口

可以将包设计为以下导入形式。这里的 your-package 是包名占位符,子路径需要由包的实际导出配置实现:

import { BN } from 'your-package';
import { loadBNFromFile, saveBNToFile } from 'your-package/node';
import { loadBNFromFile, downloadBN } from 'your-package/browser';

根入口提供 BN 核心功能;node 入口处理文件路径和文件系统操作;browser 入口处理 File、Blob 和下载。使用浏览器入口时,也就不需要连带导入 node:fs/promises。

适配函数可以和核心代码放在同一个 npm 包中,不必一开始就拆成多个包。保持依赖边界清楚即可,核心模块加载时应避免访问 window、document 或 Node.js 文件系统。

Node.js:封装文件读取和写入

以下示例中的 BNByteIO 指前面的公共接口:

import { readFile, writeFile } from 'node:fs/promises';

export async function loadBNFromFile(
  bn: BNByteIO,
  path: string
): Promise<void> {
  const bytes = await readFile(path);
  await bn.loadBNFromBytes(bytes);
}

export async function saveBNToFile(
  bn: BNByteIO,
  path: string
): Promise<void> {
  const bytes = await bn.saveBNToBytes();
  await writeFile(path, bytes);
}

readFile 返回的 Buffer 是 Uint8Array 的子类,可以传给接受 ArrayBufferView 的接口;writeFile 也可以直接接收 Uint8Array,这里不需要额外调用 Buffer.from(bytes)。

保存函数的文档应明确说明文件覆盖行为。普通文件写入也不等同于原子保存,如果后续需要这类保证,应单独设计相应策略。

浏览器:读取 File,单独处理下载

原示例有一处需要修正:FileReader.readAsArrayBuffer() 接收 Blob 或 File,不能接收路径字符串。网页通常需要通过文件选择控件获得用户选择的文件。

在支持 File.arrayBuffer() 的目标浏览器中,读取逻辑可以写成:

export async function loadBNFromFile(
  bn: BNByteIO,
  file: File
): Promise<void> {
  const bytes = await file.arrayBuffer();
  await bn.loadBNFromBytes(bytes);
}

页面负责提供文件选择界面:

const input = document.createElement('input');
input.type = 'file';
input.accept = '.bifxml';

input.addEventListener('change', async () => {
  const file = input.files?.[0];
  if (!file) return;

  try {
    await loadBNFromFile(bn, file);
  } catch (error) {
    console.error('模型加载失败', error);
  }
});

document.body.appendChild(input);

accept 只是文件选择提示,不能代替内容校验。是否为有效 BIFXML,仍应由核心解析逻辑判断。若需要兼容不支持 arrayBuffer() 的目标浏览器,可以在适配层中使用 FileReader。

下载可以另设一个语义明确的函数:

export async function downloadBN(
  bn: BNByteIO,
  filename: string
): Promise<void> {
  const bytes = await bn.saveBNToBytes();
  const blob = new Blob([new Uint8Array(bytes)], {
    type: 'application/octet-stream'
  });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');

  link.href = url;
  link.download = filename;
  document.body.appendChild(link);

  try {
    link.click();
  } finally {
    link.remove();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
}

这里复制字节以构造下载内容,并延后释放 object URL。示例中的延迟用于资源清理,不代表下载完成通知;具体触发行为仍需在目标浏览器中验证。

downloadBN() 的 Promise 完成,只表示序列化和触发下载的代码已执行,不能保证用户最终保存成功。浏览器可能根据设置直接下载、显示提示或限制自动下载,因此 downloadBN 比笼统的 saveBNBrowser 更能表达实际行为。

字节接口需要明确哪些约定?

保留字节接口后,以下约定值得在发布前写清楚:

  • 输入范围:传入 ArrayBufferView 时,只处理该视图的 byteOffset 和 byteLength 所指定的区域,不能直接读取整个底层 buffer。使用 Node.js 的 Buffer 时尤其需要注意这一点。
  • 内存归属:若导出结果引用 Emscripten 的 Wasm 堆,需要明确它何时失效。面向一般调用方时,返回独立的 Uint8Array 副本通常更容易使用。
  • 失败后的状态:原接口会清除已有网络结构,应说明解析失败后对象处于什么状态。若希望失败时保留旧模型,需要在实现中先解析成功,再替换当前状态。
  • 错误语义:区分文件读取失败与 BIFXML 内容无效,并通过 Promise rejection 将错误传给调用方,避免适配层吞掉错误。

对于当前这个小型 npm 包,可以先发布字节接口和完整示例,再补充常用的文件适配函数。如果初学者本来就是主要用户,也可以在首版一并提供这些薄封装。保留字节接口后,便捷函数不会限制用户自行接入网络、存储或其他 I/O 流程。

备注:内容仅供参考。