feat: add redirect method

This commit is contained in:
meiseayoung
2025-12-30 14:25:35 +08:00
parent 6fe79d298d
commit 645d8c2dda
5 changed files with 469 additions and 0 deletions
+7
View File
@@ -58,6 +58,11 @@ fn main() {
return c.json('{"user_id": "${user_id}"}') 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') app.listen(':3000')
} }
``` ```
@@ -157,6 +162,7 @@ fn main() {
- `c.json(data)` - Return JSON response - `c.json(data)` - Return JSON response
- `c.html(data)` - Return HTML response - `c.html(data)` - Return HTML response
- `c.file(path)` - Return file 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.status(code)` - Set response status code
- `c.params` - Route parameters - `c.params` - Route parameters
- `c.query` - Query parameters - `c.query` - Query parameters
@@ -198,6 +204,7 @@ See the `examples/` directory for more examples:
- `examples/basic/` - Basic usage - `examples/basic/` - Basic usage
- `examples/middleware/` - Middleware usage - `examples/middleware/` - Middleware usage
- `examples/route_grouping/` - Route grouping - `examples/route_grouping/` - Route grouping
- `examples/redirect_demo.v` - Redirect functionality examples
## License ## License
+151
View File
@@ -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)
```
+75
View File
@@ -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('<h1>Success!</h1><p>Form submitted successfully.</p>')
})
app.get('/mobile-version', fn (mut c hono.Context) http.Response {
return c.html('<h1>Mobile Version</h1>')
})
app.get('/desktop-version', fn (mut c hono.Context) http.Response {
return c.html('<h1>Desktop Version</h1>')
})
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')
}
+28
View File
@@ -256,6 +256,34 @@ pub fn (mut c Context) status(code int) {
c.status_code = code 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 // 处理器接口,使用 Context
+208
View File
@@ -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()
}