同时支持 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 流程。
备注:内容仅供参考。
