精通 NJS 文件系统模块(fs):NGINX 网关层的文件操作全指南
手机扫码查看
一、fs 模块核心定位与基础使用
1.1 模块导入方式
fs 模块需显式导入后使用,支持同步 API 和异步 Promise API 两种风格:
// 基础导入(同步 API + 异步 Promise API)
import fs from 'fs';
// 仅使用异步 Promise API(推荐异步场景)
const fsPromises = fs.promises;
1.2 核心能力分类
| 功能分类 | 核心 API | 适用场景 |
|---|---|---|
| 文件读写 | readFileSync()/writeFileSync()、fs.promises.readFile()/fs.promises.writeFile() |
配置文件读取、日志写入 |
| 文件描述符操作 | openSync()/closeSync()、readSync()/writeSync()、fs.promises.open() |
大文件分块读写 |
| 目录操作 | mkdirSync()/rmdirSync()、readdirSync() |
目录创建/删除、目录内容遍历 |
| 文件元信息 | statSync()/lstatSync()、fstatSync() |
文件类型判断、大小/修改时间获取 |
| 文件权限/存在性 | accessSync()、existsSync() |
权限校验、文件存在性检查 |
| 其他操作 | renameSync()、unlinkSync()、symlinkSync() |
文件重命名、删除、创建符号链接 |
二、核心 API 详解
2.1 文件读写:基础同步操作
2.1.1 读取文件:readFileSync()
同步读取文件内容,支持指定编码(返回字符串)或默认返回 Buffer:
// 示例1:读取文本文件(指定 UTF-8 编码,返回字符串)
const config = fs.readFileSync('/etc/nginx/conf.d/app.conf', { encoding: 'utf8' });
console.log("配置文件内容:", config);
// 示例2:读取二进制文件(无编码,返回 Buffer)
const binaryData = fs.readFileSync('/var/www/static/file.tar.gz');
console.log("文件前 2 字节:", binaryData.slice(0,2).toString('hex')); // 输出:1f8b(GZIP 标识)
// 示例3:指定文件标志(只读模式)
const data = fs.readFileSync('/tmp/log.txt', { flag: 'r', encoding: 'utf8' });
2.1.2 写入文件:writeFileSync()
同步写入文件,文件不存在则创建,存在则覆盖(默认 flag: 'w'):
// 示例1:写入文本文件(默认 UTF-8 编码)
fs.writeFileSync('/tmp/hello.txt', 'Hello NJS fs module');
// 示例2:追加写入(修改 flag 为 'a')
fs.writeFileSync('/tmp/log.txt', 'New access log\n', { flag: 'a', encoding: 'utf8' });
// 示例3:写入二进制数据(Buffer)
const buffer = Buffer.from([0x89, 0x50, 0x4E, 0x47]); // PNG 文件头
fs.writeFileSync('/tmp/test.png', buffer);
2.1.3 追加写入:appendFileSync()
同步追加数据到文件末尾,文件不存在则创建:
// 追加文本内容
fs.appendFileSync('/tmp/access.log', `[${new Date()}] Client IP: 192.168.1.1\n`);
// 追加 Buffer 数据
fs.appendFileSync('/tmp/bin.log', Buffer.from('binary data'), { mode: 0o644 });
2.2 文件读写:异步 Promise 操作(推荐)
0.3.9+ 支持 Promise 风格异步 API,避免同步操作阻塞 NGINX Worker 进程:
// 异步读取文件
async function readConfigAsync(r) {
try {
const config = await fs.promises.readFile('/etc/nginx/app.json', { encoding: 'utf8' });
return JSON.parse(config);
} catch (e) {
r.error(`读取配置失败:${e.message}`);
return null;
}
}
// 异步写入文件
async function writeLogAsync(r, logContent) {
try {
await fs.promises.appendFile('/tmp/async.log', `${logContent}\n`, { encoding: 'utf8' });
r.log("日志写入成功");
} catch (e) {
r.error(`日志写入失败:${e.message}`);
}
}
2.3 文件描述符操作:高级读写
通过文件描述符(fd)实现分块读写,适用于大文件操作:
// 示例:分块读取大文件
function readLargeFile(r, filePath) {
// 1. 打开文件(获取文件描述符)
const fd = fs.openSync(filePath, 'r');
const buffer = Buffer.alloc(1024); // 1KB 缓冲区
let bytesRead = 0;
let totalRead = 0;
try {
// 2. 循环分块读取
while ((bytesRead = fs.readSync(fd, buffer, 0, buffer.length, null)) > 0) {
const chunk = buffer.slice(0, bytesRead).toString('utf8');
r.log(`读取 ${bytesRead} 字节:${chunk}`);
totalRead += bytesRead;
}
r.log(`文件读取完成,总大小:${totalRead} 字节`);
} catch (e) {
r.error(`文件读取失败:${e.message}`);
} finally {
// 3. 关闭文件描述符(必须执行,避免内存泄漏)
fs.closeSync(fd);
}
}
// 示例:通过 FileHandle 异步操作(0.7.7+)
async function fileHandleDemo(r) {
let fileHandle;
try {
// 打开文件获取 FileHandle
fileHandle = await fs.promises.open('/tmp/test.txt', 'r+');
r.log(`文件描述符:${fileHandle.fd}`);
// 读取文件
const buffer = Buffer.alloc(1024);
const { bytesRead } = await fileHandle.read(buffer, 0, buffer.length, 0);
r.log(`读取字节数:${bytesRead}`);
// 写入文件
await fileHandle.write('Async write via FileHandle', null, 'utf8');
} catch (e) {
r.error(`FileHandle 操作失败:${e.message}`);
} finally {
// 关闭 FileHandle(推荐显式关闭)
if (fileHandle) await fileHandle.close();
}
}
2.4 目录操作
2.4.1 创建/删除目录:mkdirSync()/rmdirSync()
// 创建目录(默认权限 0o777)
fs.mkdirSync('/tmp/njs_dir', { mode: 0o755 });
// 删除空目录
try {
fs.rmdirSync('/tmp/njs_dir');
} catch (e) {
console.error("删除目录失败:", e.message); // 目录非空时抛出异常
}
2.4.2 遍历目录:readdirSync()
同步读取目录内容,支持返回文件名或 fs.Dirent 对象(文件类型信息):
// 示例1:仅读取文件名(默认)
const files = fs.readdirSync('/var/www/static');
console.log("目录文件列表:", files); // 输出:["index.html", "style.css", "img"]
// 示例2:读取文件类型信息(返回 fs.Dirent 对象)
const dirents = fs.readdirSync('/var/www/static', { withFileTypes: true });
dirents.forEach(dirent => {
console.log(`名称:${dirent.name}`);
console.log(`是否为文件:${dirent.isFile()}`);
console.log(`是否为目录:${dirent.isDirectory()}`);
console.log(`是否为符号链接:${dirent.isSymbolicLink()}`);
});
2.5 文件元信息与存在性检查
2.5.1 文件元信息:statSync()/lstatSync()
statSync():获取文件/目录元信息(跟随符号链接);lstatSync():获取符号链接本身的元信息(不跟随链接)。
// 获取文件元信息
const stats = fs.statSync('/tmp/hello.txt');
// 常用元信息判断
console.log(`是否为文件:${stats.isFile()}`);
console.log(`是否为目录:${stats.isDirectory()}`);
console.log(`文件大小(字节):${stats.size}`);
console.log(`最后修改时间:${stats.mtime}`);
console.log(`创建时间:${stats.birthtime}`);
// 符号链接示例(lstatSync 不跟随链接)
fs.symlinkSync('/tmp/hello.txt', '/tmp/hello.link');
const linkStats = fs.lstatSync('/tmp/hello.link');
console.log(`是否为符号链接:${linkStats.isSymbolicLink()}`); // 输出:true
const fileStats = fs.statSync('/tmp/hello.link');
console.log(`是否为符号链接:${fileStats.isSymbolicLink()}`); // 输出:false(跟随到原文件)
2.5.2 存在性/权限检查:existsSync()/accessSync()
existsSync():简单检查文件/目录是否存在(0.8.2+);accessSync():校验文件权限(读/写/执行),更精细。
// 检查文件是否存在
if (fs.existsSync('/tmp/hello.txt')) {
console.log("文件存在");
}
// 检查文件读写权限
try {
// 校验读(R_OK)+ 写(W_OK)权限
fs.accessSync('/tmp/hello.txt', fs.constants.R_OK | fs.constants.W_OK);
console.log("拥有读写权限");
} catch (e) {
console.log("无读写权限:", e.message);
}
2.6 其他常用操作
2.6.1 文件重命名/删除:renameSync()/unlinkSync()
// 重命名文件
fs.renameSync('/tmp/hello.txt', '/tmp/hello_new.txt');
// 删除文件(注意:不能删除目录,目录需用 rmdirSync())
fs.unlinkSync('/tmp/hello_new.txt');
2.6.2 创建符号链接:symlinkSync()
// 创建符号链接(相对路径基于链接父目录)
fs.symlinkSync('/etc/nginx/nginx.conf', '/tmp/nginx.conf.link');
// 读取符号链接指向的目标
const linkTarget = fs.readlinkSync('/tmp/nginx.conf.link', { encoding: 'utf8' });
console.log("符号链接目标:", linkTarget); // 输出:/etc/nginx/nginx.conf
三、核心数据对象
3.1 fs.Dirent:目录项对象
由 readdirSync({ withFileTypes: true }) 返回,描述目录中单个项的类型信息:
| 方法 | 说明 |
|---|---|
isFile() |
是否为普通文件 |
isDirectory() |
是否为目录 |
isSymbolicLink() |
是否为符号链接 |
isBlockDevice()/isCharacterDevice() |
是否为块设备/字符设备 |
isFIFO()/isSocket() |
是否为管道/套接字 |
name |
目录项名称(只读) |
3.2 fs.Stats:文件元信息对象
由 statSync()/lstatSync()/fstatSync() 返回,包含文件的完整元信息:
| 属性/方法 | 说明 |
|---|---|
size |
文件大小(字节) |
atime/mtime/ctime/birthtime |
访问时间/修改时间/状态变更时间/创建时间 |
uid/gid |
文件所属用户/组 ID(POSIX 系统) |
isFile()/isDirectory()/isSymbolicLink() |
类型判断(同 Dirent) |
dev/ino/mode |
设备 ID/Inode 号/文件权限位 |
3.3 fs.FileHandle:文件句柄对象(0.7.7+)
由 fs.promises.open() 返回,封装文件描述符的异步操作:
| 方法/属性 | 说明 |
|---|---|
fd |
底层文件描述符(整数) |
read()/write() |
异步读取/写入文件 |
stat() |
异步获取文件元信息 |
close() |
异步关闭文件句柄(必须显式调用) |
四、实战场景:NJS 中的 fs 模块应用
4.1 场景1:读取配置文件并动态生效
在 NGINX 网关层读取 JSON 配置文件,根据配置动态调整请求处理逻辑:
# NGINX 配置
http {
js_import config.js;
server {
listen 80;
location /api {
js_content config.handleRequest;
}
}
}
// config.js
import fs from 'fs';
// 缓存配置(避免每次请求读取文件)
let appConfig = null;
const CONFIG_PATH = '/etc/nginx/app-config.json';
// 加载配置文件
function loadConfig() {
try {
const raw = fs.readFileSync(CONFIG_PATH, { encoding: 'utf8' });
appConfig = JSON.parse(raw);
console.log("配置加载成功:", appConfig);
} catch (e) {
console.error("配置加载失败:", e.message);
appConfig = { allowIps: [], maxRequests: 1000 }; // 默认配置
}
}
// 初始化加载配置
loadConfig();
// 处理请求:根据配置校验客户端 IP
function handleRequest(r) {
const clientIp = r.remoteAddress;
// 检查 IP 是否在允许列表中
if (appConfig.allowIps.length > 0 && !appConfig.allowIps.includes(clientIp)) {
r.return(403, `IP ${clientIp} 无访问权限`);
return;
}
r.return(200, `请求成功,配置最大请求数:${appConfig.maxRequests}`);
}
export default { handleRequest };
4.2 场景2:记录访问日志到本地文件
异步记录请求信息到日志文件,避免同步写入阻塞请求处理:
// log.js
import fs from 'fs';
const fsPromises = fs.promises;
const LOG_PATH = '/var/log/nginx/njs-access.log';
// 异步写入访问日志
async function writeAccessLog(r) {
const logEntry = `[${new Date().toISOString()}] ${r.method} ${r.uri} ${r.remoteAddress} ${r.status}\n`;
try {
await fsPromises.appendFile(LOG_PATH, logEntry, { encoding: 'utf8' });
} catch (e) {
r.error(`日志写入失败:${e.message}`);
}
}
// 请求处理完成后写入日志
function handleRequest(r) {
r.return(200, "Hello World");
// 异步写入日志,不阻塞响应返回
writeAccessLog(r);
}
export default { handleRequest };
4.3 场景3:校验静态资源元信息
读取静态文件的大小、修改时间等元信息,添加到响应头:
// static.js
import fs from 'fs';
function setFileMetaHeaders(r) {
const filePath = `/var/www/static${r.uri}`;
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
r.return(404, "File not found");
return;
}
// 获取文件元信息
const stats = fs.statSync(filePath);
// 设置响应头
r.headersOut['Content-Length'] = stats.size;
r.headersOut['Last-Modified'] = stats.mtime.toUTCString();
r.headersOut['X-File-Type'] = stats.isFile() ? 'file' : 'directory';
// 返回文件内容
const content = fs.readFileSync(filePath);
r.return(200, content);
}
export default { setFileMetaHeaders };
五、版本兼容与最佳实践
5.1 核心版本兼容要点
- 0.3.9+:支持
fs.promises异步 Promise API、symlinkSync()、accessSync(); - 0.4.2+:支持
mkdirSync()/rmdirSync()/readdirSync(); - 0.4.4+:
readFileSync()/writeFileSync()支持 Buffer 类型、编码扩展(hex/base64/base64url); - 0.7.1+:支持
statSync()/lstatSync()、fs.Stats对象; - 0.7.7+:支持文件描述符操作(
openSync()/readSync()/writeSync())、fs.FileHandle; - 0.8.2+:支持
existsSync(); - 0.8.7+:支持
readlinkSync()。
5.2 最佳实践
- 异步优先:NGINX Worker 进程为单线程,同步 API 会阻塞请求处理,建议优先使用
fs.promises异步 API; - 缓存优化:配置文件等高频读取的文件,加载后缓存到内存,避免每次请求重复读取;
- 错误处理:所有文件操作必须包裹
try/catch,避免文件不存在、权限不足等异常导致脚本崩溃; - 权限控制:确保 NGINX 运行用户(通常为
www-data/nginx)拥有文件/目录的读写权限; - 大文件处理:大文件(>1MB)建议使用文件描述符分块读写(
readSync()/writeSync()),避免一次性读取占用过多内存; - 显式关闭资源:使用
openSync()/fs.promises.open()获取的文件描述符/FileHandle,必须显式关闭(closeSync()/filehandle.close()),避免资源泄漏。
六、总结
- NJS
fs模块提供了完整的文件系统操作能力,支持同步 API(简单场景)和异步 Promise API(高性能场景),核心覆盖文件读写、目录操作、元信息获取等; - 核心数据对象(
fs.Dirent/fs.Stats/fs.FileHandle)提供了文件/目录的详细信息,是判断文件类型、获取元数据的关键; - 实战中优先使用异步 API 避免阻塞 NGINX 进程,同时做好缓存、错误处理和资源释放;
- 使用时需注意版本兼容,不同 NJS 版本对 API 的支持范围不同,建议升级到 0.7.7+ 以获得完整功能。
书签篮