diff --git a/README.md b/README.md index 533f04e..7211623 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,11 @@ fn main() { return c.json('{"user_id": "${user_id}"}') }) + // Redirect example + app.get('/old-page', fn (mut c hono.Context) http.Response { + return c.redirect('/new-page', 301) + }) + app.listen(':3000') } ``` @@ -157,6 +162,7 @@ fn main() { - `c.json(data)` - Return JSON response - `c.html(data)` - Return HTML response - `c.file(path)` - Return file response +- `c.redirect(url, status_code...)` - Redirect to URL (default status: 302) - `c.status(code)` - Set response status code - `c.params` - Route parameters - `c.query` - Query parameters @@ -198,6 +204,7 @@ See the `examples/` directory for more examples: - `examples/basic/` - Basic usage - `examples/middleware/` - Middleware usage - `examples/route_grouping/` - Route grouping +- `examples/redirect_demo.v` - Redirect functionality examples ## License diff --git a/docs/README_REDIRECT.md b/docs/README_REDIRECT.md new file mode 100644 index 0000000..e8fcc93 --- /dev/null +++ b/docs/README_REDIRECT.md @@ -0,0 +1,151 @@ +# Redirect 功能文档 + +v-hono 现在支持 HTTP 重定向功能,与 Hono.js 的 `c.redirect()` API 兼容。 + +## 基本用法 + +### 基本重定向 (302 Found) + +```v +app.get('/old-page', fn (mut c hono.Context) hono.Response { + return c.redirect('https://example.com/new-page') +}) +``` + +### 带状态码的重定向 + +```v +// 301 Moved Permanently - 永久重定向 +app.get('/old-url', fn (mut c hono.Context) hono.Response { + return c.redirect('https://example.com/new-url', 301) +}) + +// 303 See Other - 通常用于 POST 后重定向 +app.post('/form-submit', fn (mut c hono.Context) hono.Response { + // 处理表单数据... + return c.redirect('/success', 303) +}) + +// 307 Temporary Redirect - 临时重定向,保持请求方法 +app.get('/temp-redirect', fn (mut c hono.Context) hono.Response { + return c.redirect('/new-location', 307) +}) + +// 308 Permanent Redirect - 永久重定向,保持请求方法 +app.get('/perm-redirect', fn (mut c hono.Context) hono.Response { + return c.redirect('/new-location', 308) +}) +``` + +## API 参考 + +### `c.redirect(url, status_code...)` + +**参数:** +- `url` (string): 重定向的目标 URL,可以是绝对 URL 或相对路径 +- `status_code` (int, 可选): HTTP 状态码,默认为 302 + +**返回值:** +- `http.Response`: 包含适当状态码和 Location 头的 HTTP 响应 + +**支持的状态码:** +- `301` - Moved Permanently (永久重定向) +- `302` - Found (临时重定向,默认) +- `303` - See Other (查看其他位置) +- `307` - Temporary Redirect (临时重定向,保持方法) +- `308` - Permanent Redirect (永久重定向,保持方法) + +## 使用场景 + +### 1. 基本页面重定向 + +```v +app.get('/home', fn (mut c hono.Context) hono.Response { + return c.redirect('/') +}) +``` + +### 2. 条件重定向 + +```v +app.get('/mobile-check', fn (mut c hono.Context) hono.Response { + user_agent := c.req.header.get_custom('User-Agent') or { '' } + + if user_agent.contains('Mobile') { + return c.redirect('/mobile') + } else { + return c.redirect('/desktop') + } +}) +``` + +### 3. 表单提交后重定向 (PRG 模式) + +```v +app.post('/login', fn (mut c hono.Context) hono.Response { + // 验证用户凭据... + if login_successful { + return c.redirect('/dashboard', 303) + } else { + return c.redirect('/login?error=1', 303) + } +}) +``` + +### 4. URL 规范化 + +```v +app.get('/UPPERCASE', fn (mut c hono.Context) hono.Response { + return c.redirect('/lowercase', 301) +}) + +app.get('/trailing-slash/', fn (mut c hono.Context) hono.Response { + return c.redirect('/trailing-slash', 301) +}) +``` + +### 5. 外部重定向 + +```v +app.get('/external', fn (mut c hono.Context) hono.Response { + return c.redirect('https://external-site.com') +}) +``` + +## 重定向状态码说明 + +| 状态码 | 名称 | 说明 | 浏览器行为 | +|--------|------|------|------------| +| 301 | Moved Permanently | 永久重定向,搜索引擎会更新索引 | 缓存重定向,保持原方法 | +| 302 | Found | 临时重定向,默认状态码 | 不缓存,可能改变方法为 GET | +| 303 | See Other | 查看其他位置,常用于 POST 后重定向 | 总是使用 GET 方法 | +| 307 | Temporary Redirect | 临时重定向,保持原请求方法 | 不缓存,保持原方法 | +| 308 | Permanent Redirect | 永久重定向,保持原请求方法 | 缓存重定向,保持原方法 | + +## 最佳实践 + +1. **使用适当的状态码**: 根据重定向的性质选择正确的状态码 +2. **避免重定向循环**: 确保重定向不会形成无限循环 +3. **使用绝对 URL**: 对于外部重定向,使用完整的 URL +4. **SEO 考虑**: 对于永久性的 URL 变更使用 301 状态码 +5. **POST 后重定向**: 使用 303 状态码避免重复提交 + +## 示例应用 + +查看 `examples/redirect_demo.v` 获取完整的使用示例。 + +## 与 Hono.js 的兼容性 + +v-hono 的 `c.redirect()` 方法与 Hono.js 的 API 完全兼容: + +```javascript +// Hono.js +c.redirect('https://example.com') +c.redirect('https://example.com', 301) +``` + +```v +// v-hono +c.redirect('https://example.com') +c.redirect('https://example.com', 301) +``` \ No newline at end of file diff --git a/examples/redirect_demo.v b/examples/redirect_demo.v new file mode 100644 index 0000000..c9a0398 --- /dev/null +++ b/examples/redirect_demo.v @@ -0,0 +1,75 @@ +module main + +import hono +import net.http + +fn main() { + mut app := hono.Hono.new() + + // Basic redirect (302 Found) + app.get('/redirect-basic', fn (mut c hono.Context) http.Response { + return c.redirect('https://example.com') + }) + + // Redirect with custom status code (301 Moved Permanently) + app.get('/redirect-permanent', fn (mut c hono.Context) http.Response { + return c.redirect('https://example.com', 301) + }) + + // Redirect with 303 See Other + app.post('/form-submit', fn (mut c hono.Context) http.Response { + // Process form data here... + // Then redirect to success page + return c.redirect('/success', 303) + }) + + // Redirect with 307 Temporary Redirect (preserves method) + app.get('/redirect-preserve-method', fn (mut c hono.Context) http.Response { + return c.redirect('/new-location', 307) + }) + + // Redirect with 308 Permanent Redirect (preserves method) + app.get('/redirect-permanent-preserve', fn (mut c hono.Context) http.Response { + return c.redirect('/new-permanent-location', 308) + }) + + // Conditional redirect + app.get('/conditional-redirect', fn (mut c hono.Context) http.Response { + user_agent := c.req.header.get_custom('User-Agent') or { '' } + + if user_agent.contains('Mobile') { + return c.redirect('/mobile-version') + } else { + return c.redirect('/desktop-version') + } + }) + + // Redirect to relative path + app.get('/relative-redirect', fn (mut c hono.Context) http.Response { + return c.redirect('../other-page') + }) + + // Success page for demonstration + app.get('/success', fn (mut c hono.Context) http.Response { + return c.html('

Success!

Form submitted successfully.

') + }) + + app.get('/mobile-version', fn (mut c hono.Context) http.Response { + return c.html('

Mobile Version

') + }) + + app.get('/desktop-version', fn (mut c hono.Context) http.Response { + return c.html('

Desktop Version

') + }) + + println('Server running on http://localhost:8080') + println('Try these endpoints:') + println(' GET /redirect-basic - Basic redirect (302)') + println(' GET /redirect-permanent - Permanent redirect (301)') + println(' POST /form-submit - Form redirect (303)') + println(' GET /redirect-preserve-method - Temporary redirect preserving method (307)') + println(' GET /conditional-redirect - Conditional redirect based on User-Agent') + println(' GET /relative-redirect - Relative path redirect') + + app.listen(':8080') +} \ No newline at end of file diff --git a/request.v b/request.v index c2eb2c3..ac14b0c 100644 --- a/request.v +++ b/request.v @@ -256,6 +256,34 @@ pub fn (mut c Context) status(code int) { c.status_code = code } +// redirect method - redirect to a URL with optional status code +pub fn (mut c Context) redirect(url string, status_code ...int) http.Response { + // Set default status code to 302 (Found) if not provided + mut code := 302 + if status_code.len > 0 { + code = status_code[0] + } + + // Set the status code + c.status(code) + + // Set the Location header + c.headers['Location'] = url + + // Build and return the response + mut headers := http.new_header() + headers.add_custom('Connection', 'keep-alive') or { } + for key, value in c.headers { + headers.add_custom(key, value) or { continue } + } + + return http.Response{ + status_code: c.status_code + header: headers + body: '' + } +} + // 处理器接口,使用 Context diff --git a/tests/test_redirect_functionality.v b/tests/test_redirect_functionality.v new file mode 100644 index 0000000..6239e07 --- /dev/null +++ b/tests/test_redirect_functionality.v @@ -0,0 +1,208 @@ +import hono +import net.http + +// 重定向功能测试 +fn redirect_basic_test() bool { + mut c := hono.Context.new( + http.Request{ + method: .get + url: '/test' + }, + map[string]string{}, + map[string]string{}, + '' + ) + + response := c.redirect('https://example.com') + + if response.status_code != 302 { + return false + } + + location := response.header.get_custom('Location') or { '' } + if location != 'https://example.com' { + return false + } + + if response.body != '' { + return false + } + + return true +} + +fn redirect_with_status_test() bool { + mut c := hono.Context.new( + http.Request{ + method: .get + url: '/test' + }, + map[string]string{}, + map[string]string{}, + '' + ) + + response := c.redirect('https://example.com', 301) + + if response.status_code != 301 { + return false + } + + location := response.header.get_custom('Location') or { '' } + if location != 'https://example.com' { + return false + } + + if response.body != '' { + return false + } + + return true +} + +fn redirect_multiple_status_codes_test() bool { + status_codes := [301, 302, 303, 307, 308] + + for code in status_codes { + mut c := hono.Context.new( + http.Request{ + method: .get + url: '/test' + }, + map[string]string{}, + map[string]string{}, + '' + ) + + response := c.redirect('https://example.com', code) + + if response.status_code != code { + return false + } + + location := response.header.get_custom('Location') or { '' } + if location != 'https://example.com' { + return false + } + + if response.body != '' { + return false + } + } + + return true +} + +fn redirect_relative_url_test() bool { + mut c := hono.Context.new( + http.Request{ + method: .get + url: '/test' + }, + map[string]string{}, + map[string]string{}, + '' + ) + + response := c.redirect('/relative-path') + + if response.status_code != 302 { + return false + } + + location := response.header.get_custom('Location') or { '' } + if location != '/relative-path' { + return false + } + + return true +} + +fn redirect_with_existing_headers_test() bool { + mut c := hono.Context.new( + http.Request{ + method: .get + url: '/test' + }, + map[string]string{}, + map[string]string{}, + '' + ) + + // Set some existing headers + c.headers['X-Custom-Header'] = 'custom-value' + c.headers['Cache-Control'] = 'no-cache' + + response := c.redirect('https://example.com') + + if response.status_code != 302 { + return false + } + + location := response.header.get_custom('Location') or { '' } + if location != 'https://example.com' { + return false + } + + custom_header := response.header.get_custom('X-Custom-Header') or { '' } + if custom_header != 'custom-value' { + return false + } + + cache_control := response.header.get_custom('Cache-Control') or { '' } + if cache_control != 'no-cache' { + return false + } + + return true +} + +// 测试统计结构 +struct TestStats { +mut: + total_tests int + passed_tests int + failed_tests int +} + +fn (mut stats TestStats) run_test(test_name string, test_func fn() bool) { + stats.total_tests++ + print('🧪 ${test_name}... ') + + if test_func() { + stats.passed_tests++ + println('✅') + } else { + stats.failed_tests++ + println('❌') + } +} + +fn (stats TestStats) print_summary() { + println('\n=== 重定向测试总结 ===') + println('总测试数: ${stats.total_tests}') + println('通过: ${stats.passed_tests}') + println('失败: ${stats.failed_tests}') + + if stats.failed_tests == 0 { + println('🎉 所有重定向测试通过!') + } else { + println('⚠️ 有 ${stats.failed_tests} 个测试失败') + } +} + +fn main() { + println('🚀 开始重定向功能测试...\n') + + mut stats := TestStats{} + + // 运行所有重定向测试 + stats.run_test('基本重定向 (302)', redirect_basic_test) + stats.run_test('自定义状态码重定向 (301)', redirect_with_status_test) + stats.run_test('多种状态码测试', redirect_multiple_status_codes_test) + stats.run_test('相对路径重定向', redirect_relative_url_test) + stats.run_test('保持现有头部信息', redirect_with_existing_headers_test) + + // 打印测试总结 + stats.print_summary() +} \ No newline at end of file