浏览器端的 JavaScript 无法直接读取服务器上的文件夹,真正干活的是 Node.js:用 fs.readdir 拿到目录内容,用 path.join(__dirname, '目标目录') 拼出可靠路径,再把结果通过接口交给前端。 下面按从小白到实战的顺序,把这条链路拆开讲清楚。
浏览器里的 JS 为什么摸不到服务器文件夹
前端 js 能直接读取服务器本地文件夹吗
不能,这是浏览器安全沙箱决定的,你用 <input type="file" webkitdirectory> 选择文件夹,拿到的只是用户主动授权的 FileList,路径还被替换成 C:\fakepath\...,浏览器没有权限、也没有通道去访问服务器的 D:\data\uploads。
行业共识认为,这条边界短期内不会松动,想读服务器目录,只有三种路子:
- Node.js 后端自己读,比如定时扫目录、生成文件清单。
- 后端开接口,前端请求
/api/files?dir=xxx拿列表。 - 直接把目录挂成静态资源,前端按 URL 访问具体文件。
前两种用得最多,记住一句话:路径字符串是在服务端拼的,浏览器只负责发请求。
Node.js 读取服务器文件夹路径的基本写法
绝对路径、相对路径和 __dirname
先分清三种“起点”:
- 绝对路径:
/var/www/uploads或C:\app\data,从盘符或根开始,任何位置都能用。 - 相对路径:
./uploads,起点是当前工作目录(process.cwd()),不是脚本所在目录,很多人在这里栽跟头。 __dirname:当前脚本文件所在的绝对目录,CommonJS 里直接可用;ESM 里要用import.meta.dirname(Node 20.11+)或fileURLToPath(import.meta.url)。
典型的坑:用 pm2 start app.js 启动,进程工作目录是 /root,fs.readdirSync('./uploads') 就找不到文件了,改成下面这样立刻稳:
const path = require('path');
const fs = require('fs');
const dir = path.join(__dirname, 'uploads');
fs.readdir(dir, (err, files) => {
if (err) return console.error(err);
console.log(files);
});
path.join 和 path.resolve 差在哪
两个函数都返回规范化路径,但语义不同:
| 对比项 | path.join | path.resolve |
|---|---|---|
| 行为 | 按顺序拼接片段并规范化 | 从右往左拼接,直到拼出绝对路径 |
| 遇到绝对路径片段 | 继续拼接 | 立刻以该绝对路径为准 |
| 起点 | 不涉及 | 未生成绝对路径时以 process.cwd() 为底 |
| 适用场景 | 已知父目录,拼子目录 | 需要得到绝对路径,或处理用户传入路径 |
日常拼目录用 path.join 就够,结果清晰、没有歧义。path.resolve('a', '/b') 会返回 /b,很多人第一次见会愣一下。
Node.js 读取服务器目录文件列表的完整方法
fs.readdirSync 和 fs.readdir 该选哪个
同步和异步,看你在哪用:
readdirSync:脚本、CLI 工具、启动时一次性扫描目录,代码短,直接返回数组。readdir:回调式异步,适合老项目,避免阻塞事件循环。fs.promises.readdir/fs.readdirSync之外的await:现代写法,配合async/await最舒服。
const { readdir } = require('fs/promises');
async function listDir(dir) {
const entries = await readdir(dir, { withFileTypes: true });
return entries.map(e => ({
name: e.name,
isDir: e.isDirectory(),
size: e.isFile() ? require('fs').statSync(require('path').join(dir, e.name)).size : null
}));
}
关键在 withFileTypes: true,不加这个参数,你只拿到名字,还得逐个 statSync 判断是文件还是文件夹,IO 次数翻倍。
递归读取子目录
readdir 本身不递归,要拿整棵目录树,两条路:
fs.readdir(dir, { recursive: true }):Node 20.1+ 支持,一行搞定,返回相对路径数组。- 自己写递归:兼容老版本,顺便能控制深度和过滤规则。
function walk(dir, depth = 0, maxDepth = 3) {
if (depth > maxDepth) return [];
const items = fs.readdirSync(dir, { withFileTypes: true });
return items.flatMap(item => {
const full = path.join(dir, item.name);
return item.isDirectory() ? walk(full, depth + 1, maxDepth) : [full];
});
}
加了 maxDepth,是为了防止误扫 node_modules 或系统目录,把进程拖死。
让前端拿到文件夹内容:接口和静态目录
Express 提供文件列表接口
后端读目录,前端发请求,这是最常见的组合,核心是路径校验,不能让用户传什么就读什么。
app.get('/api/files', async (req, res) => {
const sub = req.query.dir || '';
const base = path.join(__dirname, 'uploads');
const target = path.join(base, sub);
if (!target.startsWith(base + path.sep)) {
return res.status(403).json({ error: '非法路径' });
}
const list = await fs.promises.readdir(target, { withFileTypes: true });
res.json(list.map(d => d.name));
});
path.join 会自动处理 ,所以必须用 startsWith 做前缀校验,否则 ?dir=../../etc 就能读到服务器任意目录。
静态资源托管的路径配置
如果只是想让前端能下载文件,不必写接口,直接挂静态目录:
app.use('/files', express.static(path.join(__dirname, 'uploads')));
前端访问 /files/report.pdf 即可,注意别把整个项目根目录挂上去,等于把源码和配置全暴露了。
路径处理最容易踩的坑
- Windows 和 Linux 分隔符不同:写死
'a/b'在 Windows 上也能跑,但'a\\b'到 Linux 就废了,统一用path.join,它会按平台输出正确分隔符。 - 中文路径乱码:Node 默认按 UTF-8 处理文件名,Linux 服务器 locale 如果是
C,读出来的中文名可能是乱码,部署时确认LANG=zh_CN.UTF-8或en_US.UTF-8。 - 权限不足:
EACCES报错不是路径写错了,是运行进程的用户没读权限,查一下ls -l的属主。 - 路径不存在:
ENOENT要单独处理,别让整个服务挂掉,读之前用fs.existsSync判断,或直接 try/catch。 - 符号链接:
readdir不跟随软链,目录软链会被当成文件,需要穿透时加fs.realpath。 - ESM 里没有
__dirname:"type": "module"的项目里,用import.meta.dirname或path.dirname(fileURLToPath(import.meta.url))。
据 Node.js 官方文档说明,fs 模块的所有路径参数都接受 Buffer 和 URL,但传 file:// 开头的 URL 时必须用 url.fileURLToPath 转换,直接传字符串会抛 TypeError。
js 读取服务器文件夹路径的常见问题
为什么我写的相对路径在本地能跑,部署到服务器就报错
因为相对路径的起点是进程工作目录,不是脚本位置,本地你在项目根目录敲 node app.js,工作目录恰好等于项目根目录;线上用 systemd、pm2 或 Docker 启动时,工作目录往往变成 或 /root,解决办法只有一个:涉及读文件的路径,一律用 __dirname 或 path.resolve 锚定,别依赖 。
读取大文件夹时程序卡住或者内存飙升怎么办
readdir 一次性返回整个目录的数组,几十万文件就会吃满内存,换成流式方案:fs.opendir 返回 Dir 对象,可以用 for await...of 逐个拿 Dirent,边读边处理,内存占用稳定在一个文件的量级,同时给递归加深度限制,跳过 .git、node_modules 这类目录,扫描速度能提升明显,业内专家指出,目录遍历的性能瓶颈通常在 stat 调用次数,而不是 readdir 本身,所以能用 withFileTypes 就别额外调 stat。
路径里带空格或者特殊字符会不会出问题
Node 层面对空格、中文、、& 都能正常处理,path.join('a b', 'c d.txt') 没问题,真正出问题的是把路径塞进 URL 或 shell 命令的时候:URL 里要 encodeURIComponent,shell 里要加引号或用 execFile 传数组参数,直接字符串拼接 exec('ls ' + dir),一个文件名带分号就能执行任意命令。
总结一句:浏览器只发请求,Node.js 用 path.join(__dirname, ...) 定路径、用 fs.readdir 读目录,再通过接口或静态托管把结果交给前端。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/785830.html


