MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript在Node中操作文件的最佳实践

2022-12-154.6k 阅读

1. 了解 Node.js 文件系统模块

在 Node.js 中,操作文件主要依赖于内置的 fs(文件系统)模块。这个模块提供了一系列的方法来执行文件和目录的创建、读取、写入、删除等操作。fs 模块有两种主要的操作模式:同步和异步。

1.1 同步操作

同步操作会阻塞 Node.js 的事件循环,直到操作完成。这意味着在操作执行期间,其他代码无法执行。同步方法通常在方法名中包含 Sync 后缀。

const fs = require('fs');

try {
    const data = fs.readFileSync('example.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error(err);
}

在上述代码中,readFileSync 方法尝试同步读取 example.txt 文件的内容。如果文件不存在或读取过程中发生错误,try...catch 块会捕获并处理错误。

1.2 异步操作

异步操作不会阻塞事件循环,允许其他代码在操作执行期间继续执行。异步方法通常接受一个回调函数作为最后一个参数,该回调函数在操作完成时被调用。

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

这里的 readFile 方法是异步的,当文件读取完成时,回调函数被调用。如果发生错误,err 参数会包含错误信息。

2. 读取文件

2.1 读取文本文件

读取文本文件是常见的操作。可以使用 fs.readFilefs.readFileSync 方法,指定编码为 utf8 来读取文本内容。

2.1.1 异步读取文本文件

const fs = require('fs');

function readTextFileAsync(filePath) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

readTextFileAsync('example.txt')
   .then(data => {
        console.log(data);
    })
   .catch(err => {
        console.error(err);
    });

上述代码通过将 fs.readFile 封装在 Promise 中,实现了一个更现代的异步读取文本文件的方式。

2.1.2 同步读取文本文件

const fs = require('fs');

function readTextFileSync(filePath) {
    try {
        return fs.readFileSync(filePath, 'utf8');
    } catch (err) {
        console.error(err);
        return null;
    }
}

const data = readTextFileSync('example.txt');
if (data) {
    console.log(data);
}

同步读取文本文件相对简单,但要注意在可能阻塞的场景下使用。

2.2 读取二进制文件

读取二进制文件时,不需要指定编码。

2.2.1 异步读取二进制文件

const fs = require('fs');

function readBinaryFileAsync(filePath) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

readBinaryFileAsync('image.jpg')
   .then(buffer => {
        // 处理二进制数据,例如上传到服务器
        console.log('Binary data read successfully:', buffer.length);
    })
   .catch(err => {
        console.error(err);
    });

这里读取的二进制数据以 Buffer 对象的形式返回。

2.2.2 同步读取二进制文件

const fs = require('fs');

function readBinaryFileSync(filePath) {
    try {
        return fs.readFileSync(filePath);
    } catch (err) {
        console.error(err);
        return null;
    }
}

const buffer = readBinaryFileSync('image.jpg');
if (buffer) {
    console.log('Binary data read successfully:', buffer.length);
}

同步读取二进制文件同样简单,但需谨慎使用。

3. 写入文件

3.1 写入文本文件

3.1.1 异步写入文本文件

可以使用 fs.writeFile 方法异步写入文本文件。

const fs = require('fs');

function writeTextFileAsync(filePath, content) {
    return new Promise((resolve, reject) => {
        fs.writeFile(filePath, content, 'utf8', err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

writeTextFileAsync('newFile.txt', 'This is some text content')
   .then(() => {
        console.log('File written successfully');
    })
   .catch(err => {
        console.error(err);
    });

默认情况下,writeFile 会覆盖文件内容。如果文件不存在,会创建新文件。

3.1.2 同步写入文本文件

const fs = require('fs');

function writeTextFileSync(filePath, content) {
    try {
        fs.writeFileSync(filePath, content, 'utf8');
        console.log('File written successfully');
    } catch (err) {
        console.error(err);
    }
}

writeTextFileSync('newFile.txt', 'This is some text content');

同步写入操作会立即执行,同样要注意阻塞问题。

3.2 追加文本到文件

3.2.1 异步追加文本

使用 fs.appendFile 方法异步追加文本到文件。

const fs = require('fs');

function appendTextFileAsync(filePath, content) {
    return new Promise((resolve, reject) => {
        fs.appendFile(filePath, content, 'utf8', err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

appendTextFileAsync('existingFile.txt', '\nThis is appended text')
   .then(() => {
        console.log('Text appended successfully');
    })
   .catch(err => {
        console.error(err);
    });

3.2.2 同步追加文本

const fs = require('fs');

function appendTextFileSync(filePath, content) {
    try {
        fs.appendFileSync(filePath, content, 'utf8');
        console.log('Text appended successfully');
    } catch (err) {
        console.error(err);
    }
}

appendTextFileSync('existingFile.txt', '\nThis is appended text');

3.3 写入二进制文件

3.3.1 异步写入二进制文件

对于二进制文件,使用 fs.writeFile 且不指定编码。

const fs = require('fs');
const http = require('http');

function downloadAndWriteBinaryFileAsync(url, filePath) {
    return new Promise((resolve, reject) => {
        const fileStream = fs.createWriteStream(filePath);
        http.get(url, response => {
            response.pipe(fileStream);
            fileStream.on('finish', () => {
                fileStream.close();
                resolve();
            });
            fileStream.on('error', err => {
                fs.unlinkSync(filePath);
                reject(err);
            });
        }).on('error', err => {
            reject(err);
        });
    });
}

downloadAndWriteBinaryFileAsync('http://example.com/image.jpg', 'localImage.jpg')
   .then(() => {
        console.log('Binary file written successfully');
    })
   .catch(err => {
        console.error(err);
    });

这里通过 http.get 下载二进制文件并异步写入本地。

3.3.2 同步写入二进制文件

const fs = require('fs');
const http = require('http');

function downloadAndWriteBinaryFileSync(url, filePath) {
    const response = http.get(url);
    let data = '';
    response.on('data', chunk => {
        data += chunk;
    });
    response.on('end', () => {
        try {
            fs.writeFileSync(filePath, data);
            console.log('Binary file written successfully');
        } catch (err) {
            console.error(err);
        }
    });
    response.on('error', err => {
        console.error(err);
    });
}

downloadAndWriteBinaryFileSync('http://example.com/image.jpg', 'localImage.jpg');

同步写入二进制文件在处理网络下载时不太常用,因为它会阻塞事件循环。

4. 文件和目录操作

4.1 创建目录

4.1.1 异步创建目录

使用 fs.mkdir 方法异步创建目录。

const fs = require('fs');

function createDirectoryAsync(directoryPath) {
    return new Promise((resolve, reject) => {
        fs.mkdir(directoryPath, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

createDirectoryAsync('newDirectory')
   .then(() => {
        console.log('Directory created successfully');
    })
   .catch(err => {
        console.error(err);
    });

4.1.2 同步创建目录

const fs = require('fs');

function createDirectorySync(directoryPath) {
    try {
        fs.mkdirSync(directoryPath);
        console.log('Directory created successfully');
    } catch (err) {
        console.error(err);
    }
}

createDirectorySync('newDirectory');

4.2 删除文件和目录

4.2.1 删除文件

删除文件使用 fs.unlink 方法(异步)或 fs.unlinkSync 方法(同步)。

const fs = require('fs');

function deleteFileAsync(filePath) {
    return new Promise((resolve, reject) => {
        fs.unlink(filePath, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

deleteFileAsync('unwantedFile.txt')
   .then(() => {
        console.log('File deleted successfully');
    })
   .catch(err => {
        console.error(err);
    });

同步删除文件如下:

const fs = require('fs');

function deleteFileSync(filePath) {
    try {
        fs.unlinkSync(filePath);
        console.log('File deleted successfully');
    } catch (err) {
        console.error(err);
    }
}

deleteFileSync('unwantedFile.txt');

4.2.2 删除目录

删除目录使用 fs.rmdir 方法(异步)或 fs.rmdirSync 方法(同步)。注意,目录必须为空才能删除。

const fs = require('fs');

function deleteDirectoryAsync(directoryPath) {
    return new Promise((resolve, reject) => {
        fs.rmdir(directoryPath, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

deleteDirectoryAsync('emptyDirectory')
   .then(() => {
        console.log('Directory deleted successfully');
    })
   .catch(err => {
        console.error(err);
    });

同步删除目录:

const fs = require('fs');

function deleteDirectorySync(directoryPath) {
    try {
        fs.rmdirSync(directoryPath);
        console.log('Directory deleted successfully');
    } catch (err) {
        console.error(err);
    }
}

deleteDirectorySync('emptyDirectory');

如果目录不为空,需要先递归删除目录内的所有文件和子目录。

4.3 重命名文件和目录

4.3.1 重命名文件

使用 fs.rename 方法(异步)或 fs.renameSync 方法(同步)。

const fs = require('fs');

function renameFileAsync(oldPath, newPath) {
    return new Promise((resolve, reject) => {
        fs.rename(oldPath, newPath, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

renameFileAsync('oldFile.txt', 'newFile.txt')
   .then(() => {
        console.log('File renamed successfully');
    })
   .catch(err => {
        console.error(err);
    });

同步重命名文件:

const fs = require('fs');

function renameFileSync(oldPath, newPath) {
    try {
        fs.renameSync(oldPath, newPath);
        console.log('File renamed successfully');
    } catch (err) {
        console.error(err);
    }
}

renameFileSync('oldFile.txt', 'newFile.txt');

4.3.2 重命名目录

重命名目录操作与重命名文件类似。

const fs = require('fs');

function renameDirectoryAsync(oldPath, newPath) {
    return new Promise((resolve, reject) => {
        fs.rename(oldPath, newPath, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

renameDirectoryAsync('oldDirectory', 'newDirectory')
   .then(() => {
        console.log('Directory renamed successfully');
    })
   .catch(err => {
        console.error(err);
    });

同步重命名目录:

const fs = require('fs');

function renameDirectorySync(oldPath, newPath) {
    try {
        fs.renameSync(oldPath, newPath);
        console.log('Directory renamed successfully');
    } catch (err) {
        console.error(err);
    }
}

renameDirectorySync('oldDirectory', 'newDirectory');

4.4 检查文件和目录是否存在

可以使用 fs.stat 方法(异步)或 fs.statSync 方法(同步)来检查文件或目录是否存在。

const fs = require('fs');

function checkExistsAsync(path) {
    return new Promise((resolve, reject) => {
        fs.stat(path, (err, stats) => {
            if (err) {
                if (err.code === 'ENOENT') {
                    resolve(false);
                } else {
                    reject(err);
                }
            } else {
                resolve(true);
            }
        });
    });
}

checkExistsAsync('example.txt')
   .then(exists => {
        if (exists) {
            console.log('File exists');
        } else {
            console.log('File does not exist');
        }
    })
   .catch(err => {
        console.error(err);
    });

同步检查:

const fs = require('fs');

function checkExistsSync(path) {
    try {
        fs.statSync(path);
        return true;
    } catch (err) {
        if (err.code === 'ENOENT') {
            return false;
        } else {
            console.error(err);
            return false;
        }
    }
}

const exists = checkExistsSync('example.txt');
if (exists) {
    console.log('File exists');
} else {
    console.log('File does not exist');
}

5. 高级文件操作技巧

5.1 流式操作文件

流式操作允许逐块处理文件,而不是一次性读取或写入整个文件。这在处理大文件时非常有用,因为它可以减少内存使用。

5.1.1 流式读取文件

const fs = require('fs');

const readableStream = fs.createReadStream('largeFile.txt', 'utf8');

readableStream.on('data', chunk => {
    console.log('Received a chunk of data:', chunk.length);
    // 处理数据块,例如解析、过滤等
});

readableStream.on('end', () => {
    console.log('All data has been read');
});

readableStream.on('error', err => {
    console.error(err);
});

5.1.2 流式写入文件

const fs = require('fs');

const dataToWrite = 'This is a large amount of data to be written to the file';
const writeStream = fs.createWriteStream('largeOutputFile.txt', 'utf8');

writeStream.write(dataToWrite.slice(0, 1000));
writeStream.write(dataToWrite.slice(1000, 2000));
writeStream.end(dataToWrite.slice(2000));

writeStream.on('finish', () => {
    console.log('All data has been written');
});

writeStream.on('error', err => {
    console.error(err);
});

5.1.3 管道操作

管道操作可以将一个可读流直接连接到一个可写流,实现高效的数据传输。

const fs = require('fs');

const readableStream = fs.createReadStream('inputFile.txt');
const writeStream = fs.createWriteStream('outputFile.txt');

readableStream.pipe(writeStream);

readableStream.on('error', err => {
    console.error('Read error:', err);
});

writeStream.on('error', err => {
    console.error('Write error:', err);
});

5.2 监控文件变化

使用 fs.watchfs.watchFile 方法可以监控文件或目录的变化。

5.2.1 使用 fs.watch

const fs = require('fs');

const watcher = fs.watch('directoryToWatch', (eventType, filename) => {
    if (eventType === 'change') {
        console.log(`${filename} has been changed`);
    } else if (eventType === 'rename') {
        console.log(`${filename} has been renamed`);
    }
});

// 停止监控
setTimeout(() => {
    watcher.close();
    console.log('Stopped watching');
}, 10000);

5.2.2 使用 fs.watchFile

const fs = require('fs');

const filePath = 'fileToWatch.txt';
const statsBefore = fs.statSync(filePath);

fs.watchFile(filePath, (curr, prev) => {
    if (curr.mtime.getTime()!== prev.mtime.getTime()) {
        console.log('File has been modified');
    }
});

5.3 处理文件权限

在 Node.js 中,可以使用 fs.chmod 方法(异步)或 fs.chmodSync 方法(同步)来更改文件或目录的权限。

const fs = require('fs');

function changeFilePermissionsAsync(filePath, mode) {
    return new Promise((resolve, reject) => {
        fs.chmod(filePath, mode, err => {
            if (err) {
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

changeFilePermissionsAsync('example.txt', 0o644)
   .then(() => {
        console.log('File permissions changed successfully');
    })
   .catch(err => {
        console.error(err);
    });

同步更改权限:

const fs = require('fs');

function changeFilePermissionsSync(filePath, mode) {
    try {
        fs.chmodSync(filePath, mode);
        console.log('File permissions changed successfully');
    } catch (err) {
        console.error(err);
    }
}

changeFilePermissionsSync('example.txt', 0o644);

6. 错误处理与最佳实践

6.1 错误处理策略

在文件操作中,错误处理至关重要。常见的错误包括文件不存在、权限不足、磁盘空间不足等。

6.1.1 同步操作的错误处理

在同步操作中,使用 try...catch 块捕获错误。

const fs = require('fs');

try {
    const data = fs.readFileSync('nonexistentFile.txt', 'utf8');
    console.log(data);
} catch (err) {
    if (err.code === 'ENOENT') {
        console.log('File does not exist');
    } else if (err.code === 'EACCES') {
        console.log('Permission denied');
    } else {
        console.error(err);
    }
}

6.1.2 异步操作的错误处理

在异步操作中,通过回调函数或 Promisecatch 块处理错误。

const fs = require('fs');

function readFileAsync(filePath) {
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, 'utf8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

readFileAsync('nonexistentFile.txt')
   .then(data => {
        console.log(data);
    })
   .catch(err => {
        if (err.code === 'ENOENT') {
            console.log('File does not exist');
        } else if (err.code === 'EACCES') {
            console.log('Permission denied');
        } else {
            console.error(err);
        }
    });

6.2 最佳实践建议

  • 避免阻塞操作:在可能的情况下,尽量使用异步操作,特别是在处理 I/O 密集型任务时,以防止阻塞 Node.js 的事件循环。
  • 合理使用流式操作:对于大文件,使用流式操作可以显著减少内存占用,提高应用程序的性能。
  • 权限管理:在创建或修改文件时,谨慎设置文件权限,确保安全性。
  • 错误处理:始终对文件操作进行全面的错误处理,以提供友好的用户反馈并防止应用程序崩溃。
  • 资源释放:在使用完文件描述符或流后,确保正确关闭它们,以释放系统资源。例如,使用 stream.end()stream.close() 方法。

通过遵循这些最佳实践,可以在 Node.js 中高效、安全地进行文件操作,构建稳定可靠的应用程序。无论是开发 Web 服务器、命令行工具还是其他类型的 Node.js 应用,熟练掌握文件操作都是必不可少的技能。