共用方式為


ASP.NET Core 託管 Blazor WebAssembly 應用程式的部署配置

備註

這不是本文的最新版本。 如需目前的版本,請參閱 本文的 .NET 9 版本

警告

此版本的 ASP.NET Core 已不再受支援。 如需詳細資訊,請參閱 .NET 和 .NET Core 支持原則。 如需目前的版本,請參閱 本文的 .NET 9 版本

這很重要

此資訊涉及一款尚未發行的產品,此產品在正式商業發布之前可能會經過大幅修改。 Microsoft 對於此處提供的資訊,不做任何明確或隱含的保證。

如需目前的版本,請參閱 本文的 .NET 9 版本

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

備註

本指引說明封鎖用戶端下載和執行 DLL 的環境。 在 .NET 8 或更新版本中,Blazor 會使用 Webcil 檔案格式解決此問題。 如需詳細資訊,請參閱 主機和部署 ASP.NET Core Blazor WebAssembly。 .NET 8 或更新版本中的應用程式不支援 Blazor 使用本文所述的實驗性 NuGet 套件進行多部分組合。 您可以使用本文中的指引,為 .NET 8 或更新版本建立自己的多部分組合 NuGet 套件。

Blazor WebAssembly 應用程式需要 動態連結庫 (DLL) 才能運作,但某些環境會封鎖用戶端下載和執行 DLL。 安全性產品通常能夠掃描周遊網路並封鎖或隔離 DLL 檔案的檔案內容。 本文說明在這些環境中啟用 Blazor WebAssembly 應用程式的一種方法,其中會從應用程式的 DLL 建立多部分配套檔案,以便一起下載 DLL,而略過安全性限制。

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

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

  • 允許自定義開機程式的 Blazor。
  • MSBuild 擴充性以轉換已發佈檔案的清單,並定義 Blazor 發佈延伸模組。 Blazor 發佈延伸模組是在發佈程式期間定義的檔案,可為執行已發佈 Blazor WebAssembly 應用程式所需的一組檔案提供替代表示法。 在本文中,會建立一個Blazor 發佈擴展,以產生一個多部分包,其中將所有應用程式的 DLL 封裝成單一檔案,以便一起下載這些 DLL。

本文所示範的方法可做為開發人員設計自己的策略和自定義載入程式的起點。

警告

對於規避安全性限制採取的任何方法,都必須仔細考慮其安全性影響。 建議您先與貴組織的網路安全專業人員進一步探討相關問題,再考慮採用本文中的方法。 要考慮的替代方案包括:

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

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

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

警告

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

本文稍後的Blazor WebAssembly一節及其三個子區段,提供套件中組態和程式代碼的詳細說明。 當您為應用程式建立自己的策略和自定義載入程式 Blazor WebAssembly 時,請務必瞭解詳細說明。 若要在不自定義的情況下,將已發佈、實驗性、不支援的 NuGet 套件用作本機示範,請執行下列步驟:

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

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

    備註

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

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

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

Blazor WebAssembly透過 NuGet 套件自定義載入過程

警告

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

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

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

  • 用於轉換發行檔案的 MSBuild 工作任務。
  • 具有 MSBuild 目標的 NuGet 套件,會連結至 Blazor 發佈程式、轉換輸出,並定義一或多個 Blazor 發行延伸模組檔案(在此案例中為單一套件組合)。
  • 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 檔案,就像它們套用到其他檔案一樣。

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有關延伸模組。 執行目標中的任務會生成套件。 已發佈檔案的清單是由 Blazor WebAssembly 專案群組中的 PublishBlazorBootStaticWebAsset 管線所提供。 套件組合路徑是使用 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初始化表達式,請將具有名稱JS的檔案新增{NAME}.lib.module.jswwwroot封裝項目的資料夾,其中{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初始化表達式,請將具有名稱JS的檔案新增{NAME}.lib.module.jswwwroot封裝項目的資料夾,其中{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 壓縮的資產檔案。

將 C# 程式代碼放在 Program.cs 專案中,緊接在設定後援檔案為 Serverindex.html 行之前,以回應對組合檔案的請求(例如,app.MapFallbackToFile("index.html");):

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)。