Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Node.js静态文件服务教程,详解静态资源托管、MIME类型处理、缓存策略。包含完整代码示例,适合Web开发者快速掌握文件服务技术。
核心关键词:Node.js静态文件服务2024、静态资源托管、MIME类型处理、Node.js文件服务器、Web缓存策略
长尾关键词:Node.js怎么托管静态文件、静态文件服务器搭建、MIME类型配置方法、Node.js缓存策略、静态资源优化技巧
通过本节Node.js静态文件服务教程,你将系统性掌握:
静态文件服务是什么?这是Web开发中的基础概念。静态文件服务是指为HTML、CSS、JavaScript、图片等静态资源提供HTTP访问的服务,也是Web应用性能的重要影响因素。
💡 设计理念:静态文件服务应该快速、可靠、安全,为用户提供最佳的资源加载体验
让我们从最简单的静态文件服务器开始:
// 🎉 基础静态文件服务器
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// 静态文件服务器类
class StaticFileServer {
constructor(rootDir = './public', port = 3000) {
this.rootDir = path.resolve(rootDir);
this.port = port;
this.ensureRootDir();
}
ensureRootDir() {
if (!fs.existsSync(this.rootDir)) {
fs.mkdirSync(this.rootDir, { recursive: true });
console.log(`创建静态文件目录: ${this.rootDir}`);
}
}
async serveFile(req, res) {
try {
const parsedUrl = url.parse(req.url);
let pathname = parsedUrl.pathname;
// 防止路径遍历攻击
pathname = this.sanitizePath(pathname);
// 构建完整文件路径
const filePath = path.join(this.rootDir, pathname);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
this.send404(res);
return;
}
// 获取文件信息
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
// 尝试查找默认文件
const indexFile = this.findIndexFile(filePath);
if (indexFile) {
await this.sendFile(res, indexFile);
} else {
this.sendDirectoryListing(res, filePath, pathname);
}
} else {
await this.sendFile(res, filePath);
}
} catch (error) {
console.error('文件服务错误:', error);
this.send500(res);
}
}
sanitizePath(pathname) {
// 解码URL编码
pathname = decodeURIComponent(pathname);
// 移除查询参数
pathname = pathname.split('?')[0];
// 规范化路径,防止路径遍历
pathname = path.normalize(pathname);
// 确保路径以/开头
if (!pathname.startsWith('/')) {
pathname = '/' + pathname;
}
// 防止访问上级目录
if (pathname.includes('..')) {
pathname = '/';
}
return pathname;
}
findIndexFile(dirPath) {
const indexFiles = ['index.html', 'index.htm', 'default.html'];
for (const indexFile of indexFiles) {
const indexPath = path.join(dirPath, indexFile);
if (fs.existsSync(indexPath)) {
return indexPath;
}
}
return null;
}
async sendFile(res, filePath) {
try {
const stats = fs.statSync(filePath);
const mimeType = this.getMimeType(filePath);
// 设置响应头
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Length', stats.size);
res.setHeader('Last-Modified', stats.mtime.toUTCString());
// 创建文件读取流
const fileStream = fs.createReadStream(filePath);
// 处理流错误
fileStream.on('error', (error) => {
console.error('文件读取错误:', error);
this.send500(res);
});
// 发送文件
res.writeHead(200);
fileStream.pipe(res);
console.log(`发送文件: ${filePath} (${mimeType})`);
} catch (error) {
console.error('发送文件错误:', error);
this.send500(res);
}
}
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.htm': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain; charset=utf-8',
'.pdf': 'application/pdf',
'.zip': 'application/zip',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
return mimeTypes[ext] || 'application/octet-stream';
}
sendDirectoryListing(res, dirPath, urlPath) {
try {
const files = fs.readdirSync(dirPath);
let html = `
<!DOCTYPE html>
<html>
<head>
<title>目录列表 - ${urlPath}</title>
<meta charset="utf-8">
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
.header { border-bottom: 1px solid #ccc; padding-bottom: 10px; }
.file-list { list-style: none; padding: 0; }
.file-item { padding: 8px 0; border-bottom: 1px solid #eee; }
.file-item a { text-decoration: none; color: #0066cc; }
.file-item a:hover { text-decoration: underline; }
.file-size { color: #666; font-size: 0.9em; margin-left: 10px; }
.directory { font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>目录列表</h1>
<p>路径: ${urlPath}</p>
</div>
<ul class="file-list">
`;
// 添加上级目录链接
if (urlPath !== '/') {
const parentPath = path.dirname(urlPath);
html += `
<li class="file-item">
<a href="${parentPath}" class="directory">📁 ..</a>
</li>
`;
}
// 添加文件和目录
files.forEach(file => {
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);
const isDirectory = stats.isDirectory();
const fileUrl = path.posix.join(urlPath, file);
const icon = isDirectory ? '📁' : '📄';
const className = isDirectory ? 'directory' : '';
const size = isDirectory ? '' : `<span class="file-size">(${this.formatFileSize(stats.size)})</span>`;
html += `
<li class="file-item">
<a href="${fileUrl}" class="${className}">${icon} ${file}</a>
${size}
</li>
`;
});
html += `
</ul>
</body>
</html>
`;
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
} catch (error) {
console.error('目录列表错误:', error);
this.send500(res);
}
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
send404(res) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>404 - 文件未找到</title>
<meta charset="utf-8">
</head>
<body>
<h1>404 - 文件未找到</h1>
<p>请求的文件不存在。</p>
</body>
</html>
`);
}
send500(res) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<!DOCTYPE html>
<html>
<head>
<title>500 - 服务器错误</title>
<meta charset="utf-8">
</head>
<body>
<h1>500 - 服务器内部错误</h1>
<p>服务器处理请求时发生错误。</p>
</body>
</html>
`);
}
start() {
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
if (req.method === 'GET') {
this.serveFile(req, res);
} else {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed');
}
});
server.listen(this.port, () => {
console.log(`静态文件服务器启动:http://localhost:${this.port}`);
console.log(`服务目录:${this.rootDir}`);
});
return server;
}
}
// 启动服务器
const fileServer = new StaticFileServer('./public', 3000);
fileServer.start();// 🚀 带缓存策略的静态文件服务器
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
class CachedStaticServer {
constructor(rootDir = './public', options = {}) {
this.rootDir = path.resolve(rootDir);
this.options = {
maxAge: options.maxAge || 3600, // 默认1小时缓存
enableETag: options.enableETag !== false,
enableGzip: options.enableGzip !== false,
enableBrotli: options.enableBrotli !== false,
...options
};
}
async serveFile(req, res) {
try {
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
let pathname = this.sanitizePath(parsedUrl.pathname);
const filePath = path.join(this.rootDir, pathname);
if (!fs.existsSync(filePath)) {
this.send404(res);
return;
}
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
const indexFile = this.findIndexFile(filePath);
if (indexFile) {
await this.sendFileWithCache(req, res, indexFile);
} else {
this.sendDirectoryListing(res, filePath, pathname);
}
} else {
await this.sendFileWithCache(req, res, filePath);
}
} catch (error) {
console.error('文件服务错误:', error);
this.send500(res);
}
}
async sendFileWithCache(req, res, filePath) {
try {
const stats = fs.statSync(filePath);
const mimeType = this.getMimeType(filePath);
// 生成ETag
const etag = this.generateETag(stats);
// 设置缓存相关头
this.setCacheHeaders(res, stats, etag);
// 检查条件请求
if (this.checkConditionalRequest(req, stats, etag)) {
res.writeHead(304);
res.end();
console.log(`304 Not Modified: ${filePath}`);
return;
}
// 设置内容类型
res.setHeader('Content-Type', mimeType);
// 检查是否支持压缩
const acceptEncoding = req.headers['accept-encoding'] || '';
const shouldCompress = this.shouldCompress(mimeType, stats.size);
if (shouldCompress && acceptEncoding.includes('br') && this.options.enableBrotli) {
await this.sendCompressedFile(res, filePath, 'br');
} else if (shouldCompress && acceptEncoding.includes('gzip') && this.options.enableGzip) {
await this.sendCompressedFile(res, filePath, 'gzip');
} else {
await this.sendPlainFile(res, filePath, stats);
}
} catch (error) {
console.error('发送文件错误:', error);
this.send500(res);
}
}
setCacheHeaders(res, stats, etag) {
// 设置缓存控制
res.setHeader('Cache-Control', `public, max-age=${this.options.maxAge}`);
// 设置ETag
if (this.options.enableETag) {
res.setHeader('ETag', etag);
}
// 设置Last-Modified
res.setHeader('Last-Modified', stats.mtime.toUTCString());
// 设置Expires
const expires = new Date(Date.now() + this.options.maxAge * 1000);
res.setHeader('Expires', expires.toUTCString());
}
generateETag(stats) {
// 基于文件大小和修改时间生成ETag
const hash = crypto.createHash('md5');
hash.update(`${stats.size}-${stats.mtime.getTime()}`);
return `"${hash.digest('hex')}"`;
}
checkConditionalRequest(req, stats, etag) {
// 检查If-None-Match (ETag)
const ifNoneMatch = req.headers['if-none-match'];
if (ifNoneMatch && ifNoneMatch === etag) {
return true;
}
// 检查If-Modified-Since
const ifModifiedSince = req.headers['if-modified-since'];
if (ifModifiedSince) {
const modifiedSince = new Date(ifModifiedSince);
const lastModified = new Date(stats.mtime);
// 忽略毫秒差异
lastModified.setMilliseconds(0);
if (lastModified <= modifiedSince) {
return true;
}
}
return false;
}
shouldCompress(mimeType, fileSize) {
// 只压缩文本类型文件且大小超过阈值
const compressibleTypes = [
'text/',
'application/javascript',
'application/json',
'application/xml',
'image/svg+xml'
];
const isCompressible = compressibleTypes.some(type => mimeType.includes(type));
const isSizeAppropriate = fileSize > 1024; // 大于1KB才压缩
return isCompressible && isSizeAppropriate;
}
async sendCompressedFile(res, filePath, encoding) {
try {
const stats = fs.statSync(filePath);
// 设置压缩相关头
res.setHeader('Content-Encoding', encoding);
res.setHeader('Vary', 'Accept-Encoding');
// 创建压缩流
let compressor;
if (encoding === 'br') {
compressor = zlib.createBrotliCompress();
} else if (encoding === 'gzip') {
compressor = zlib.createGzip();
}
// 创建文件读取流
const fileStream = fs.createReadStream(filePath);
res.writeHead(200);
// 管道:文件 -> 压缩 -> 响应
fileStream.pipe(compressor).pipe(res);
console.log(`发送压缩文件: ${filePath} (${encoding})`);
} catch (error) {
console.error('压缩文件发送错误:', error);
this.send500(res);
}
}
async sendPlainFile(res, filePath, stats) {
try {
res.setHeader('Content-Length', stats.size);
const fileStream = fs.createReadStream(filePath);
fileStream.on('error', (error) => {
console.error('文件读取错误:', error);
this.send500(res);
});
res.writeHead(200);
fileStream.pipe(res);
console.log(`发送文件: ${filePath}`);
} catch (error) {
console.error('文件发送错误:', error);
this.send500(res);
}
}
// 范围请求支持(断点续传)
handleRangeRequest(req, res, filePath, stats) {
const range = req.headers.range;
if (!range) {
return false;
}
const parts = range.replace(/bytes=/, "").split("-");
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
if (start >= stats.size || end >= stats.size) {
res.writeHead(416, {
'Content-Range': `bytes */${stats.size}`
});
res.end();
return true;
}
const chunksize = (end - start) + 1;
const fileStream = fs.createReadStream(filePath, { start, end });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${stats.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': this.getMimeType(filePath)
});
fileStream.pipe(res);
console.log(`发送范围请求: ${filePath} (${start}-${end})`);
return true;
}
// 其他方法保持不变...
sanitizePath(pathname) {
pathname = decodeURIComponent(pathname);
pathname = pathname.split('?')[0];
pathname = path.normalize(pathname);
if (!pathname.startsWith('/')) pathname = '/' + pathname;
if (pathname.includes('..')) pathname = '/';
return pathname;
}
findIndexFile(dirPath) {
const indexFiles = ['index.html', 'index.htm', 'default.html'];
for (const indexFile of indexFiles) {
const indexPath = path.join(dirPath, indexFile);
if (fs.existsSync(indexPath)) return indexPath;
}
return null;
}
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain; charset=utf-8',
'.pdf': 'application/pdf',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg',
'.woff': 'font/woff',
'.woff2': 'font/woff2'
};
return mimeTypes[ext] || 'application/octet-stream';
}
send404(res) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>404 - 文件未找到</h1>');
}
send500(res) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>500 - 服务器内部错误</h1>');
}
sendDirectoryListing(res, dirPath, urlPath) {
// 简化的目录列表实现
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`<h1>目录列表: ${urlPath}</h1><p>目录浏览功能</p>`);
}
start(port = 3000) {
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
if (req.method === 'GET' || req.method === 'HEAD') {
this.serveFile(req, res);
} else {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed');
}
});
server.listen(port, () => {
console.log(`缓存静态文件服务器启动:http://localhost:${port}`);
console.log(`服务目录:${this.rootDir}`);
console.log('缓存配置:', this.options);
});
return server;
}
}
// 启动带缓存的服务器
const cachedServer = new CachedStaticServer('./public', {
maxAge: 3600, // 1小时缓存
enableETag: true, // 启用ETag
enableGzip: true, // 启用Gzip压缩
enableBrotli: true // 启用Brotli压缩
});
cachedServer.start(3000);缓存策略要点:
// 🔒 安全的静态文件服务器
const http = require('http');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
class SecureStaticServer {
constructor(rootDir = './public', options = {}) {
this.rootDir = path.resolve(rootDir);
this.options = {
allowDotFiles: options.allowDotFiles || false,
maxFileSize: options.maxFileSize || 100 * 1024 * 1024, // 100MB
allowedExtensions: options.allowedExtensions || null,
blockedExtensions: options.blockedExtensions || ['.exe', '.bat', '.cmd', '.sh'],
enableDirectoryListing: options.enableDirectoryListing || false,
rateLimitWindow: options.rateLimitWindow || 60000, // 1分钟
rateLimitMax: options.rateLimitMax || 100, // 每分钟最多100请求
...options
};
this.rateLimitMap = new Map();
this.setupCleanupInterval();
}
setupCleanupInterval() {
// 定期清理过期的限流记录
setInterval(() => {
const now = Date.now();
for (const [ip, data] of this.rateLimitMap.entries()) {
if (now - data.windowStart > this.options.rateLimitWindow) {
this.rateLimitMap.delete(ip);
}
}
}, this.options.rateLimitWindow);
}
checkRateLimit(req) {
const clientIP = this.getClientIP(req);
const now = Date.now();
if (!this.rateLimitMap.has(clientIP)) {
this.rateLimitMap.set(clientIP, {
count: 1,
windowStart: now
});
return true;
}
const data = this.rateLimitMap.get(clientIP);
// 检查是否在同一时间窗口内
if (now - data.windowStart < this.options.rateLimitWindow) {
data.count++;
if (data.count > this.options.rateLimitMax) {
return false; // 超过限制
}
} else {
// 新的时间窗口
data.count = 1;
data.windowStart = now;
}
return true;
}
getClientIP(req) {
return req.headers['x-forwarded-for'] ||
req.headers['x-real-ip'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'127.0.0.1';
}
validatePath(pathname) {
// 1. 防止路径遍历
const normalizedPath = path.normalize(pathname);
if (normalizedPath.includes('..')) {
throw new Error('Path traversal detected');
}
// 2. 检查隐藏文件
if (!this.options.allowDotFiles && path.basename(normalizedPath).startsWith('.')) {
throw new Error('Access to dot files denied');
}
// 3. 检查文件扩展名
const ext = path.extname(normalizedPath).toLowerCase();
if (this.options.blockedExtensions.includes(ext)) {
throw new Error(`File extension ${ext} is blocked`);
}
if (this.options.allowedExtensions && !this.options.allowedExtensions.includes(ext)) {
throw new Error(`File extension ${ext} is not allowed`);
}
return normalizedPath;
}
validateFile(filePath, stats) {
// 检查文件大小
if (stats.size > this.options.maxFileSize) {
throw new Error('File too large');
}
// 检查文件权限
try {
fs.accessSync(filePath, fs.constants.R_OK);
} catch (error) {
throw new Error('File access denied');
}
return true;
}
setSecurityHeaders(res) {
// 设置安全相关的HTTP头
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Content-Security-Policy', "default-src 'self'");
// 移除可能泄露服务器信息的头
res.removeHeader('X-Powered-By');
}
async serveFile(req, res) {
try {
// 1. 检查限流
if (!this.checkRateLimit(req)) {
res.writeHead(429, { 'Content-Type': 'text/plain' });
res.end('Too Many Requests');
return;
}
// 2. 设置安全头
this.setSecurityHeaders(res);
// 3. 解析和验证路径
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const pathname = this.validatePath(decodeURIComponent(parsedUrl.pathname));
const filePath = path.join(this.rootDir, pathname);
// 4. 检查文件是否存在
if (!fs.existsSync(filePath)) {
this.send404(res);
return;
}
const stats = fs.statSync(filePath);
// 5. 处理目录请求
if (stats.isDirectory()) {
if (!this.options.enableDirectoryListing) {
this.send403(res);
return;
}
const indexFile = this.findIndexFile(filePath);
if (indexFile) {
const indexStats = fs.statSync(indexFile);
this.validateFile(indexFile, indexStats);
await this.sendFile(res, indexFile, indexStats);
} else {
this.sendDirectoryListing(res, filePath, pathname);
}
} else {
// 6. 验证文件
this.validateFile(filePath, stats);
await this.sendFile(res, filePath, stats);
}
} catch (error) {
console.error('安全验证失败:', error.message);
if (error.message.includes('traversal') ||
error.message.includes('denied') ||
error.message.includes('blocked')) {
this.send403(res);
} else if (error.message.includes('too large')) {
this.send413(res);
} else {
this.send500(res);
}
}
}
async sendFile(res, filePath, stats) {
try {
const mimeType = this.getMimeType(filePath);
// 设置响应头
res.setHeader('Content-Type', mimeType);
res.setHeader('Content-Length', stats.size);
res.setHeader('Last-Modified', stats.mtime.toUTCString());
// 对于可执行文件类型,强制下载
const ext = path.extname(filePath).toLowerCase();
const executableTypes = ['.exe', '.bat', '.cmd', '.sh', '.ps1'];
if (executableTypes.includes(ext)) {
res.setHeader('Content-Disposition', `attachment; filename="${path.basename(filePath)}"`);
}
// 创建文件流
const fileStream = fs.createReadStream(filePath);
fileStream.on('error', (error) => {
console.error('文件读取错误:', error);
this.send500(res);
});
res.writeHead(200);
fileStream.pipe(res);
console.log(`安全发送文件: ${filePath}`);
} catch (error) {
console.error('文件发送错误:', error);
this.send500(res);
}
}
findIndexFile(dirPath) {
const indexFiles = ['index.html', 'index.htm', 'default.html'];
for (const indexFile of indexFiles) {
const indexPath = path.join(dirPath, indexFile);
if (fs.existsSync(indexPath)) {
return indexPath;
}
}
return null;
}
getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'application/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain; charset=utf-8',
'.pdf': 'application/pdf',
'.mp4': 'video/mp4',
'.mp3': 'audio/mpeg'
};
return mimeTypes[ext] || 'application/octet-stream';
}
sendDirectoryListing(res, dirPath, urlPath) {
// 简化的安全目录列表
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>目录列表: ${urlPath}</h1>
<p>安全的目录浏览功能</p>
<p><em>注意:某些文件类型可能被隐藏以确保安全</em></p>
`);
}
send403(res) {
res.writeHead(403, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>403 - 访问被拒绝</h1><p>您没有权限访问此资源。</p>');
}
send404(res) {
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>404 - 文件未找到</h1>');
}
send413(res) {
res.writeHead(413, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>413 - 文件过大</h1>');
}
send500(res) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>500 - 服务器内部错误</h1>');
}
start(port = 3000) {
const server = http.createServer((req, res) => {
const clientIP = this.getClientIP(req);
console.log(`${req.method} ${req.url} - ${clientIP}`);
if (req.method === 'GET' || req.method === 'HEAD') {
this.serveFile(req, res);
} else {
res.writeHead(405, { 'Content-Type': 'text/plain' });
res.end('Method Not Allowed');
}
});
server.listen(port, () => {
console.log(`安全静态文件服务器启动:http://localhost:${port}`);
console.log(`服务目录:${this.rootDir}`);
console.log('安全配置:', {
allowDotFiles: this.options.allowDotFiles,
maxFileSize: `${this.options.maxFileSize / 1024 / 1024}MB`,
rateLimitMax: this.options.rateLimitMax,
enableDirectoryListing: this.options.enableDirectoryListing
});
});
return server;
}
}
// 启动安全的静态文件服务器
const secureServer = new SecureStaticServer('./public', {
allowDotFiles: false,
maxFileSize: 50 * 1024 * 1024, // 50MB
blockedExtensions: ['.exe', '.bat', '.cmd', '.sh', '.ps1'],
enableDirectoryListing: false,
rateLimitMax: 100
});
secureServer.start(3000);安全防护要点:
通过本节Node.js静态文件服务教程的学习,你已经掌握:
A: 静态文件服务器只提供预先存在的文件,不进行服务端处理;动态Web服务器可以根据请求生成内容。静态服务器性能更高,但功能有限。实际应用中通常两者结合使用。
A: 根据文件类型选择:1)HTML文件使用较短缓存时间;2)CSS/JS文件使用版本号+长期缓存;3)图片等资源使用中等缓存时间;4)频繁更新的文件禁用缓存或使用ETag。
A: 对于高流量网站,建议使用Nginx等专业Web服务器处理静态文件,Node.js专注于动态内容。但对于中小型应用,Node.js的静态文件服务完全可以满足需求。
A: 使用流式传输和范围请求:1)支持HTTP Range头;2)实现断点续传;3)使用适当的缓冲区大小;4)设置合理的超时时间;5)考虑使用专门的文件下载服务。
A: 关键指标包括:1)响应时间;2)缓存命中率;3)带宽使用量;4)并发连接数;5)错误率。可以使用APM工具或自建监控系统收集这些指标。
"静态文件服务是Web应用的基础设施,掌握了高效、安全的静态文件服务技术,你就具备了构建高性能Web应用的重要能力。继续深入学习,向Web架构专家的目标迈进!"