使用Node.js安全写入JSON对象属性的可靠方案有哪些?
问题根因
你遇到的JSON尾部追加异常绝大多数是异步写入并发冲突导致的:
- 当前代码里的
fs.promises.writeFile是异步操作,你没有用await等待写入完成,若短时间内多次触发同一个PACKAGE_FILE的写入逻辑,多个写入任务会并行执行,操作系统的文件写操作没有互斥,就会出现前一次写入的内容未完全覆盖、后一次写入的内容追加到前一次末尾的情况。 - 小概率场景是读取JSON文件时没有等待读操作完成,就直接修改后写入,拿到的
file对象本身就是不完整的。
安全实现方案
方案1:添加await控制串行执行,禁止并发写
先捋顺异步操作的时序,确保同一时间只有一个写操作执行:
// 先确保读取文件的操作已经完成 const file = JSON.parse(await fs.promises.readFile(PACKAGE_FILE, 'utf-8')) // 修改属性 for (const [key, value] of Object.entries(dependencyFileTypeScript)) { file.devDependencies ??= {} // 加兜底逻辑,避免devDependencies不存在时报错 file.devDependencies[key] = value } // 等待写入完成再执行后续操作 try { await fs.promises.writeFile(PACKAGE_FILE, JSON.stringify(file, null, 2)) } catch (err) { console.error('写入package.json失败:', err) }
方案2:用原子写入替代普通writeFile
如果是生产环境频繁操作配置文件的场景,建议用原子写入逻辑,既可以避免写入中途进程崩溃导致文件损坏,也能天然避免并发冲突:
import { writeFile } from 'fs/promises' import { tmpdir } from 'os' import { join } from 'path' import { renameSync } from 'fs' async function atomicWriteJson(filePath, data, indent = 2) { // 先写入临时文件 const tmpPath = join(tmpdir(), `${Date.now()}-${Math.random().toString(36).slice(2)}.json`) await writeFile(tmpPath, JSON.stringify(data, null, indent)) // 用重命名操作原子替换原文件,同一文件系统下重命名是原子操作,不会出现半写状态 renameSync(tmpPath, filePath) } // 调用示例 await atomicWriteJson(PACKAGE_FILE, file)
方案3:加文件锁避免多进程/多实例写入冲突
如果你的程序有多个进程都可能修改同一个JSON文件,需要添加文件互斥锁:
import lockfile from 'proper-lockfile' import fs from 'fs/promises' try { // 加锁 const release = await lockfile.lock(PACKAGE_FILE) try { // 读文件→改属性→写文件的逻辑全部放在锁持有期间执行 const file = JSON.parse(await fs.readFile(PACKAGE_FILE, 'utf-8')) Object.assign(file.devDependencies ?? {}, dependencyFileTypeScript) await fs.writeFile(PACKAGE_FILE, JSON.stringify(file, null, 2)) } finally { // 释放锁 await release() } } catch (err) { console.error('操作失败:', err) }
内容的提问来源于stack exchange,提问作者Aleksandrs
相关产品推荐
相关产品推荐

