啟用 Razor 執行階段編譯 (Enable Razor runtime compilation) 技術細節探索 | The Will Will Web

The Will Will Web

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

啟用 Razor 執行階段編譯 (Enable Razor runtime compilation) 技術細節探索

其實我們這幾年幾乎都在用前後端分離的架構在開發網站,單純使用 ASP.NET Core MVC 的機會並不多,但是我知道很多人還是在用 MVC 在維護網站,而且 ASP.NET Core 預設會將所有 Razor 頁面 (Views) 編譯成 DLL 檔,網站啟動之後如果要修改 View 的內容,還需要重新編譯專案才能測試到新的結果,非常不方便。如果要啟用所謂的 Razor 執行階段編譯 (Razor runtime compilation) 就要對專案做出一點設定,但是網路上能查到的文件都沒有對技術細節講的足夠深入,所以我也花了一些時間探索了許多技術細節,打算在這篇文章呈現。

在建立專案時啟用 Razor 執行階段編譯

這應該是最簡單、最無腦的啟用方式!

  1. 建立專案時使用 --razor-runtime-compilation-rrc 參數

    dotnet new webapp --razor-runtime-compilation
    

雖然啟用 Razor 執行階段編譯很簡單,但是不瞭解背後的原理,很難舉一反三靈活運用。

在開發偵錯時啟用 Razor 執行階段編譯

如果要將現有專案加入 Razor 執行階段編譯 能力,可以參考以下步驟進行:

  1. 加入 Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 套件

    dotnet add package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
    

    這個動作會在你的 *.csproj 專案檔加入以下套件:

    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="5.0.10" />
    </ItemGroup>
    

    除此之外,也會在專案檔加入以下 MSBuild 屬性:

    <CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
    

    這個 CopyRefAssembliesToPublishDirectory 屬性在網路上很難找到完整的資訊,我花了好多時間才研究出端倪,我留到文章最後再一併說明。

  2. 使用「環境變數」啟用 Razor 執行階段編譯

    你可以在 Properties/launchSettings.json (啟動設定檔) 中加入以下 2 個環境變數:

    "environmentVariables": {
      "ASPNETCORE_ENVIRONMENT": "Development",
      "ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"
    }
    

    這裡的 ASPNETCORE_HOSTINGSTARTUPASSEMBLIES 可以指定 ASP.NET Core 應用程式的啟動組件,他會自動找出該組件中的 HostingStartup attribute,並在應用程式啟動時執行特定程式碼!

  3. 使用「程式碼」啟用 Razor 執行階段編譯

    如果你寫 ASP.NET Core MVC 的話,可以在 Startup.cs 加入這段:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllersWithViews().AddRazorRuntimeCompilation();
    }
    

    如果你寫 ASP.NET Core Razor Page 的話,可以在 Startup.cs 加入這段:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages().AddRazorRuntimeCompilation();
    }
    

    基本上使用「程式碼」的方式啟用 Razor runtime compilation 比較沒有彈性,透過環境變數來啟用這個功能還是相對單純許多!當然,你要用 C# 條件式編譯 的方法有條件的啟用也是可以的!

在正式部署時啟用 Razor 執行階段編譯

這個功能我們在開發時期套用確實是非常方便,但是在實際部署的時候可否也啟用 Razor runtime compilation 呢?這是完全可以的,只要額外在 *.csproj 專案檔加入一個 MSBuild 屬性 (Properties) 即可!

以下是啟用 Razor 執行階段編譯的專案範本會加入的屬性設定,但是這個 CopyRefAssembliesToPublishDirectory 屬性的相關文件非常缺乏,我找了好久都找不到完整的脈絡,只好從原始碼著手研究這個屬性背後的真正意義。

<PropertyGroup>
  <CopyRefAssembliesToPublishDirectory>false</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>

事實上,你只要把這個屬性改成 true 就可以做到發佈後的版本也能啟用 Razor 執行階段編譯功能,只是啟用的有點不太明顯,因為發佈的時候並不會把 Views 資料夾也一併輸出到發佈的目標目錄!

<PropertyGroup>
  <CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
</PropertyGroup>

網路上看到的資料,都是建議額外加上 RazorCompileOnPublish 屬性宣告,並設定為 false,讓專案在發佈的時候不要編譯 Razor 頁面,而不要編譯 Razor 頁面就意味著會將 Views 直接輸出到發佈資料夾

<PropertyGroup>
  <CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
  <RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>

實際部署到正式環境後,記得啟用 Razor 執行階段編譯有兩種方式:

  1. 使用「環境變數」啟用 Razor 執行階段編譯

    啟動網站前別忘了要加上 ASPNETCORE_HOSTINGSTARTUPASSEMBLIES 環境變數,並設定為 Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 才能正確啟用此功能。

    如果 ASP.NET Core 網站是部署到 IIS 底下,記得要去調整 web.config 的內容,加上環境變數設定:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <location path="." inheritInChildApplications="false">
        <system.webServer>
          <handlers>
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
          </handlers>
        <aspNetCore processPath="dotnet" arguments=".\WebApp1EnableRRC.dll" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" hostingModel="inprocess">
          <environmentVariables>
            <environmentVariable name="ASPNETCORE_HOSTINGSTARTUPASSEMBLIES" value="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation"/>
          </environmentVariables>
        </aspNetCore>
        </system.webServer>
      </location>
    </configuration>
    
  2. 使用「程式碼」啟用 Razor 執行階段編譯

    你只要有在 Startup.ConfigureServices 加入 .AddRazorRuntimeCompilation() 就不用再做任何設定!

深入研究當中的技術細節

我也不知道哪根筋不對,雖然已經得到想要的結果,但就是還想更清楚的知道這兩個屬性(CopyRefAssembliesToPublishDirectory, RazorCompileOnPublish)到底在幹嘛,因為網路上的資料非常希缺,所以花了不少時間研究。

RazorCompileOnPublish 從字面上來看很容易理解,就是要不要在 Publish 的時候編譯我們專案中的 Razor 頁面,將所有 Views 在發行時編譯成 DLL 檔,加快 Views 的執行速度。這個屬性預設值是 true

CopyRefAssembliesToPublishDirectory 從字面上來看就沒有很容易理解,什麼叫做 複製參考組件到發行目錄?我只能從 ASP.NET Core 2.1 版的 ASP.NET Core Razor SDK 文件看到以下解釋:

When true, copy reference assembly items to the publish directory. Typically, reference assemblies aren't required for a published app if Razor compilation occurs at build-time or publish-time. Set to true if your published app requires runtime compilation. For example, set the value to true if the app modifies .cshtml files at runtime or uses embedded views. Defaults to false.

我看不太懂這裡寫的 reference assembly items 是什麼意思?但這份文件到 ASP.NET Core 3.1 之後就移除了這段說明,我從 ASP.NET Core 5.0 的同份 ASP.NET Core Razor SDK 文件只看的到 Runtime compilation of Razor views 說明如下:

By default, the Razor SDK doesn't publish reference assemblies that are required to perform runtime compilation. This results in compilation failures when the application model relies on runtime compilation—for example, the app uses embedded views or changes views after the app is published. Set CopyRefAssembliesToPublishDirectory to true to continue publishing reference assemblies.

這段話我大部分都看的懂,唯獨以下兩點疑慮:

  1. 何謂 reference assemblies 呢?到底在講誰?專案的參考組件?還是誰參考的組件?

    關於這一點,在我完整理解來龍去脈之後,才意會到我並沒有完整理解這段英文的意思,我只看了 the Razor SDK doesn't publish reference assemblies 而已,大腦卻自動忽略了 that are required to perform runtime compilation 這段話。所以其完整的意思就是:

    Razor SDK 預設不會發佈 為了執行 Razor 執行階段編譯 所需參考到的組件 (assemblies)!

    所以這裡的 reference assemblies 跟你的專案並沒有任何關係,主要是 Razor SDK 參考到的組件為主!

  2. 官方文件雖然說要將 CopyRefAssembliesToPublishDirectory 設定為 true,但是網路上大部分人的文章都說只要將 RazorCompileOnPublish 設定為 false 就好,為什麼設定為 truefalse 都可以正常運作呢?那我為什麼還要去設定他呢?

    我為了理解這個問題,先嘗試了這兩個屬性設定的所有種組合:

    1. RazorCompileOnPublish=false , CopyRefAssembliesToPublishDirectory=false
    2. RazorCompileOnPublish=false , CopyRefAssembliesToPublishDirectory=true
    3. RazorCompileOnPublish=true , CopyRefAssembliesToPublishDirectory=false
    4. RazorCompileOnPublish=true , CopyRefAssembliesToPublishDirectory=true

    我發現只要 RazorCompileOnPublish 設定為 false, 這個 CopyRefAssembliesToPublishDirectory 屬性就沒有意義了,因為最終一定會被設定為 true,不然你發行之後根本無法執行 Razor 執行階段編譯,我覺得這樣的設計是非常合理的!那何時設定 CopyRefAssembliesToPublishDirectory 屬性才有意義呢?是的,當然就是設定 RazorCompileOnPublishtrue 的時候才有意義!

    當你將 RazorCompileOnPublish 設定為 true,意味著我要編譯所有的 Views 成為 DLL 組件,此時還能實現 Razor 執行階段編譯嗎?答案是:可以!只要你將 CopyRefAssembliesToPublishDirectory 設定為 true 即可,設定好之後會在發佈後的資料夾發現一個 refs 目錄,這個目錄下的所有組件就是 Razor SDK 在對 Razor 進行編譯時所需要的組件!

在上述疑惑都一一撥雲見日之後,你可以在發行 ASP.NET Core 網站時對現有的 Views 進行編譯,享受優異的 Views 執行效能。但在有需要的時候「手動」將 Views 資料夾複製到部署的網站主機,同時享用 Razor 執行階段編譯的便利性,真是一舉兩得!👍

以下就是這種特殊情境的 *.csproj 設定範例:

<PropertyGroup>
  <CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
  <RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>

另外還有一種特殊情境,就是你希望編譯 Razor 頁面,同時又要啟用 Razor 執行階段編譯,又同時要在發行時將 Views 也一併產生到發行目錄,你只要再加上一個 CopyRazorGenerateFilesToPublishDirectory 屬性即可,範例如下:

<PropertyGroup>
  <CopyRefAssembliesToPublishDirectory>true</CopyRefAssembliesToPublishDirectory>
  <RazorCompileOnPublish>true</RazorCompileOnPublish>
  <CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>

但由於 CopyRefAssembliesToPublishDirectoryRazorCompileOnPublish 的預設值都是 true,所以其實你只要這樣寫即可:

<PropertyGroup>
  <CopyRazorGenerateFilesToPublishDirectory>true</CopyRazorGenerateFilesToPublishDirectory>
</PropertyGroup>

Amazing! 😃

直播影片

相關連結