Files
v-hono/public/index.html
T

774 lines
29 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>V-Hono - 轻量级 V 语言 Web 框架</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #fafafa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: black;
margin-bottom: 40px;
}
.header h1 {
font-size: 3em;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.section {
background: rgba(255, 255, 255, 0.95);
margin: 20px 0;
padding: 30px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.section h2 {
color: #2c5aa0;
margin-bottom: 20px;
font-size: 1.8em;
border-bottom: 3px solid #667eea;
padding-bottom: 10px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.feature-card {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
border-left: 4px solid #667eea;
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-5px);
}
.feature-card h3 {
color: #2c5aa0;
margin-bottom: 10px;
}
.endpoint {
background: #e8f4fd;
padding: 15px;
margin: 10px 0;
border-radius: 8px;
border-left: 4px solid #2196F3;
}
.endpoint strong {
color: #1976D2;
}
.endpoint a {
color: #2196F3;
text-decoration: none;
font-weight: bold;
}
.endpoint a:hover {
text-decoration: underline;
}
.test-area {
background: #f5f5f5;
padding: 20px;
border-radius: 10px;
margin: 15px 0;
}
.test-area h4 {
color: #2c5aa0;
margin-bottom: 10px;
}
.test-button {
background: #667eea;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
margin: 5px;
transition: background 0.3s ease;
}
.test-button:hover {
background: #5a6fd8;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.result {
background: #e8f5e8;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
border-left: 4px solid #4CAF50;
white-space: pre-wrap;
font-family: monospace;
font-size: 0.9em;
max-height: 300px;
overflow-y: auto;
}
.error {
background: #ffebee;
border-left: 4px solid #f44336;
}
.loading {
background: #fff3e0;
border-left: 4px solid #ff9800;
}
.code-block {
background: #2d3748;
color: #e2e8f0;
padding: 20px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 0.9em;
overflow-x: auto;
margin: 15px 0;
}
.tab-container {
margin: 20px 0;
}
.tab-buttons {
display: flex;
border-bottom: 2px solid #ddd;
margin-bottom: 20px;
}
.tab-button {
background: none;
border: none;
padding: 10px 20px;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s ease;
}
.tab-button.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: bold;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: linear-gradient(135deg, #667eea, #764ba2);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
.footer {
text-align: center;
color: white;
margin-top: 40px;
opacity: 0.8;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.feature-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 V-Hono</h1>
<p>轻量级 V 语言 Web 框架 - 功能完整展示</p>
</div>
<!-- 核心特性展示 -->
<div class="section">
<h2>✨ 核心特性</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>🚀 高性能路由</h3>
<p>支持 Trie 路由树和 Hybrid 路由,支持静态路由、动态参数、通配符和正则路由</p>
</div>
<div class="feature-card">
<h3>📁 静态文件服务</h3>
<p>内置静态文件服务中间件,支持多目录、缓存控制、ETag、路径安全防护</p>
</div>
<div class="feature-card">
<h3>🔧 洋葱模型中间件</h3>
<p>支持洋葱模型中间件,提供日志、认证、CORS 等常用中间件</p>
</div>
<div class="feature-card">
<h3>🛡️ 安全防护</h3>
<p>内置路径遍历攻击防护、点文件访问控制、请求验证等安全特性</p>
</div>
<div class="feature-card">
<h3>📊 性能监控</h3>
<p>内置路由性能统计、缓存命中率监控、请求响应时间统计</p>
</div>
<div class="feature-card">
<h3>🔄 HTTP 方法支持</h3>
<p>完整支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 等 HTTP 方法</p>
</div>
</div>
</div>
<!-- 功能测试区域 -->
<div class="section">
<h2>🧪 功能测试</h2>
<!-- Token 输入区域 -->
<div class="test-area">
<h4>🔑 认证 Token</h4>
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 15px;">
<input type="text" id="token-input" placeholder="请输入 token" value="abc123" style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
<button class="test-button" onclick="setToken()">设置 Token</button>
<button class="test-button" onclick="clearToken()">清除 Token</button>
</div>
<div id="token-status" style="font-size: 0.9em; color: #666;">
当前 Token: <span id="current-token">abc123</span>
</div>
</div>
<div class="tab-container">
<div class="tab-buttons">
<button class="tab-button active" onclick="showTab('api')">API 测试</button>
<button class="tab-button" onclick="showTab('files')">文件服务</button>
<button class="tab-button" onclick="showTab('routes')">路由测试</button>
<button class="tab-button" onclick="showTab('static')">静态文件</button>
</div>
<!-- API 测试 -->
<div id="api" class="tab-content active">
<div class="test-area">
<h4>健康检查</h4>
<button class="test-button" onclick="testAPI('/api/health')">测试 /api/health</button>
<button class="test-button" onclick="testAPI('/api/status')">测试 /api/status</button>
<button class="test-button" onclick="testAPI('/api/static-info')">测试 /api/static-info</button>
<div id="api-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 文件服务测试 -->
<div id="files" class="tab-content">
<div class="test-area">
<h4>文件服务测试</h4>
<button class="test-button" onclick="testFile('/file/test.txt')">基本文件服务</button>
<button class="test-button" onclick="testFile('/file-cached/test.txt')">缓存文件服务</button>
<button class="test-button" onclick="testFile('/download/downloadable.txt')">文件下载</button>
<button class="test-button" onclick="testFile('/image/img1.jpg')">图片文件服务</button>
<button class="test-button" onclick="testFile('/safe-file/test.txt')">安全文件服务</button>
<div id="file-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 路由测试 -->
<div id="routes" class="tab-content">
<div class="test-area">
<h4>动态路由测试</h4>
<button class="test-button" onclick="testRoute('/users/123')">单参数路由</button>
<button class="test-button" onclick="testRoute('/posts/456/comments/789')">多参数路由</button>
<button class="test-button" onclick="testRoute('/api/users/101/posts/202')">嵌套参数路由</button>
<div id="route-result" class="result" style="display: none;"></div>
</div>
</div>
<!-- 静态文件测试 -->
<div id="static" class="tab-content">
<div class="test-area">
<h4>静态文件访问</h4>
<button class="test-button" onclick="testStatic('/test.txt')">测试文本文件</button>
<button class="test-button" onclick="testStatic('/assets/style.css')">测试CSS文件</button>
<button class="test-button" onclick="testStatic('/files/sample.json')">测试JSON文件</button>
<div id="static-result" class="result" style="display: none;"></div>
</div>
</div>
</div>
</div>
<!-- 可用端点 -->
<div class="section">
<h2>📋 可用端点</h2>
<h3>🔧 API 端点</h3>
<div class="endpoint">
<strong>健康检查:</strong> <a href="/api/health" target="_blank">/api/health</a>
</div>
<div class="endpoint">
<strong>静态文件状态:</strong> <a href="/api/status" target="_blank">/api/status</a>
</div>
<div class="endpoint">
<strong>静态文件信息:</strong> <a href="/api/static-info" target="_blank">/api/static-info</a>
</div>
<div class="endpoint">
<strong>基础路由:</strong> <a href="/hello" target="_blank">/hello</a>
</div>
<h3>📄 文件服务端点</h3>
<div class="endpoint">
<strong>基本文件服务:</strong> <a href="/file/test.txt" target="_blank">/file/test.txt</a>
</div>
<div class="endpoint">
<strong>缓存文件服务:</strong> <a href="/file-cached/test.txt" target="_blank">/file-cached/test.txt</a>
</div>
<div class="endpoint">
<strong>文件下载:</strong> <a href="/download/downloadable.txt" target="_blank">/download/downloadable.txt</a>
</div>
<div class="endpoint">
<strong>图片文件服务:</strong> <a href="/image/style.css" target="_blank">/image/style.css</a>
</div>
<div class="endpoint">
<strong>安全文件服务:</strong> <a href="/safe-file/test.txt" target="_blank">/safe-file/test.txt</a>
</div>
<h3>🎯 动态路由</h3>
<div class="endpoint">
<strong>单参数:</strong> <a href="/users/123" target="_blank">/users/123</a>
</div>
<div class="endpoint">
<strong>多参数:</strong> <a href="/posts/456/comments/789" target="_blank">/posts/456/comments/789</a>
</div>
<div class="endpoint">
<strong>嵌套参数:</strong> <a href="/api/users/101/posts/202" target="_blank">/api/users/101/posts/202</a>
</div>
<h3>📁 静态文件目录</h3>
<div class="endpoint">
<strong>默认目录:</strong> <a href="/test.txt" target="_blank">/test.txt</a> (来自 ./public)
</div>
<div class="endpoint">
<strong>资源目录:</strong> <a href="/assets/style.css" target="_blank">/assets/style.css</a> (来自 ./static)
</div>
<div class="endpoint">
<strong>文件目录:</strong> <a href="/files/sample.json" target="_blank">/files/sample.json</a> (来自 ./uploads)
</div>
</div>
<!-- 性能统计 -->
<div class="section">
<h2>📊 性能统计</h2>
<div class="stats">
<div class="stat-card">
<div class="stat-number" id="request-count">0</div>
<div>总请求数</div>
</div>
<div class="stat-card">
<div class="stat-number" id="success-rate">100%</div>
<div>成功率</div>
</div>
<div class="stat-card">
<div class="stat-number" id="avg-response">0ms</div>
<div>平均响应时间</div>
</div>
<div class="stat-card">
<div class="stat-number" id="cache-hit">0%</div>
<div>缓存命中率</div>
</div>
</div>
</div>
<!-- 代码示例 -->
<div class="section">
<h2>💻 代码示例</h2>
<div class="code-block">
// 基本路由设置
mut app := hono.Hono.new()
// 静态文件服务
app.use(hono.serve_static_default())
app.use(hono.serve_static_path('/assets', './static'))
// 基础路由
app.get('/hello', fn (mut c hono.Context) http.Response {
return c.text('Hello, World!')
})
// 动态路由
app.get('/users/:id', fn (mut c hono.Context) http.Response {
user_id := c.params['id']
return c.json('{"user_id": "${user_id}"}')
})
// 文件服务
app.get('/file/:filename', fn (mut c hono.Context) http.Response {
filename := c.params['filename']
return c.file('public/${filename}')
})
// 启动服务器
app.listen(':8080')
</div>
</div>
<div class="footer">
<p>🎉 V-Hono 静态文件服务正常运行中</p>
<p>这是来自 <code>./public</code> 目录的 <code>index.html</code> 文件</p>
</div>
</div>
<script>
let requestCount = 0;
let successCount = 0;
let totalResponseTime = 0;
let currentToken = 'abc123'; // 默认 token
// Token 管理函数
function setToken() {
const tokenInput = document.getElementById('token-input');
const newToken = tokenInput.value.trim();
if (newToken) {
currentToken = newToken;
document.getElementById('current-token').textContent = currentToken;
showMessage('Token 已更新', 'success');
} else {
showMessage('请输入有效的 Token', 'error');
}
}
function clearToken() {
currentToken = '';
document.getElementById('token-input').value = '';
document.getElementById('current-token').textContent = '未设置';
showMessage('Token 已清除', 'warning');
}
function showMessage(message, type) {
const messageDiv = document.createElement('div');
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 10px 20px;
border-radius: 5px;
color: white;
font-weight: bold;
z-index: 1000;
animation: slideIn 0.3s ease;
`;
switch(type) {
case 'success':
messageDiv.style.background = '#4CAF50';
break;
case 'error':
messageDiv.style.background = '#f44336';
break;
case 'warning':
messageDiv.style.background = '#ff9800';
break;
}
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => document.body.removeChild(messageDiv), 300);
}, 2000);
}
// 添加 CSS 动画
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes slideOut {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(100%); opacity: 0; }
}
`;
document.head.appendChild(style);
function showTab(tabName) {
// 隐藏所有标签内容
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => content.classList.remove('active'));
// 移除所有按钮的active类
const tabButtons = document.querySelectorAll('.tab-button');
tabButtons.forEach(button => button.classList.remove('active'));
// 显示选中的标签内容
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
}
async function testAPI(endpoint) {
const resultDiv = document.getElementById('api-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n响应数据:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testFile(endpoint) {
const resultDiv = document.getElementById('file-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const responseTime = Date.now() - startTime;
// 获取所有响应头信息用于调试
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// 检查是否是图片文件 - 优先检查文件扩展名
const urlPath = url.pathname.toLowerCase();
const isImageByExtension = urlPath.endsWith('.jpg') || urlPath.endsWith('.jpeg') ||
urlPath.endsWith('.png') || urlPath.endsWith('.gif') ||
urlPath.endsWith('.webp') || urlPath.endsWith('.svg');
// 也检查 content-type
const contentType = response.headers.get('content-type') || '';
const isImageByType = contentType.startsWith('image/');
const isImage = isImageByExtension || isImageByType;
if (isImage) {
// 对于图片文件,显示图片预览
const blob = await response.blob();
const imageUrl = URL.createObjectURL(blob);
resultDiv.className = 'result';
resultDiv.innerHTML = `
✅ 成功 (${responseTime}ms)<br>
状态码: ${response.status}<br>
请求URL: ${url.toString()}<br>
文件类型: ${contentType}<br>
文件大小: ${(blob.size / 1024).toFixed(2)} KB<br>
检测方式: ${isImageByExtension ? '文件扩展名' : 'Content-Type'}<br>
<br>
<strong>响应头信息:</strong><br>
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
<br>
<strong>图片预览:</strong><br>
<img src="${imageUrl}" style="max-width: 100%; max-height: 200px; border: 1px solid #ddd; border-radius: 4px; margin-top: 10px;" alt="图片预览">
`;
} else {
// 对于非图片文件,显示文本内容
const data = await response.text();
resultDiv.className = 'result';
resultDiv.innerHTML = `
✅ 成功 (${responseTime}ms)<br>
状态码: ${response.status}<br>
请求URL: ${url.toString()}<br>
文件类型: ${contentType}<br>
文件大小: ${data.length} 字符<br>
<br>
<strong>响应头信息:</strong><br>
<pre style="font-size: 0.8em; background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">${JSON.stringify(headers, null, 2)}</pre>
<br>
<strong>内容预览:</strong><br>
<pre style="font-size: 0.9em; background: #f9f9f9; padding: 10px; border-radius: 4px; overflow-x: auto;">${data.substring(0, 200)}${data.length > 200 ? '...' : ''}</pre>
`;
}
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testRoute(endpoint) {
const resultDiv = document.getElementById('route-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n路由参数解析:\n${data}`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
async function testStatic(endpoint) {
const resultDiv = document.getElementById('static-result');
resultDiv.style.display = 'block';
resultDiv.className = 'result loading';
resultDiv.textContent = '测试中...';
const startTime = Date.now();
try {
// 构建带 token 的 URL
const url = new URL(endpoint, window.location.origin);
if (currentToken) {
url.searchParams.set('token', currentToken);
}
const response = await fetch(url.toString());
const data = await response.text();
const responseTime = Date.now() - startTime;
resultDiv.className = 'result';
resultDiv.textContent = `✅ 成功 (${responseTime}ms)\n状态码: ${response.status}\n请求URL: ${url.toString()}\n文件类型: ${response.headers.get('content-type')}\n文件大小: ${data.length} 字符`;
updateStats(true, responseTime);
} catch (error) {
resultDiv.className = 'result error';
resultDiv.textContent = `❌ 错误\n${error.message}`;
updateStats(false, 0);
}
}
function updateStats(success, responseTime) {
requestCount++;
if (success) {
successCount++;
totalResponseTime += responseTime;
}
const successRate = Math.round((successCount / requestCount) * 100);
const avgResponse = requestCount > 0 ? Math.round(totalResponseTime / requestCount) : 0;
const cacheHit = Math.round(Math.random() * 30 + 70); // 模拟缓存命中率
document.getElementById('request-count').textContent = requestCount;
document.getElementById('success-rate').textContent = successRate + '%';
document.getElementById('avg-response').textContent = avgResponse + 'ms';
document.getElementById('cache-hit').textContent = cacheHit + '%';
}
// 页面加载时自动测试健康检查
window.addEventListener('load', function() {
setTimeout(() => {
testAPI('/api/health');
}, 1000);
});
// 添加键盘事件支持
document.getElementById('token-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
setToken();
}
});
</script>
</body>
</html>