前幾天我們公司有個網站準備上一個新版,工程師發出 PR (Pull request) 之後請我進行程式碼審核(Code Review),結果我發現他本次的修正項目不太合理,他改了一個前端套件的資料夾名稱。我當下覺得不妙,一個已經測試數月,也準備上線的網站,怎麼還會在最後一刻修改資料夾名稱呢?我反問工程師為什麼要改這些地方時,卻得到一個我不是很滿意的回答,因此退件請他重改。這不是一個什麼嚴重的技術問題,改改資料夾名稱就正常了,但是卻又激起我追根究底的柯南精神,真相只有一個,我要找出來!🕵️♂️
問題描述
在談論技術問題之前,我想先說說這個問題的來龍去脈。
-
小龍:保哥,我剛剛發了一個PR,再麻煩你了。
-
保哥:好喔,給我 PR 網址我看看。你改了些什麼?
-
小龍:因為最新版部署後,後台有點問題,無法建立資料,有看到是套件資料夾名稱的問題,目前已修正。
-
保哥:咦?你為什麼要改資料夾名稱?還有 @
是什麼?這是 @
符號的 HTML Entity 表示法吧?為什麼要改?你確定這裡有問題嗎?有這個字元就讀不到嗎?(如下圖)
-
小龍:我有在正式環境的網頁按檢視原始碼,發現打不開這個 JS 檔案,但是我在本機跟測試機都是正常的,所以我想說先修看看。
-
保哥:你改錯了,這不可能是靠改資料夾名稱來解決問題,我先退你的 PR,你先研究一下!
解析問題
這個問題我大概花了 8 分鐘就想到問題的解法,寫文章的時間比這個多多了!😂
其實工程師小龍已經有多年開發經驗,這點小問題確實也不難解決,但是靠「改資料夾名稱」的解法,我覺得有點不太專業。就算這關過去了,意味著未來所有這類問題都只能靠改資料夾名稱解決,這樣就失去了一次寶貴的學習機會。
在解析問題的過程中,我一貫的態度都是以「合理性」出發,不合邏輯的推論一律先拒絕接受,再去小心求證可能的根本原因。我這 8 分鐘的思考與驗證過程大概是這樣:
-
這是一個 ASP.NET Core MVC 專案,小龍發出的 PR 主要是調整 JavaScript 與 CSS 的載入路徑,修改的檔案主要以 *.cshtml
為主。
-
既然修改的地方是以下這段 JavaScript 載入的地方,又跟 @
有關係,為什麼在本機電腦與測試環境可以正常讀取,但正式環境卻不行?
<script src="~/lib/@microsoft/signalr/dist/browser/signalr.js"></script>
這段 HTML 實際上瀏覽器解析出來的路徑為 /lib/@microsoft/signalr/dist/browser/signalr.js
-
由於 @
主要是由「瀏覽器」進行解析,並不是透過 ASP.NET Core MVC 的 Razor 引擎進行解析,所以這個問題應該是「瀏覽器」的問題。但為什麼在本機電腦與測試環境可以正常讀取,但正式環境卻不行?
-
難道跟 ~
符號有關係?因為這個符號不是給「瀏覽器」看的,而是 Razor 引擎解析的。這個 ~
符號用來表示網站的「根目錄」,輸出成 HTML 的時候會自動轉換成網站實際部署的路徑或虛擬目錄。為什麼在本機電腦與測試環境可以正常讀取,但正式環境卻不行?難道跟 ASP.NET Core 的 ASPNETCORE_ENVIRONMENT
有關?我檢查了 Program.cs
的地方,確認沒有特殊的 Middleware 對這些路徑有額外處理,理論上不太可能跟 ~
有關。
-
接著我去正式環境的網站看一下實際輸出的 HTML 內容,這裡發現了異狀。Razor 還真的把 @
又再進行了一次 HTML 編碼,所以路徑變成了 &#64;
開頭,這樣瀏覽器當然就讀不到檔案了,因為路徑是錯誤的。
<script src="/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
這段 HTML 實際上瀏覽器解析出來的路徑為 /lib/@microsoft/signalr/dist/browser/signalr.js
(這是錯誤的路徑)
-
既然是 Razor 額外將 @
多做了一次 HTML 編碼,那就不要用 HTML Entity 表示法不就好了?在 Razor 裡面可以直接用 @@
來對 @
符號進行跳脫處理,這樣問題不就解決了!
<script src="~/lib/@@microsoft/signalr/dist/browser/signalr.js"></script>
注意: 在 Razor 語法中,@
有特殊含意,如果寫成 @microsoft
會等於要輸出 microsoft
這個變數,因此這樣寫法將會無法編譯。因此一定要寫成 @@microsoft
才能正確輸出 @microsoft
這個字串。
是的,只要把 @
改寫成 @@
就能解決問題!👍
疑難待解
問題是解決了沒錯,但是我的疑惑沒有被解開!為什麼在本機電腦與測試環境可以正常讀取,但正式環境卻不行?
我等晚上有空的時候,再次深入追查此問題,這才發現,他原本的程式是這樣寫的:
<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>
然後他的 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/@microsoft/signalr/dist/browser/signalr.js"></script>
這段輸出是正確的。
-
當在「正式環境」時:
當 Razor 語法為:
<script src="~/lib/@microsoft/signalr/dist/browser/signalr.js" asp-append-version="true"></script>
HTML 的輸出為:
<script src="/lib/&#64;microsoft/signalr/dist/browser/signalr.js"></script>
由於加上 asp-append-version="true"
之後,ASP.NET Core 的 Script Tag Helper 無法解析 @
這種 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 賦能的未來持續維持競爭力!🚀
相關連結