PHP 重定向看似簡單,實則涉及 Header 已發送檢測、輸出緩衝區狀態、編碼安全、JavaScript 降級、延遲跳轉、AJAX 響應兼容等多重工程細節。本文提供一個生產級通用 redirect() 函數,支持五種跳轉模式智能選擇,並深度解析每種模式的適用場景、安全陷阱與框架集成最佳實踐。
/**
* 通用重定向函數
*
* @param string $url 目標 URL
* @param int $delay 延遲秒數(0=立即)
* @param bool $js 是否返回 JS 代碼而非直接跳轉
* @param bool $jsWrapped JS 代碼是否包裝在 ';
if ($return) return $jsCode;
echo $jsCode;
exit;
}
| 模式 | 參數組合 | 輸出形式 | 適用場景 |
|---|---|---|---|
| 原生立即跳轉 | redirect('/url') | HTTP 302 Header | 常規表單提交後、登錄驗證後、權限攔截 |
| 原生延遲跳轉 | redirect('/url', 3) | HTML Meta Refresh | 需提示用戶「3秒後跳轉」、Header已發送時 |
| JS 立即跳轉 | redirect('/url', 0, true) | AJAX 請求返回、iframe 內跳轉、需執行前置 JS | |
| JS 延遲跳轉 | redirect('/url', 3, true) | AJAX 返回倒計時提示、需倒計時 UI | |
| 獲取代碼不輸出 | redirect('/url', 0, true, true, true) | 返回 JS 字符串 | 模板賦值、API 響應構建、單元測試 |
rawurlencode($url) + JS 端 decodeURIComponent() 雙重保障,防止 URL 中包含單引號、換行符、腳本標簽導致 JS 語法錯誤或 XSS:
// 惡意輸入:'javascript:alert(1)//
// 未編碼輸出:location.href = 'javascript:alert(1)//' → 執行腳本
// 編碼後:location.href = decodeURIComponent('%27javascript%3Aalert%281%29%2F%2F') → 安全字符串
headers_sent() 檢測輸出緩衝區是否已刷新。若已發送,無法發送 Header,必須降級 HTML/JS 模式。常見導致已發送的原因:
display_errors=On)echo/var_dump/print_rif (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') {
// AJAX 請求:返回 JSON 包含跳轉信息
exit(json_encode(['code' => 302, 'url' => $url, 'message' => '登錄成功']));
} else {
// 普通請求:使用 redirect()
redirect($url);
}
// 基礎控制器封裝
class BaseController {
protected function redirect($url, $delay = 0, $message = '') {
if ($this->isAjax()) {
return $this->ajaxReturn([
'code' => 302,
'url' => $url,
'message' => $message ?: '操作成功',
'delay' => $delay
]);
}
// 非 AJAX 使用通用函數
redirect($url, $delay, false, true, false); // 直接輸出並 exit
}
protected function isAjax() {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
}
// 測試用例
public function testRedirectReturnsJsCode() {
$code = redirect('/login', 0, true, true, true); // $return=true
$this->assertStringContainsString('location.href', $code);
$this->assertStringContainsString('decodeURIComponent', $code);
$this->assertNotContains('