feat: add redirect method
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
```
|
||||||
@@ -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')
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user