#!/usr/bin/env node /** * 自定义静态文件服务器,支持自定义404页面和Next.js静态导出 * 用于部署Next.js静态导出的文件 */ const http = require('http'); const fs = require('fs'); const path = require('path'); const url = require('url'); class StaticServer { constructor(directory = 'out', port = 8080) { this.directory = path.resolve(directory); this.port = port; // MIME类型映射 this.mimeTypes = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; 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', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject' }; } getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); return this.mimeTypes[ext] || 'application/octet-stream'; } ensureErrorPages() { // 确保404.html存在于out目录 const out404Path = path.join(this.directory, '404.html'); const public404Path = path.join(__dirname, '..', 'public', '404.html'); if (!fs.existsSync(out404Path) && fs.existsSync(public404Path)) { try { fs.copyFileSync(public404Path, out404Path); console.log('✅ 已复制404.html到out目录'); } catch (error) { console.log('⚠️ 无法复制404.html:', error.message); } } } sendFile(res, filePath, statusCode = 200) { try { if (!fs.existsSync(filePath)) { this.send404(res); return; } const stat = fs.statSync(filePath); const content = fs.readFileSync(filePath); const mimeType = this.getMimeType(filePath); res.writeHead(statusCode, { 'Content-Type': mimeType, 'Content-Length': stat.size, 'Cache-Control': filePath.endsWith('.html') ? 'no-cache, no-store, must-revalidate' : 'public, max-age=31536000', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'X-XSS-Protection': '1; mode=block' }); res.end(content); console.log(`✓ ${statusCode} ${filePath}`); } catch (error) { console.error(`✗ 错误读取文件 ${filePath}:`, error.message); this.send500(res, error); } } send404(res) { // 尝试找到404页面 const custom404Paths = [ path.join(this.directory, '404.html'), path.join(this.directory, '404', 'index.html'), path.join(__dirname, '..', 'public', '404.html') ]; for (const custom404Path of custom404Paths) { if (fs.existsSync(custom404Path)) { console.log(`✓ 返回自定义404页面: ${custom404Path}`); this.sendFile(res, custom404Path, 404); return; } } // 如果没有找到自定义404页面,返回简单的404 const content = `
服务器处理请求时发生错误。
`; res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': Buffer.byteLength(content, 'utf8') }); res.end(content); console.log(`✗ 500 服务器错误:`, error.message); } handleRequest(req, res) { const parsedUrl = url.parse(req.url, true); let pathname = decodeURIComponent(parsedUrl.pathname); // 移除查询参数和fragment pathname = pathname.split('?')[0].split('#')[0]; console.log(`→ ${req.method} ${pathname}`); // 安全检查:防止目录遍历攻击 if (pathname.includes('..') || pathname.includes('\0')) { this.send404(res); return; } // 处理根路径 if (pathname === '/') { const indexPath = path.join(this.directory, 'index.html'); this.sendFile(res, indexPath); return; } // 构建文件路径 let filePath = path.join(this.directory, pathname.slice(1)); // 检查文件是否存在 if (fs.existsSync(filePath)) { const stat = fs.statSync(filePath); if (stat.isDirectory()) { // 如果是目录,尝试找index.html const indexPath = path.join(filePath, 'index.html'); if (fs.existsSync(indexPath)) { this.sendFile(res, indexPath); } else { this.send404(res); } } else { // 文件存在,直接返回 this.sendFile(res, filePath); } } else { // 文件不存在,尝试添加.html扩展名 const htmlPath = filePath + '.html'; if (fs.existsSync(htmlPath)) { this.sendFile(res, htmlPath); } else { // 尝试在index.html中查找(适用于Next.js路由) const indexPath = path.join(filePath, 'index.html'); if (fs.existsSync(indexPath)) { this.sendFile(res, indexPath); } else { this.send404(res); } } } } start() { // 检查输出目录是否存在 if (!fs.existsSync(this.directory)) { console.error(`❌ 错误: 目录 '${this.directory}' 不存在`); console.error('请先运行构建命令生成静态文件:npm run build:static'); process.exit(1); } // 确保404.html存在于out目录 this.ensureErrorPages(); const server = http.createServer((req, res) => { this.handleRequest(req, res); }); server.listen(this.port, () => { console.log('🚀 静态文件服务器已启动'); console.log(`📁 服务目录: ${this.directory}`); console.log(`🌐 访问地址: http://localhost:${this.port}`); console.log('⌨️ 按 Ctrl+C 停止服务器'); console.log('━'.repeat(50)); }); // 优雅关闭 process.on('SIGINT', () => { console.log('\n🛑 正在关闭服务器...'); server.close(() => { console.log('✅ 服务器已停止'); process.exit(0); }); }); process.on('SIGTERM', () => { console.log('\n🛑 收到终止信号,正在关闭服务器...'); server.close(() => { console.log('✅ 服务器已停止'); process.exit(0); }); }); } } // 主函数 function main() { const args = process.argv.slice(2); const port = args[0] ? parseInt(args[0]) : 8080; const directory = args[1] || 'out'; if (isNaN(port) || port < 1 || port > 65535) { console.error('❌ 错误: 无效的端口号'); console.log('使用方法: node serve-static.js [端口号] [目录]'); process.exit(1); } const server = new StaticServer(directory, port); server.start(); } if (require.main === module) { main(); } module.exports = StaticServer;