ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用 Node.js 核心模块打造零依赖静态文件服务器:30 seconds of code 实战指南

用 Node.js 核心模块打造零依赖静态文件服务器:30 seconds of code 实战指南 教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载导读本文基于 30 seconds of code 仓库中的经典教程content/snippets/js/s/nodejs-static-file-server.md完整讲解如何仅用 Node.js 内置的http、fs、path三个核心模块在约 70 行代码内构建一个可用的静态文件服务器。读完本文你将掌握静态文件服务器的基本原理、目录隔离与路径穿越防护、文件类型白名单、HTML 扩展名省略等关键能力并得到一份可直接复制运行、适合作为第一个 Node.js 后端练习项目的完整实现。背景为什么从静态文件服务器开始静态文件服务器是最简单的后端入门项目之一它监听 HTTP 请求尝试将请求的 URL 与本地文件系统中的文件进行匹配并把文件内容作为响应返回。整个过程只用到了 Node.js 自带的模块不需要安装任何第三方依赖却能覆盖请求-响应模型、事件循环、文件系统 I/O、路径安全等核心概念。这与 30 seconds of code 仓库的定位高度一致——仓库专门维护了一个 Node.js 文章集合其描述明确指出该集合面向Node.js 18.x的服务器端代码与文件系统操作并且所有文章统一使用ESM 语法。仓库根目录的 package.json 也印证了这一点项目声明了type: moduleengines要求node 22.14.0。因此下文所有代码均采用import写法在当前 Node.js 环境下可以直接运行。第一步一个最简单的静态文件服务器一切从最朴素的形式开始。核心思路只有两步用http.createServer()创建服务器用fs.readFile()按请求路径读取文件import { readFile } from fs; import { createServer } from http; createServer((req, res) { readFile(__dirname req.url, (err, data) { if (err) { res.writeHead(404, { Content-Type: text/html }); res.end(404: File not found); } else { res.writeHead(200, { Content-Type: text/html }); res.end(data); } }); }).listen(8000);这段代码的逻辑一目了然fs模块负责读取__dirname req.url对应的文件。__dirname是当前脚本所在目录req.url是请求路径以/开头。文件不存在时readFile的回调会收到err此时返回404状态码与错误提示文件存在时返回200状态码并输出文件内容http模块创建服务器并监听8000端口。严格来说到这里已经是一个能工作的静态文件服务器了。但正如教程所指出的它存在几个明显的问题文件只能从脚本所在目录提供、存在严重的安全隐患、不支持常见的 URL 省略扩展名习惯。下面逐项解决。值得一提的是30 seconds of code 仓库自身的开发服务器恰好也跑在8000端口见 package.json 中的astro dev --port 8000如果你在本机调试本教程的代码注意避免端口冲突。第二步模块化——让文件来自指定目录第一个问题是灵活性我们并不希望文件只能从服务器脚本所在目录提供。解决方案是指定一个公开目录如./public再用path模块把请求路径解析到这个目录之下。使用path模块还有一个额外的好处它内部封装了不同操作系统Windows 与 POSIX的路径分隔符差异让代码跨平台更稳健。import { readFile } from fs; import { join } from path; const directoryName ./public; const requestUrl index.html; const filePath join(directoryName, requestUrl); readFile(filePath, (err, data) { // ... });这里的核心 API 是path.join()它把多个路径片段拼接成一个完整路径并自动规范化其中的分隔符。通过把根目录 请求路径拼接成最终读取路径服务器的文件来源就从脚本目录解耦到了任意指定的静态目录。第三步安全——阻止路径穿越与敏感文件泄露3.1 路径穿越防护模块化之后一个新的安全风险浮出水面如果直接把用户输入拼进路径攻击者可以用GET /../../../之类的请求越过根目录读取机器上任意文件。防住这一招需要两步校验先把根目录和拼接后的文件路径都解析成绝对路径并规范化再检查最终路径是否确实以根目录开头。import { join, normalize, resolve } from path; const directoryName ./public; const root normalize(resolve(directoryName)); const requestUrl index.html; const filePath join(root, fileName); const isPathUnderRoot normalize(resolve(filePath)).startsWith(root);resolve()把相对路径解析为基于当前工作目录的绝对路径normalize()负责清理路径中的冗余片段如.、..、连续的斜杠让../../secret这类路径被算出来而不是直接拼接最后用startsWith(root)判断解析后的文件路径是否仍在根目录之内不在则直接拒绝。关于这个校验有一个值得补充的实现细节如果使用朴素的startsWith(root)根目录为/srv/public时路径/srv/public-evil/x也会通过校验前缀匹配而非目录边界匹配。更稳妥的写法是在根目录后拼接路径分隔符再判断即filePath.startsWith(root path.sep)配合path.sep保证跨平台正确性。实战中建议按此方式收紧边界。3.2 文件类型白名单路径防护解决了越权读文件问题但还不能阻止用户读取根目录内的敏感文件比如.env、id_rsa之类。因此还要对文件扩展名做白名单校验。思路是用path.extname()取出扩展名去掉开头的.再检查它是否在支持的类型列表内import { extname } from path; const types [html, css, js, json]; const requestUrl index.html; const extension extname(requestUrl).slice(1); const isTypeSupported types.includes(extension);extname(index.html)返回.htmlslice(1)去掉点号得到html随后在types数组中查找即可。不在白名单内的扩展名如.php、.env会直接被拒绝返回404。第四步省略 HTML 扩展名——用户习惯的体验优化大多数网站的标配能力是请求/my-page时自动匹配到对应的 HTML 文件用户无需在 URL 里敲.html。为静态服务器加上这个能力并不复杂但有一个必须处理的歧义/my-page可能匹配my-page.html也可能匹配my-page/index.html。教程给出的取舍是优先匹配/my-page.html反过来也只需要调换一下判断顺序。实现思路先处理根路径/固定映射到index.html对没有扩展名的请求先用fs.accessSync()探测xxx.html是否存在存在则用它否则回退到xxx/index.htmlimport { accessSync, constants } from fs; import { join, normalize, resolve, extname } from path; const directoryName ./public; const root normalize(resolve(directoryName)); const extension extname(req.url).slice(1); let fileName requestUrl; if (requestUrl /) fileName index.html; else if (!extension) { try { accessSync(join(root, requestUrl .html), constants.F_OK); fileName requestUrl .html; } catch (e) { fileName join(requestUrl, index.html); } }这里用到了fs.accessSync()与constants.F_OKF_OK表示检查文件是否存在这一访问模式accessSync()在文件不存在时会抛出异常恰好被try/catch用来做存在性探测。需要说明的是accessSync是同步 API在高并发场景下会阻塞事件循环本教程以简洁优先如需更高吞吐可以换成fs.promises.access()或fs.access()的异步版本。最终整合完整的静态文件服务器把以上能力拼装起来再补上两个收尾细节——请求日志与更完整的 MIME 类型映射——就得到了最终成品。这里对文件类型的处理从数组升级为扩展名 → MIME 类型的映射表好处是可以在响应头里返回正确的Content-Type例如 HTML 是text/htmlPNG 是image/png浏览器才能正确渲染图片、样式与脚本import { readFile, accessSync, constants } from fs; import { createServer } from http; import { join, normalize, resolve, extname } from path; const port 8000; const directoryName ./public; const types { html: text/html, css: text/css, js: application/javascript, png: image/png, jpg: image/jpeg, jpeg: image/jpeg, gif: image/gif, json: application/json, xml: application/xml, }; const root normalize(resolve(directoryName)); const server createServer((req, res) { console.log(${req.method} ${req.url}); const extension extname(req.url).slice(1); const type extension ? types[extension] : types.html; const supportedExtension Boolean(type); if (!supportedExtension) { res.writeHead(404, { Content-Type: text/html }); res.end(404: File not found); return; } let fileName req.url; if (req.url /) fileName index.html; else if (!extension) { try { accessSync(join(root, req.url .html), constants.F_OK); fileName req.url .html; } catch (e) { fileName join(req.url, index.html); } } const filePath join(root, fileName); const isPathUnderRoot normalize(resolve(filePath)).startsWith(root); if (!isPathUnderRoot) { res.writeHead(404, { Content-Type: text/html }); res.end(404: File not found); return; } readFile(filePath, (err, data) { if (err) { res.writeHead(404, { Content-Type: text/html }); res.end(404: File not found); } else { res.writeHead(200, { Content-Type: type }); res.end(data); } }); }); server.listen(port, () { console.log(Server is listening on port ${port}); });最终代码的处理流程可以归纳为一条清晰的管线记录日志打印每个请求的方法与路径类型校验根据扩展名查 MIME 映射表types.html作为无扩展名请求的默认类型兜底查不到type为undefined直接404URL 解析根路径/→index.html无扩展名请求按先xxx.html、后xxx/index.html的顺序解析路径防护解析出真实文件路径并校验其位于根目录之内读取响应readFile成功返回200与文件内容Content-Type用步骤 2 确定的类型失败返回404。注意请求日志这一行console.log(${req.method} ${req.url})顺带利用了 console-log-cheatsheet 这类技巧中常见的模板字符串写法是调试时排查问题最快的抓手。如何运行与验证准备静态目录在项目根目录创建public文件夹放入index.html、about.html以及若干 CSS/JS/图片文件保存代码将上述完整代码保存为server.jsESM 写法需 Node.js 支持或项目声明type: module参见仓库的 package.json启动服务器node server.js # 输出Server is listening on port 8000开发调试时也可以使用node --watch server.js文件变更后自动重启验证各条路径curl http://localhost:8000/ # 返回 public/index.html curl http://localhost:8000/about # 优先命中 about.html否则 about/index.html curl http://localhost:8000/style.css # 正确 MIME 类型 text/css curl http://localhost:8000/secret.env # 404不在类型白名单 curl http://localhost:8000/../package.json # 404路径穿越被拦截如果希望对服务器行为做自动化验证仓库中的 nodejs-test-module-introduction.md 展示了 Node.js 内置node:test模块的用法——无需第三方依赖即可为上述行为编写单元或集成测试与本项目零第三方依赖的风格一脉相承。延伸阅读文件系统与 HTTP 相关能力理解静态文件服务器之后还可以在本仓库中继续深入这些相关主题read-file-lines.mdfs.readFile()与fs.readFileSync()的差异、Buffer与编码处理正是理解本文readFile回调的基础json-to-file.mdfs.writeFileSync()写文件与浏览器端Blob下载方案的对照可用来扩展服务器的写入能力http-get-post-put-delete.mdHTTP 请求的客户端视角与本文的服务器视角互补共同构成完整的 HTTP 知识闭环nodejs-chrome-debugging.md用 Chrome DevTools 调试 Node.js 进程排查服务器逻辑时非常实用https-redirect.md若后续要上线可了解如何实现 HTTP 到 HTTPS 的重定向。小结这篇文章证明了仅凭http、fs、path三个核心模块约 70 行代码就能交付一个具备目录配置、路径穿越防护、文件类型白名单、HTML 扩展名省略、请求日志与 MIME 类型映射的静态文件服务器。整个过程没有引入任何第三方依赖既是一个优秀的后端入门项目也是理解真实 Web 服务器工作原理的最小完整样本。以此为起点你可以继续补充缓存头、条件请求If-Modified-Since/ETag、范围请求乃至 HTTPS 支持一步步把玩具服务器进化成生产级方案。赞分享教程文档【免费下载链接】30-seconds-of-codeCoding articles to level up your development skills项目地址https://gitcode.com/gh_mirrors/30/30-seconds-of-code点击查看免费下载相关推荐gh_mirrors/30s/30-seconds-of-python 依赖管理指南gh_mirrors/30s/30 seconds of python 依赖管理指南 你是否在使用 Python 项目时因依赖管理混乱导致运行报错或环境不一致教程文档纯 HTML 打造数字专用输入框30-seconds-of-code 的 inputmode 与 pattern 实战指南纯 HTML 打造数字专用输入框30 seconds of code 的 inputmode 与 pattern 实战指南 导读 在 Web 表单中只允教程文档Superpowers 零依赖 Brainstorm 服务器单文件 Node.js 内置模块实现 RFC 6455 与文件监听Superpowers 零依赖 Brainstorm 服务器单文件 Node.js 内置模块实现 RFC 6455 与文件监听 在 Superpowers 的AI 技能AI 插件开发工具上一篇WebPlotDigitizer终极指南3分钟从图表图像中提取数据的完整教程下一篇WebPlotDigitizer终极指南如何从图表图像中快速提取数据创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表