ASP.NET Core 裝載式 Blazor WebAssembly 應用程式的部署配置

本文說明如何在封鎖下載和執行動態連結程式庫 (DLL) 檔案的環境中啟用裝載的 Blazor WebAssembly 部署。

注意

本指引將用於因應阻擋用戶端下載和執行 DLL 的環境。 在 .NET 8 或更新版本中,Blazor 會使用 Webcil 檔案格式解決此問題。 如需詳細資訊,請參閱裝載和部署 ASP.NET Core Blazor WebAssembly。 .NET 8 或更新版本中的 Blazor 應用程式不支援使用實驗性 NuGet 套件進行多部分統合 (如本文所述)。 如需詳細資訊,請參閱增強 Microsoft.AspNetCore.Components.WebAssembly.Multipart 組合套件以定義自訂套件組合格式 (dotnet/aspnetcore #36978)。 您可以使用本文中的指引,為 .NET 8 或更新版本建立自己的多部分統合 NuGet 套件。

Blazor WebAssembly 應用程式需要動態連結程式庫 (DLL) 才能運作,但某些環境會阻擋用戶端下載和執行 DLL。 在這些環境的子集中,變更 DLL 檔案的副檔名 (.dll) 就足以略過安全性限制,但安全性產品通常能夠掃描周遊網路的檔案內容並封鎖或隔離 DLL 檔案。 本文將說明在這些環境中啟用 Blazor WebAssembly 應用程式的一種方法,其中會從應用程式的 DLL 建立多部分套件組合檔案,讓您可以略過安全性限制來一起下載 DLL。

裝載的 Blazor WebAssembly 應用程式可以使用下列功能來自訂其已發佈的檔案和應用程式 DLL 的封裝:

  • 允許自訂 開機處理序的 JavaScript 初始設定式Blazor。
  • MSBuild 擴充性,可轉換已發佈檔案的清單並定義 Blazor 發佈擴充。 Blazor 發佈擴充是在發佈流程期間定義的檔案,可為執行已發佈 Blazor WebAssembly 應用程式所需的一組檔案提供替代表示法。 本文中會建立 Blazor 發佈擴充來產生多部分套件組合,其中會將所有應用程式的 DLL 封裝成單一檔案,讓 DLL 可一起下載。

本文所示範的方法可作為開發人員設計自己的策略和自訂載入流程的起點。

警告

對於為規避安全性限制而採取的任何方法,都必須仔細考慮其安全性影響。 建議您先與組織的網路安全性專業人員一起探索該主題,然後再採用本文中的方法。 要考慮的替代方案包括:

  • 啟用安全性應用裝置和安全性軟體,以允許網路用戶端下載並使用 Blazor WebAssembly 應用程式所需的確切檔案。
  • 從 Blazor WebAssembly 主控模型切換至 Blazor Server 主控模型,這會維護伺服器上所有應用程式的 C# 程式碼,而且不需要將 DLL 下載到用戶端。 Blazor Server 也提供讓 C# 程式碼保持隱密的優點,而不需要像 Blazor WebAssembly 應用程式一樣使用 Web API 應用程式來保持 C# 程式碼的隱私。

實驗性 NuGet 套件和範例應用程式

本文所述的方法適用於以 .NET 6 或更新版本為目標之應用程式的實驗Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle套件(NuGet.org)。 套件包含 MSBuild 目標,可自訂 Blazor 發佈輸出和 JavaScript 初始設定式來使用自訂開機資源載入器,本文稍後會詳細說明每個載入器。

實驗性程式碼 (包括 NuGet 套件參考來源和 CustomPackagedApp 範例應用程式)

警告

實驗性功能和預覽功能是為了收集意見反應而提供,不支援用於生產環境。

在本文稍後的透過 NuGet 套件自訂 Blazor WebAssembly 載入流程一節中,其中三個小節會提供在 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 套件中進行設定和編寫程式碼的詳細說明。 當您為 Blazor WebAssembly 應用程式建立自己的策略和自訂載入流程時,請務必了解詳細說明。 若要使用已發佈、實驗性、不支援且無自訂的 NuGet 套件作為本機示範,請執行下列步驟:

  1. 使用現有的裝載式 Blazor WebAssembly解決方案,或是使用 Visual Studio 或將 -ho|--hosted 選項傳遞至 dotnet new 命令 (dotnet new blazorwasm -ho),從 Blazor WebAssembly 專案範本中建立新的解決方案。 如需詳細資訊,請參閱 ASP.NET Core Blazor 工具。

  2. Client 專案中,新增實驗性 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 套件。

    注意

    如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

  3. Server 專案中,新增端點來提供套件組合檔案 (app.bundle)。 您可以在本文的從主機伺服器應用程式中提供套件組合一節中找到範例程式碼。

  4. 在發行組態中發佈應用程式。

透過 NuGet 套件自訂 Blazor WebAssembly 載入流程

警告

本節與其中三個小節的指引包含從頭建置 NuGet 套件,以實作您自己的策略和自訂載入流程。 .NET 6 和 7 的實驗性Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle 套件 (NuGet.org) 是以本節中的指引為基礎。 在多部分套件組合下載方法的本機示範中使用提供的套件時,您不需要遵循本節中的指引。 如需如何使用所提供套件的指引,請參閱實驗性 NuGet 套件和範例應用程式一節。

Blazor 應用程式資源會封裝成多部分套件組合檔案,並由瀏覽器透過自訂 JavaScript (JS) 初始設定式載入。 對於透過 JS 初始設定式使用套件的應用程式,應用程式只需要在要求時提供套件組合檔案。 此方法的其他所有層面都會以透明方式處理。

預設的已發佈 Blazor 應用程式載入方式需要四個自訂項目:

  • 要轉換發佈檔案的 MSBuild 工作。
  • 具有 MSBuild 目標的 NuGet 套件,用於連結至 Blazor 發佈流程、轉換輸出,並定義一個或多個 Blazor 發佈擴充檔案 (在此案例中為單一套件組合)。
  • 用來更新 Blazor WebAssembly 資源載入器回呼的 JS 初始設定式,使其可載入套件組合,並為應用程式提供個別檔案。
  • 主機 Server 應用程式上的協助程式,用來確保套件組合會依要求提供給用戶端。

建立 MSBuild 工作以自訂已發佈檔案的清單,並定義新的擴充

將 MSBuild 工作建立為公用 C# 類別,該類別可以作為 MSBuild 編譯的一部分匯入,並可與組建互動。

C# 類別需要下列項目:

注意

本文範例的 NuGet 套件是以 Microsoft 提供的套件命名(Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle)。 如需有關命名和產生自己的 NuGet 套件的指引,請參閱下列 NuGet 文章:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Build.Framework" Version="{VERSION}" />
    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="{VERSION}" />
  </ItemGroup>

</Project>

在 NuGet.org 上決定 {VERSION} 預留位置的最新套件版本:

若要建立 MSBuild 工作,請建立公用 C# 類別來擴充 Microsoft.Build.Utilities.Task (不是 System.Threading.Tasks.Task) 並宣告三個屬性:

  • PublishBlazorBootStaticWebAsset:要為 Blazor 應用程式發佈的檔案清單。
  • BundlePath:撰寫套件組合的路徑。
  • Extension:要包含在組建中的新發佈擴充。

下列 BundleBlazorAssets 類別範例是進行進一步自訂的起點:

  • Execute 方法中,套件組合會從下列三種檔案類型中建立:
    • JavaScript 檔案 (dotnet.js)
    • WASM 檔案 (dotnet.wasm)
    • 應用程式 DLL (.dll)
  • 已建立 multipart/form-data 套件組合。 每個檔案和其個別描述都會透過 Content-Disposition 標頭Content-Type 標頭新增至套件組合。
  • 建立套件組合之後,套件組合就會寫入檔案。
  • 組建會針對擴充進行設定。 下列程式碼會建立擴充項目,並將其新增至 Extension 屬性。 每個擴充項目都包含三項資料:
    • 擴充檔案的路徑。
    • 相對於 Blazor WebAssembly 應用程式根目錄的 URL 路徑。
    • 擴充的名稱,其會將指定擴充所產生的檔案分組。

完成上述目標之後,就可建立 MSBuild 工作來自訂 Blazor 發佈輸出。 Blazor 會負責收集擴充,並確定擴充會複製到發佈輸出資料夾中的正確位置 (例如:bin\Release\net6.0\publish)。 相同的最佳化 (例如壓縮) 會套用至 JavaScript、WASM 和 DLL 檔案,如同 Blazor 會套用到其他檔案。

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks/BundleBlazorAssets.cs

using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

namespace Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks
{
    public class BundleBlazorAssets : Task
    {
        [Required]
        public ITaskItem[]? PublishBlazorBootStaticWebAsset { get; set; }

        [Required]
        public string? BundlePath { get; set; }

        [Output]
        public ITaskItem[]? Extension { get; set; }

        public override bool Execute()
        {
            var bundle = new MultipartFormDataContent(
                "--0a7e8441d64b4bf89086b85e59523b7d");

            foreach (var asset in PublishBlazorBootStaticWebAsset)
            {
                var name = Path.GetFileName(asset.GetMetadata("RelativePath"));
                var fileContents = File.OpenRead(asset.ItemSpec);
                var content = new StreamContent(fileContents);
                var disposition = new ContentDispositionHeaderValue("form-data");
                disposition.Name = name;
                disposition.FileName = name;
                content.Headers.ContentDisposition = disposition;
                var contentType = Path.GetExtension(name) switch
                {
                    ".js" => "text/javascript",
                    ".wasm" => "application/wasm",
                    _ => "application/octet-stream"
                };
                content.Headers.ContentType = 
                    MediaTypeHeaderValue.Parse(contentType);
                bundle.Add(content);
            }

            using (var output = File.Open(BundlePath, FileMode.OpenOrCreate))
            {
                output.SetLength(0);
                bundle.CopyToAsync(output).ConfigureAwait(false).GetAwaiter()
                    .GetResult();
                output.Flush(true);
            }

            var bundleItem = new TaskItem(BundlePath);
            bundleItem.SetMetadata("RelativePath", "app.bundle");
            bundleItem.SetMetadata("ExtensionName", "multipart");

            Extension = new ITaskItem[] { bundleItem };

            return true;
        }
    }
}

撰寫 NuGet 套件以自動轉換發佈輸出

產生具有 MSBuild 目標的 NuGet 套件,這些目標會在參考套件時自動包含在其中:

  • 建立新的 Razor 類別庫 (RCL) 專案
  • 依照 NuGet 慣例建立目標檔案,以在取用專案中自動匯入套件。 例如,建立 build\net6.0\{PACKAGE ID}.targets,其中 {PACKAGE ID} 是套件的套件識別碼。
  • 從包含 MSBuild 工作的類別庫收集輸出,並確認輸出已封裝在正確的位置。
  • 新增必要的 MSBuild 程式碼以附加至 Blazor 管線,並叫用 MSBuild 工作來產生套件組合。

本節所述的方法只會使用套件來傳遞目標和內容,這與套件包含程式庫 DLL 的大部分套件不同。

警告

本節所述的範例套件會示範如何自訂 Blazor 發佈流程。 範例 NuGet 套件僅供本機示範使用。 不支援在生產環境中使用此套件。

注意

本文範例的 NuGet 套件是以 Microsoft 提供的套件命名(Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle)。 如需有關命名和產生自己的 NuGet 套件的指引,請參閱下列 NuGet 文章:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.csproj

<Project Sdk="Microsoft.NET.Sdk.Razor">

  <PropertyGroup>
    <NoWarn>NU5100</NoWarn>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <Description>
      Sample demonstration package showing how to customize the Blazor publish 
      process. Using this package in production is not supported!
    </Description>
    <IsPackable>true</IsPackable>
    <IsShipping>true</IsShipping>
    <IncludeBuildOutput>false</IncludeBuildOutput>
  </PropertyGroup>

  <ItemGroup>
    <None Update="build\**" 
          Pack="true" 
          PackagePath="%(Identity)" />
    <Content Include="_._" 
             Pack="true" 
             PackagePath="lib\net6.0\_._" />
  </ItemGroup>

  <Target Name="GetTasksOutputDlls" 
          BeforeTargets="CoreCompile">
    <MSBuild Projects="..\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.csproj" 
             Targets="Publish;PublishItemsOutputGroup" 
             Properties="Configuration=Release">
      <Output TaskParameter="TargetOutputs" 
              ItemName="_TasksProjectOutputs" />
    </MSBuild>
    <ItemGroup>
      <Content Include="@(_TasksProjectOutputs)" 
               Condition="'%(_TasksProjectOutputs.Extension)' == '.dll'" 
               Pack="true" 
               PackagePath="tasks\%(_TasksProjectOutputs.TargetPath)" 
               KeepMetadata="Pack;PackagePath" />
    </ItemGroup>
  </Target>

</Project>

注意

上述範例中的 <NoWarn>NU5100</NoWarn> 屬性會隱藏 tasks 資料夾中所放置組件的相關警告。 如需詳細資訊,請參閱 NuGet 警告 NU5100

新增 .targets 檔案以將 MSBuild 工作連線至建置管線。 在此檔案中,下列目標已完成:

  • 將工作匯入至建置流程。 請注意,DLL 的路徑會相對於套件中檔案的最終位置。
  • ComputeBlazorExtensionsDependsOn 屬性會將自訂目標附加至 Blazor WebAssembly 管線。
  • 擷取工作輸出上的 Extension 屬性,並將其新增至 BlazorPublishExtension 以告知 Blazor 有關擴充的資訊。 叫用目標中的工作會產生套件組合。 已發佈檔案的清單是由 PublishBlazorBootStaticWebAsset 項目群組中的 Blazor WebAssembly 管線所提供。 套件組合路徑是使用 IntermediateOutputPath 來定義 (通常是在 obj 資料夾內)。 最後,套件組合會自動複製到發佈輸出資料夾中的正確位置 (例如:bin\Release\net6.0\publish)。

參考套件時,套件會在發佈期間產生 Blazor 檔案的套件組合。

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/build/net6.0/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.targets

<Project>
  <UsingTask 
    TaskName="Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.BundleBlazorAssets" 
    AssemblyFile="$(MSBuildThisProjectFileDirectory)..\..\tasks\Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.Tasks.dll" />

  <PropertyGroup>
    <ComputeBlazorExtensionsDependsOn>
      $(ComputeBlazorExtensionsDependsOn);_BundleBlazorDlls
    </ComputeBlazorExtensionsDependsOn>
  </PropertyGroup>

  <Target Name="_BundleBlazorDlls">
    <BundleBlazorAssets
      PublishBlazorBootStaticWebAsset="@(PublishBlazorBootStaticWebAsset)"
      BundlePath="$(IntermediateOutputPath)bundle.multipart">
      <Output TaskParameter="Extension" 
              ItemName="BlazorPublishExtension"/>
    </BundleBlazorAssets>
  </Target>

</Project>

從套件組合自動啟動 Blazor

NuGet 套件會利用 JavaScript (JS) 初始設定式,從套件組合自動啟動 Blazor WebAssembly 應用程式,而不是使用個別 DLL 檔案。 JS 初始設定式可用來變更 Blazor開機資源載入器,並使用套件組合。

若要建立 JS 初始設定式,請將名稱為 {NAME}.lib.module.js 的 JS 檔案新增至 套件專案的 wwwroot 資料夾,其中 {NAME} 預留位置是套件識別碼。 例如,Microsoft 套件的檔案會命名為 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js。 匯出的函式 beforeWebAssemblyStartafterWebAssemblyStarted 會處理載入作業。

JS 初始設定式:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js

const resources = new Map();

export async function beforeWebAssemblyStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterWebAssemblyStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

若要建立 JS 初始設定式,請將名稱為 {NAME}.lib.module.js 的 JS 檔案新增至 套件專案的 wwwroot 資料夾,其中 {NAME} 預留位置是套件識別碼。 例如,Microsoft 套件的檔案會命名為 Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js。 匯出的函式 beforeStartafterStarted 會處理載入作業。

JS 初始設定式:

Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle/wwwroot/Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js

const resources = new Map();

export async function beforeStart(options, extensions) {
  if (!extensions || !extensions.multipart) {
    return;
  }

  try {
    const integrity = extensions.multipart['app.bundle'];
    const bundleResponse = 
      await fetch('app.bundle', { integrity: integrity, cache: 'no-cache' });
    const bundleFromData = await bundleResponse.formData();
    for (let value of bundleFromData.values()) {
      resources.set(value, URL.createObjectURL(value));
    }
    options.loadBootResource = function (type, name, defaultUri, integrity) {
      return resources.get(name) ?? null;
    }
  } catch (error) {
    console.log(error);
  }
}

export async function afterStarted(blazor) {
  for (const [_, url] of resources) {
    URL.revokeObjectURL(url);
  }
}

從主機伺服器應用程式提供套件組合

由於安全性限制,ASP.NET Core 預設不會提供 app.bundle 檔案。 用戶端要求檔案時,需要要求處理協助程式才能提供檔案。

注意

由於相同的最佳化會以透明方式套用至應用程式檔案套用的發佈擴充,因此會自動在發佈時產生 app.bundle.gzapp.bundle.br 壓縮的資產檔案。

在將後援檔案設定為 index.html (app.MapFallbackToFile("index.html");) 的行的正前方,將 C# 程式碼放在 Server 專案的 Program.cs 中,以回應套件組合檔案的要求 (例如 app.bundle):

app.MapGet("app.bundle", (HttpContext context) =>
{
    string? contentEncoding = null;
    var contentType = 
        "multipart/form-data; boundary=\"--0a7e8441d64b4bf89086b85e59523b7d\"";
    var fileName = "app.bundle";

    var acceptEncodings = context.Request.Headers.AcceptEncoding;

    if (Microsoft.Net.Http.Headers.StringWithQualityHeaderValue
        .StringWithQualityHeaderValue
        .TryParseList(acceptEncodings, out var encodings))
    {
        if (encodings.Any(e => e.Value == "br"))
        {
            contentEncoding = "br";
            fileName += ".br";
        }
        else if (encodings.Any(e => e.Value == "gzip"))
        {
            contentEncoding = "gzip";
            fileName += ".gz";
        }
    }

    if (contentEncoding != null)
    {
        context.Response.Headers.ContentEncoding = contentEncoding;
    }

    return Results.File(
        app.Environment.WebRootFileProvider.GetFileInfo(fileName)
            .CreateReadStream(), contentType);
});

內容類型會符合稍早在建置工作中定義的類型。 端點會檢查瀏覽器所接受的內容編碼,並提供最佳檔案:Brotli (.br) 或 Gzip (.gz)。