AwsLinker/scripts/serve-static.js

325 lines
10 KiB
JavaScript
Raw Permalink Normal View History

2025-09-16 17:19:58 +08:00
#!/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 = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>404 - 页面未找到 - AwsLinker</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.container {
max-width: 500px;
width: 90%;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
padding: 3rem 2rem;
text-align: center;
}
.error-code {
font-size: 4rem;
font-weight: 800;
color: #667eea;
margin-bottom: 1rem;
}
.error-title {
font-size: 1.5rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 1rem;
}
.error-description {
color: #718096;
margin-bottom: 2rem;
line-height: 1.6;
}
.btn {
display: inline-block;
padding: 0.75rem 1.5rem;
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 500;
transition: transform 0.2s;
}
.btn:hover {
transform: translateY(-2px);
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">404</div>
<h1 class="error-title">页面未找到</h1>
<p class="error-description">抱歉您访问的页面不存在或已被移除</p>
<a href="/" class="btn">返回首页</a>
</div>
</body>
</html>`;
res.writeHead(404, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(content, 'utf8')
});
res.end(content);
console.log(`✗ 404 返回默认404页面`);
}
send500(res, error) {
const content = `
<!DOCTYPE html>
<html>
<head>
<title>500 - 服务器错误</title>
<meta charset="utf-8">
</head>
<body>
<h1>500 - 服务器内部错误</h1>
<p>服务器处理请求时发生错误</p>
<p><a href="/">返回首页</a></p>
</body>
</html>`;
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;