The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

ASP.NET Core MVC 的 Razor 語法對於 @ 符號的編碼陷阱

前幾天我們公司有個網站準備上一個新版,工程師發出 PR (Pull request) 之後請我進行程式碼審核(Code Review),結果我發現他本次的修正項目不太合理,他改了一個前端套件的資料夾名稱。我當下覺得不妙,一個已經測試數月,也準備上線的網站,怎麼還會在最後一刻修改資料夾名稱呢?我反問工程師為什麼要改這些地方時,卻得到一個我不是很滿意的回答,因此退件請他重改。這不是一個什麼嚴重的技術問題,改改資料夾名稱就正常了,但是卻又激起我追根究底的柯南精神,真相只有一個,我要找出來!🕵️‍♂️

A wide abstract conceptual image representing coding challenges in ASP NET Core MVC with Razor syntax, featuring a maze-like structure of interconnect

問題描述

在談論技術問題之前,我想先說說這個問題的來龍去脈。

  • 小龍:保哥,我剛剛發了一個PR,再麻煩你了。

  • 保哥:好喔,給我 PR 網址我看看。你改了些什麼?

  • 小龍:因為最新版部署後,後台有點問題,無法建立資料,有看到是套件資料夾名稱的問題,目前已修正。

  • 保哥:咦?你為什麼要改資料夾名稱?還有 @ 是什麼?這是 @ 符號的 HTML Entity 表示法吧?為什麼要改?你確定這裡有問題嗎?有這個字元就讀不到嗎?(如下圖)

    SNAG-0113

  • 小龍:我有在正式環境的網頁按檢視原始碼,發現打不開這個 JS 檔案,但是我在本機跟測試機都是正常的,所以我想說先修看看。

  • 保哥:你改錯了,這不可能是靠改資料夾名稱來解決問題,我先退你的 PR,你先研究一下!

解析問題

這個問題我大概花了 8 分鐘就想到問題的解法,寫文章的時間比這個多多了!😂

其實工程師小龍已經有多年開發經驗,這點小問題確實也不難解決,但是靠「改資料夾名稱」的解法,我覺得有點不太專業。就算這關過去了,意味著未來所有這類問題都只能靠改資料夾名稱解決,這樣就失去了一次寶貴的學習機會。

在解析問題的過程中,我一貫的態度都是以「合理性」出發,不合邏輯的推論一律先拒絕接受,再去小心求證可能的根本原因。我這 8 分鐘的思考與驗證過程大概是這樣:

  1. 這是一個 ASP.NET Core MVC 專案,小龍發出的 PR 主要是調整 JavaScript 與 CSS 的載入路徑,修改的檔案主要以 *.cshtml 為主。

  2. 既然修改的地方是以下這段 JavaScript 載入的地方,又跟 @ 有關係,為什麼在本機電腦測試環境可以正常讀取,但正式環境卻不行?

    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
    

    這段 HTML 實際上瀏覽器解析出來的路徑為 /lib/@microsoft/signalr/dist/browser/signalr.js

  3. 由於 &#64; 主要是由「瀏覽器」進行解析,並不是透過 ASP.NET Core MVC 的 Razor 引擎進行解析,所以這個問題應該是「瀏覽器」的問題。但為什麼在本機電腦測試環境可以正常讀取,但正式環境卻不行?

  4. 難道跟 ~ 符號有關係?因為這個符號不是給「瀏覽器」看的,而是 Razor 引擎解析的。這個 ~ 符號用來表示網站的「根目錄」,輸出成 HTML 的時候會自動轉換成網站實際部署的路徑虛擬目錄。為什麼在本機電腦測試環境可以正常讀取,但正式環境卻不行?難道跟 ASP.NET Core 的 ASPNETCORE_ENVIRONMENT 有關?我檢查了 Program.cs 的地方,確認沒有特殊的 Middleware 對這些路徑有額外處理,理論上不太可能跟 ~ 有關。

  5. 接著我去正式環境的網站看一下實際輸出的 HTML 內容,這裡發現了異狀。Razor 還真的把 &#64; 又再進行了一次 HTML 編碼,所以路徑變成了 &amp;#64; 開頭,這樣瀏覽器當然就讀不到檔案了,因為路徑是錯誤的。

    <script src="/lib/&amp;#64;microsoft/signalr/dist/browser/signalr.js"></script>
    

    這段 HTML 實際上瀏覽器解析出來的路徑為 /lib/&#64;microsoft/signalr/dist/browser/signalr.js (這是錯誤的路徑)

  6. 既然是 Razor 額外將 &#64; 多做了一次 HTML 編碼,那就不要用 HTML Entity 表示法不就好了?在 Razor 裡面可以直接用 @@ 來對 @ 符號進行跳脫處理,這樣問題不就解決了!

    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
    

    注意: 在 Razor 語法中,@ 有特殊含意,如果寫成 @microsoft 會等於要輸出 microsoft 這個變數,因此這樣寫法將會無法編譯。因此一定要寫成 @@microsoft 才能正確輸出 @microsoft 這個字串。

是的,只要把 &#64; 改寫成 @@ 就能解決問題!👍

疑難待解

問題是解決了沒錯,但是我的疑惑沒有被解開!為什麼在本機電腦測試環境可以正常讀取,但正式環境卻不行?

我等晚上有空的時候,再次深入追查此問題,這才發現,他原本的程式是這樣寫的:

<environment include="Development">
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
</environment>

<environment exclude="Development">
    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
</environment>

然後他的 PR 改成這樣:

<environment include="Development">
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
</environment>

<environment exclude="Development">
    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
</environment>

這下子我明白了,原來小龍沒有意識到網頁中有透過一個 Environment Tag Helper 來處理「正式環境」的 JavaScript 載入,所以在測試璃正式璃的 JS 載入行為確實不太一樣。

重點是:他把兩個環境的 JS 載入路徑都改成 @@ 了,其實原本在「開發環境」的那段 JS 載入是沒有錯誤的,但他還是一併修改了,反正一起改了也是可以正常載入!(沒有問題的 Code 為什麼也要改呢?) 😅

所以這個問題的根本原因是,當在「非開發環境」的時候,ASP.NET Core 的 Script Tag Helper 會因為設定 asp-append-version="true" 的關係,去對 JS 檔案加入一個以檔案內容為雜湊的版本號,這樣可以避免瀏覽器快取問題。

  • 當在「開發環境」時:

    當 Razor 語法為:

    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
    

    HTML 的輸出為:

    <script src="/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
    

    這段輸出是正確的。

  • 當在「正式環境」時:

    當 Razor 語法為:

    <script src="~/lib/&#64;microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
    

    HTML 的輸出為:

    <script src="/lib/&amp;#64;microsoft/signalr/dist/browser/signalr.js"></script>
    

    由於加上 asp-append-version="true" 之後,ASP.NET Core 的 Script Tag Helper 無法解析 &#64; 這種 HTML Entity 表示法,所以才導致路徑錯誤,抓不到檔案。重點是,Razor 引擎在執行時竟然自動忽略了這個錯誤,輸出了一個無效的 HTML Script 標籤!

    當我們將 Razor 語法修改為以下格式:

    <script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
    

    HTML 的輸出就會變成:

    <script src="/lib/@microsoft/signalr/dist/browser/signalr.js?v=rK9Zx8UOZ-yR2hZ9Ov8dBexROapvuO2mE9jAx7ZIquA"></script>
    

    因為 ASP.NET Core 的 Script Tag Helper 可以正確解析實體檔案的路徑,因此輸出的 JS 檔案路徑就成功的加入了一個 v= 查詢字串,加入了這個實體 JS 檔案的雜湊值,這才是正確運行的結果!

結語

在此,我宣告本案偵結,順利找出問題背後的真相!👍

這次事件我也跟小龍分享:「你下次要改 Bug 之前,要先想想合理性。不要球來就打,看到什麼改什麼。」

其實我們身為一個軟體開發人員,有時候不光是要解決眼前的問題,閒暇之餘更應該要思考問題的根本原因,這樣才能避免未來類似的問題再度發生。

我多年顧問與帶人的經驗也告訴我,由於「資深工程師」經驗較多,鬼故事看多了,看什麼都不覺得奇怪,自己心中也有一把尺,很知道這些「常見的問題」該怎樣解決,變成一種老師傅才知道的技藝。但是這些經驗的累積所形成的「觀念」,其實不一定正確,雖然特定類型的問題可以很快的解決,但是當問題稍微有點變化時,就會再度卡關,等實驗出「心得」,就可以再次培養出新的技藝,然後一脈相承的傳給後輩。這樣真的好嗎?😅

現在應該越來越多人靠 AI 在寫 Code,但如何有效的整理軟體開發的上下文(Context)是需要深入思考的 (俗稱通靈),這部分 AI 很難幫忙,短期內都不可能真正取代軟體工程師。很多時候問題發生時並不是我們眼睛看到的那樣,多想個一兩步,利用人類強大的邏輯推理能力分析一下脈絡,甚至靠 AI 發覺一些新知與解題方向,再搭配一點驗證知識正確性的能力,我們才有可能在 AI 賦能的未來持續維持競爭力!🚀

相關連結

留言評論