首页 / 专栏 / njs / 精通 NJS 文件系统模块(fs):NGINX 网关层的文件操作全指南

精通 NJS 文件系统模块(fs):NGINX 网关层的文件操作全指南

  • 发布时间: 2026-01-10 23:19:11
  • 相关标签: NJS 文件系统 网关层 文件
  • 简介: NJS(NGINX JavaScript)的 `fs` 模块为 NGINX 网关层提供了完整的文件系统操作能力,支持文件读写、目录管理、权限校验、符号链接处理等核心功能,同时兼容同步 API 和异步 Promise API(0.3.9+)。无论是读取配置文件、记录访问日志,还是处理静态资源元信息,`fs` 模块都是 NJS 实现本地文件交互的核心工具。本文将全面解析 `fs` 模块的核心 API、数据对象及实战场景,结合版本兼容要点讲解最佳实践。

手机扫码查看

一、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 核心版本兼容要点

  1. 0.3.9+:支持 fs.promises 异步 Promise API、symlinkSync()accessSync()
  2. 0.4.2+:支持 mkdirSync()/rmdirSync()/readdirSync()
  3. 0.4.4+:readFileSync()/writeFileSync() 支持 Buffer 类型、编码扩展(hex/base64/base64url);
  4. 0.7.1+:支持 statSync()/lstatSync()fs.Stats 对象;
  5. 0.7.7+:支持文件描述符操作(openSync()/readSync()/writeSync())、fs.FileHandle
  6. 0.8.2+:支持 existsSync()
  7. 0.8.7+:支持 readlinkSync()

5.2 最佳实践

  1. 异步优先:NGINX Worker 进程为单线程,同步 API 会阻塞请求处理,建议优先使用 fs.promises 异步 API;
  2. 缓存优化:配置文件等高频读取的文件,加载后缓存到内存,避免每次请求重复读取;
  3. 错误处理:所有文件操作必须包裹 try/catch,避免文件不存在、权限不足等异常导致脚本崩溃;
  4. 权限控制:确保 NGINX 运行用户(通常为 www-data/nginx)拥有文件/目录的读写权限;
  5. 大文件处理:大文件(>1MB)建议使用文件描述符分块读写(readSync()/writeSync()),避免一次性读取占用过多内存;
  6. 显式关闭资源:使用 openSync()/fs.promises.open() 获取的文件描述符/FileHandle,必须显式关闭(closeSync()/filehandle.close()),避免资源泄漏。

六、总结

  1. NJS fs 模块提供了完整的文件系统操作能力,支持同步 API(简单场景)和异步 Promise API(高性能场景),核心覆盖文件读写、目录操作、元信息获取等;
  2. 核心数据对象(fs.Dirent/fs.Stats/fs.FileHandle)提供了文件/目录的详细信息,是判断文件类型、获取元数据的关键;
  3. 实战中优先使用异步 API 避免阻塞 NGINX 进程,同时做好缓存、错误处理和资源释放;
  4. 使用时需注意版本兼容,不同 NJS 版本对 API 的支持范围不同,建议升级到 0.7.7+ 以获得完整功能。

同分类推荐