Flutter三方库适配OpenHarmony【doc_text】— 临时文件管理与资源清理策略
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.netdoc_text 在解析 .docx 文件时会产生临时文件——因为需要把 ZIP 内容解压到磁盘上。这些临时文件用完之后必须清理,否则会占用存储空间。这篇讲 doc_text 的临时文件管理策略,包括创建、使用、清理的完整生命周期,以及一些容易踩的坑。及时清理:用完立即删除异常安全:try
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
doc_text 在解析 .docx 文件时会产生临时文件——因为 zlib.decompressFile 需要把 ZIP 内容解压到磁盘上。这些临时文件用完之后必须清理,否则会占用存储空间。这篇讲 doc_text 的临时文件管理策略,包括创建、使用、清理的完整生命周期,以及一些容易踩的坑。
一、临时目录的创建
1.1 命名策略
const tempDir = filePath + "_temp";
// 例如:"/data/storage/el2/base/test.docx" → "/data/storage/el2/base/test.docx_temp"
1.2 创建代码
try {
fs.mkdirSync(tempDir);
} catch (e) {
// 目录可能已存在
}
1.3 为什么用 try-catch 包裹
| 场景 | mkdirSync 行为 | catch 处理 |
|---|---|---|
| 目录不存在 | 创建成功 | 不触发 |
| 目录已存在 | 抛出异常 | 静默忽略 |
| 父目录不存在 | 抛出异常 | 静默忽略 |
| 权限不足 | 抛出异常 | 静默忽略 |
1.4 目录已存在的场景
什么时候临时目录会已经存在?
1. 上一次解析同一个文件时崩溃了,没来得及清理
2. 两次解析同一个文件,第一次的清理还没完成
3. 用户手动创建了同名目录(极少见)
💡 静默忽略"目录已存在"的异常是合理的——如果目录已经存在,decompressFile 会覆盖里面的文件,不影响功能。但如果是权限不足导致创建失败,后续的 decompressFile 也会失败,会被外层 catch 捕获。
二、临时目录的使用
2.1 解压到临时目录
await zlib.decompressFile(filePath, tempDir);
解压后的目录结构:
test.docx_temp/
├── [Content_Types].xml
├── _rels/
│ └── .rels
├── word/
│ ├── document.xml ← 我们要读的
│ ├── styles.xml
│ ├── fontTable.xml
│ ├── settings.xml
│ └── _rels/
│ └── document.xml.rels
└── docProps/
├── core.xml
└── app.xml
2.2 从临时目录读取文件
const documentXmlPath = tempDir + "/word/document.xml";
if (!fs.accessSync(documentXmlPath)) {
this.cleanupTempDir(tempDir);
return null;
}
const xmlFile = fs.openSync(documentXmlPath, fs.OpenMode.READ_ONLY);
const xmlStat = fs.statSync(documentXmlPath);
const xmlBuf = new ArrayBuffer(xmlStat.size);
fs.readSync(xmlFile.fd, xmlBuf);
fs.closeSync(xmlFile);
2.3 只读取 document.xml
| 文件 | 是否读取 | 原因 |
|---|---|---|
| word/document.xml | ✅ | 正文文本在这里 |
| word/styles.xml | ❌ | 样式信息,不需要 |
| word/fontTable.xml | ❌ | 字体信息,不需要 |
| word/header1.xml | ❌ | 页眉,当前不提取 |
| word/footer1.xml | ❌ | 页脚,当前不提取 |
| docProps/core.xml | ❌ | 文档属性,不需要 |
📌 解压了整个 ZIP 但只读了一个文件。这是
zlib.decompressFileAPI 的限制——它不支持只解压特定文件。如果能只解压 word/document.xml,可以节省 I/O 和存储空间。
三、cleanupTempDir 递归删除
3.1 完整实现
private cleanupTempDir(dirPath: string): void {
try {
const files = fs.listFileSync(dirPath);
for (const file of files) {
const fullPath = dirPath + "/" + file;
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
this.cleanupTempDir(fullPath); // 递归删除子目录
} else {
fs.unlinkSync(fullPath); // 删除文件
}
}
fs.rmdirSync(dirPath); // 删除空目录
} catch (e) {
// 忽略清理错误
}
}
3.2 执行流程
cleanupTempDir("test.docx_temp")
│
├── listFileSync → ["[Content_Types].xml", "_rels", "word", "docProps"]
│
├── "[Content_Types].xml" → isDirectory? No → unlinkSync
│
├── "_rels" → isDirectory? Yes → cleanupTempDir("test.docx_temp/_rels")
│ ├── listFileSync → [".rels"]
│ ├── ".rels" → unlinkSync
│ └── rmdirSync("test.docx_temp/_rels")
│
├── "word" → isDirectory? Yes → cleanupTempDir("test.docx_temp/word")
│ ├── listFileSync → ["document.xml", "styles.xml", ..., "_rels"]
│ ├── "document.xml" → unlinkSync
│ ├── "styles.xml" → unlinkSync
│ ├── "_rels" → cleanupTempDir → 递归删除
│ └── rmdirSync("test.docx_temp/word")
│
├── "docProps" → isDirectory? Yes → cleanupTempDir → 递归删除
│
└── rmdirSync("test.docx_temp")
3.3 使用的 fs API
| API | 作用 | 说明 |
|---|---|---|
fs.listFileSync(dir) |
列出目录内容 | 返回文件名数组 |
fs.statSync(path) |
获取文件信息 | 判断是文件还是目录 |
fs.unlinkSync(path) |
删除文件 | 不能删除目录 |
fs.rmdirSync(dir) |
删除空目录 | 目录必须为空 |
3.4 删除顺序
必须先删除目录中的所有文件和子目录,才能删除目录本身。
这就是为什么需要递归——先深入最内层,从里往外删。
错误顺序:
rmdirSync("test.docx_temp") → 失败!目录不为空
正确顺序:
unlinkSync("test.docx_temp/word/document.xml")
unlinkSync("test.docx_temp/word/styles.xml")
rmdirSync("test.docx_temp/word")
// ... 删除所有子目录
rmdirSync("test.docx_temp") → 成功!
四、异常安全
4.1 try-catch 包裹
private cleanupTempDir(dirPath: string): void {
try {
// 所有清理逻辑
} catch (e) {
// 忽略清理错误
}
}
4.2 为什么忽略清理错误
| 清理错误 | 影响 | 处理 |
|---|---|---|
| 文件被占用 | 临时文件残留 | 下次覆盖 |
| 权限不足 | 临时文件残留 | 无法处理 |
| 文件已被删除 | 无影响 | 忽略 |
| 目录不存在 | 无影响 | 忽略 |
4.3 清理失败不应影响主流程
// 正确:清理失败不影响返回结果
const text = this.parseDocxXml(xmlBuf);
this.cleanupTempDir(tempDir); // 即使失败,text 已经拿到了
return text;
// 如果清理抛异常且没有 catch:
const text = this.parseDocxXml(xmlBuf);
this.cleanupTempDir(tempDir); // 抛异常!
return text; // 永远执行不到!
💡 清理操作永远不应该影响主流程的返回值。即使临时文件没删干净,文本已经成功提取了,应该正常返回。
五、文件句柄管理
5.1 openSync / closeSync 配对
// 模式1:读取原始文件
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
// ... 使用 file.fd ...
fs.closeSync(file);
// 模式2:读取 XML 文件
const xmlFile = fs.openSync(documentXmlPath, fs.OpenMode.READ_ONLY);
// ... 使用 xmlFile.fd ...
fs.closeSync(xmlFile);
5.2 文件句柄泄漏的风险
// 当前代码的潜在问题:
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
const stat = fs.statSync(filePath);
const buf = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buf); // 如果这里抛异常...
fs.closeSync(file); // 这行不会执行 → 句柄泄漏!
5.3 改进方案
// 使用 try-finally 保证关闭
const file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
try {
const stat = fs.statSync(filePath);
const buf = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buf);
} finally {
fs.closeSync(file); // 无论成功失败都关闭
}
| 方案 | 优点 | 缺点 |
|---|---|---|
| 当前(无 finally) | 简单 | 异常时句柄泄漏 |
| try-finally | 安全 | 多几行代码 |
5.4 实际影响
文件句柄泄漏的影响:
1. 短期:无明显影响(系统有句柄上限,通常几千个)
2. 长期:如果频繁调用且频繁失败,可能耗尽句柄
3. 进程退出时:系统自动回收所有句柄
对于 doc_text 的使用场景(偶尔调用),句柄泄漏的风险很低。
六、与 Android 端的对比
6.1 Android 端:无临时文件
// Android 使用 POI,在内存中处理
FileInputStream fis = new FileInputStream(filePath);
XWPFDocument docx = new XWPFDocument(fis); // 内存中解压 ZIP
// 不产生临时文件
fis.close();
6.2 对比
| 维度 | OpenHarmony | Android |
|---|---|---|
| ZIP 解压方式 | 解压到磁盘 | 在内存中解压 |
| 临时文件 | 有 | 无 |
| 需要清理 | ✅ | ❌ |
| 内存占用 | 低 | 高(整个 ZIP 在内存中) |
| 磁盘 I/O | 高(写临时文件) | 低 |
| 实现复杂度 | 中(需要清理逻辑) | 低 |
6.3 为什么 OpenHarmony 不能在内存中解压
// zlib.decompressFile 的 API 签名
zlib.decompressFile(inFile: string, outFile: string): Promise<void>
// 它只支持文件到文件的解压
// 不支持文件到内存的解压
// 所以必须用临时目录
📌 如果 OpenHarmony 的 zlib 模块提供了内存解压 API(比如
decompressToBuffer),就可以避免临时文件。但当前版本没有这个 API。
七、临时文件残留的处理
7.1 残留场景
| 场景 | 原因 | 临时文件状态 |
|---|---|---|
| 正常完成 | cleanupTempDir 成功 | ✅ 已清理 |
| 解析异常 | 外层 catch 捕获,但没调用 cleanup | ❌ 残留 |
| 应用崩溃 | 进程终止 | ❌ 残留 |
| 清理失败 | 权限或文件占用 | ❌ 残留 |
7.2 残留文件的影响
残留的临时目录:test.docx_temp/
大小:通常几 KB 到几 MB(取决于 docx 文件大小)
影响:
1. 占用存储空间(通常很小)
2. 下次解析同一文件时会覆盖(不影响功能)
3. 不会影响其他文件的解析
7.3 改进方案
// 方案1:使用 try-finally 保证清理
const tempDir = filePath + "_temp";
try {
fs.mkdirSync(tempDir);
await zlib.decompressFile(filePath, tempDir);
// ... 读取和解析 ...
return text;
} finally {
this.cleanupTempDir(tempDir);
}
// 方案2:启动时清理旧的临时目录
onAttachedToEngine(binding: FlutterPluginBinding): void {
// 清理可能残留的临时目录
this.cleanupOldTempDirs();
}
八、最佳实践总结
8.1 临时文件管理的原则
- 及时清理:用完立即删除
- 异常安全:try-finally 保证清理
- 静默失败:清理失败不影响主流程
- 可预测命名:临时目录名可以从源文件名推导
- 递归删除:处理嵌套目录结构
8.2 doc_text 的改进建议

总结
doc_text 的临时文件管理涵盖了创建、使用、清理的完整生命周期:
- 创建:filePath + “_temp”,mkdirSync + try-catch
- 使用:zlib.decompressFile 解压,只读取 document.xml
- 清理:cleanupTempDir 递归删除,忽略清理错误
- 句柄管理:openSync / closeSync 配对,但缺少 finally 保护
- 残留处理:异常场景可能残留,但影响有限
下一篇我们看错误处理体系——三种错误码、各种边界场景的防御策略。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐


所有评论(0)