「系統要同時支援兩套介面版本,是不是要把路由複製一份,各自對應一套 controller?」
這是多數人聽到「一套系統要同時維護兩個版本的畫面」時,第一個冒出來的直覺做法——路由加前綴分版本、controller 各寫一份。但今天要看的真實案例,用的是一個完全不同、而且更輕量的做法:同一組路由,靠一支 middleware 動態決定要載入哪一套畫面。
這個系列的素材專案有兩套前台介面(先稱它們主題一、主題二),乍看之下很容易讓人以為背後是兩套獨立的路由跟 controller——實際上完全不是。翻開路由設定檔會發現:根本沒有版本前綴、也沒有分開的路由檔案。同一個路由名稱、同一個 controller,兩套主題共用同一套後端邏輯。
真正決定「這次請求要看到哪一套畫面」的,是一支 middleware。
SetTheme middleware 怎麼運作這支 middleware 的核心邏輯,簡化後大致是這樣:
protected function registerThemeViewPaths(string $theme): void
{
$themePath = resource_path("views/{$theme}");
$finder = View::getFinder();
$finder->prependLocation($themePath);
Blade::anonymousComponentPath($themePath.'/components');
}
它做的事情是:先決定這次請求該用哪一套主題(可能來自 query string 參數、也可能來自 session 裡記住的使用者偏好),然後呼叫 Laravel 的 View::getFinder()->prependLocation(),把對應主題的 View 目錄,插進 View 尋找路徑的最前面。
要理解這支 middleware 為什麼有效,得先知道 Laravel 怎麼把一個 View 名稱(例如 home)解析成實際要載入的檔案。Laravel 內部有一個 View Finder,維護著一份「要去哪些目錄底下找 View 檔案」的路徑清單,預設情況下這份清單通常只有一個目錄(resources/views)。當程式碼呼叫 view('home') 時,View Finder 會依序在清單裡的每個目錄找有沒有 home.blade.php,找到第一個符合的就用它。
prependLocation() 這個方法,做的事情是把一個新的目錄插到清單最前面,而不是加到最後。這代表:只要這個目錄底下有同名的 View 檔案,它就會比預設目錄更早被找到、優先被使用。
所以整個機制串起來是這樣:controller 完全不知道現在是哪一套主題,它一樣呼叫 view('home');但因為 middleware 已經在請求進來的時候,把對應主題的目錄插到 View Finder 清單最前面,所以最終被載入的,會是那套主題底下的 home.blade.php,而不是預設目錄裡的版本。同一段程式碼路徑,因為執行期的 View Finder 狀態不同,渲染出完全不同的畫面。
Anonymous component(不用註冊、直接用檔名當標籤名稱的 Blade component)的路徑也用同樣的邏輯註冊,確保兩套主題各自的元件庫也能正確切換。
理解機制之後,回頭看這個設計的取捨:如果改用「複製一份路由、各自對應一套 controller」的做法,兩套主題共用的業務邏輯(資料查詢、授權判斷、路由參數解析)全部都要維護兩份,任何一個邏輯修正都要同步改兩個地方,很容易漏改其中一份導致兩套主題行為不一致。
用 View Finder 切換主題,controller 跟業務邏輯只有一份,兩套主題之間唯一的差異被限制在「畫面長什麼樣」這一層,不會外溢到業務邏輯層。這也解釋了為什麼這個系列稍早看到的測試檔案裡,會有兩組幾乎同名的測試(分屬兩套主題各自的資料夾)——它們測的是同一個 controller/路由,在不同 View Finder 設定下渲染出的不同畫面,不是兩套獨立的後端邏輯。
這支 middleware 解決了「兩套畫面共用一份邏輯」的問題,但也帶來一個容易被忽略的風險——這個系列前幾天講過的 sitemap Content-Type bug(第 12 天),根因正是這支 middleware 原本被套用在不該套用的路由上:sitemap.xml 這個路由回傳的是 XML 內容,完全不需要主題切換這個機制,但因為 middleware 掛在一個共用的路由群組上,這個路由也被牽連,SetTheme 執行過程中觸碰到的 session 邏輯,干擾了 XML 回應本身。
這是一個值得記住的教訓:一個機制被多個路由共用,代表任何時候要調整這個機制,都要盤點清楚「所有依賴它的路由」,而不能只想著「我要改的那個路由」——因為改動的影響範圍,往往比你以為的更廣。
回想你的專案裡,有沒有一個 middleware 或共用邏輯,掛在一個比較大的路由群組上,但其實只有部分路由真的需要它?如果現在要調整這個邏輯,你有把握列出所有真正依賴它的路由清單嗎?
View::getFinder()->prependLocation() 把對應主題的目錄插進 View 尋找路徑最前面,controller 完全不用知道現在是哪套主題明天要看另一個真實案例:當上傳的檔案結構換了新的路徑規則,累積在舊路徑底下的歷史資料要怎麼安全搬過去,而不會在搬遷過程中弄丟任何一個檔案。