Макет развертывания для размещенных Blazor WebAssembly приложений ASP.NET Core

В этой статье объясняется, как включить размещенные Blazor WebAssembly развертывания в средах, которые блокируют загрузку и выполнение файлов библиотеки динамической компоновки (DLL).

Примечание.

В этом руководстве рассматриваются среды, которые блокируют загрузку и выполнение БИБЛИОТЕК DLL клиентами. В .NET 8 или более поздней версии Blazor используется формат файла Webcil для решения этой проблемы. Дополнительные сведения см. в статье Размещение и развертывание ASP.NET Core Blazor WebAssembly. Многопартийное объединение с помощью экспериментального пакета NuGet, описанного в этой статье, не поддерживается для Blazor приложений в .NET 8 или более поздней версии. Дополнительные сведения см. в статье "Улучшение пакета Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle", чтобы определить настраиваемый формат пакета (dotnet/aspnetcore #36978). Инструкции из этой статьи можно использовать для создания собственного пакета NuGet для .NET 8 или более поздней версии.

Для работы приложений 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# в частном порядке без необходимости использования приложений веб-API для обеспечения конфиденциальности кода на C# в приложениях Blazor WebAssembly.

Экспериментальный пакет NuGet и пример приложения

Подход, описанный в этой статье, используется экспериментальнымMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle пакетом (NuGet.org) для приложений, предназначенных для .NET 6 или более поздней версии. Пакет содержит целевые объекты MSBuild для настройки выходных данных публикации Blazor и инициализатор JavaScript для использования настраиваемого загрузчика ресурсов загрузки, каждый из которых подробно описан далее в этой статье.

Экспериментальный код (включает источник ссылки на пакет NuGet и CustomPackagedApp пример приложения)

Предупреждение

Экспериментальные и предварительные функции предоставляются для сбора отзывов и не поддерживаются для использования в рабочей среде.

Далее в этой статье в разделе Настройка процесса загрузки Blazor WebAssembly с помощью пакета NuGet с тремя подразделами представлены подробные объяснения конфигурации и кода в пакете Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Подробные объяснения важны для понимания того, как вы создаете собственную стратегию и пользовательский процесс загрузки для приложений Blazor WebAssembly. Чтобы использовать опубликованный, экспериментальный, неподдерживаемый пакет NuGet без настройки в качестве локальной демонстрации, выполните следующие действия.

  1. Используйте существующее размещенное Blazor WebAssemblyрешение или создайте новое решение из шаблона проекта Blazor WebAssembly с помощью Visual Studio или путем передачи параметра -ho|--hosted в команду dotnet new (dotnet new blazorwasm -ho). Дополнительные сведения см. в статье Инструментарий для ASP.NET Core Blazor.

  2. В проекте Client добавьте ссылку на экспериментальный пакет Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle:

    Примечание.

    Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

  3. В проекте Server добавьте конечную точку для обслуживания файла пакета (app.bundle). Пример кода представлен в разделе Обслуживание пакета из приложения сервера узла этой статьи.

  4. Опубликуйте приложение в конфигурации выпуска.

Настройка процесса загрузки Blazor WebAssembly с помощью пакета NuGet

Предупреждение

Рекомендации в этом разделе с тремя подразделами относятся к созданию пакета NuGet с нуля, чтобы реализовать собственную стратегию и настраиваемый процесс загрузки. Экспериментальный Microsoft.AspNetCore.Components.WebAssembly.MultipartBundleпакет (NuGet.org) для .NET 6 и 7 основан на рекомендациях в этом разделе. При использовании предоставленного пакета в локальной демонстрации подхода с загрузкой многоэлементного пакета вам не нужно следовать указаниям в этом разделе. Рекомендации по использованию предоставленного пакета см. в разделе Экспериментальный пакет NuGet и пример приложения.

Ресурсы приложения Blazor упаковываются в файл многоэлементного пакета и загружаются браузером с помощью настраиваемого инициализатора JavaScript (JS). Для приложения, использующего пакет с инициализатором JS, приложение требует, чтобы файл пакета был обслужен по запросу. Все остальные аспекты этого подхода обрабатываются прозрачно.

Для загрузки опубликованного по умолчанию приложения Blazor необходимо выполнить четыре настройки:

  • Задача MSBuild для преобразования файлов публикации.
  • Пакет NuGet с целевыми объектами MSBuild, который подключается к процессу публикации Blazor, преобразует выходные данные и определяет один или несколько файлов расширения публикации Blazor (в данном случае один пакет).
  • Инициализатор JS для обновления обратного вызова загрузчика ресурсов Blazor WebAssembly для загрузки пакета и предоставления приложению отдельных файлов.
  • Вспомогательное приложение в ведущем приложении Server, чтобы обеспечить обслуживание пакета для клиентов по запросу.

Создание задачи MSBuild для настройки списка опубликованных файлов и определения новых расширений

Создайте MSBuild задачу в виде открытого класса C#, который можно импортировать в составе компиляции MSBuild и который может взаимодействовать со сборкой.

Для класса C# необходимо следующее:

Примечание.

Пакет NuGet для примеров в этой статье назван в соответствии с пакетом, предоставленным корпорацией Майкрософт, 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>

Определите последние версии пакетов для заполнителей {VERSION} на сайте NuGet.org:

Чтобы создать задачу 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. Каждый элемент расширения содержит три части данных:
    • Путь к файлу расширения.
    • Путь URL-адреса относительно корня приложения Blazor WebAssembly.
    • Имя расширения, которое группирует файлы, созданные данным расширением.

После выполнения предыдущих задач создается задача 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 для автоматического преобразования выходных данных публикации

Создайте пакет NuGet с целевыми объектами MSBuild, которые автоматически включаются при ссылке на пакет:

  • Создайте новый проект библиотеки классов Razor (RCL).
  • Создайте файл целевых объектов, следуя соглашениям NuGet, для автоматического импорта пакета в проекты. Например, создайте build\net6.0\{PACKAGE ID}.targets, где {PACKAGE ID} — это идентификатор пакета.
  • Соберите выходные данные из библиотеки классов, содержащей задачу MSBuild, и убедитесь, что выходные данные упакованы в правильном расположении.
  • Добавьте необходимый код MSBuild для присоединения к конвейеру Blazor и вызова задачи MSBuild для создания пакета.

В описанном в этом разделе подходе используется только пакет для доставки целевых объектов и содержимого, который отличается от большинства пакетов, в которых пакет включает библиотеку DLL.

Предупреждение

Пример пакета, описанный в этом разделе, демонстрирует настройку процесса публикации Blazor. Пример пакета NuGet предназначен только для использования в качестве локальной демонстрации. Использование этого пакета в рабочей среде не поддерживается.

Примечание.

Пакет NuGet для примеров в этой статье назван в соответствии с пакетом, предоставленным корпорацией Майкрософт, 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.js в папку wwwroot проекта пакета, где заполнитель {NAME} обозначает идентификатор пакета. Например, файл для пакета Microsoft называется Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Обработку загрузки выполняют экспортированные функции beforeWebAssemblyStart и afterWebAssemblyStarted.

Инициализаторы 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.js в папку wwwroot проекта пакета, где заполнитель {NAME} обозначает идентификатор пакета. Например, файл для пакета Microsoft называется Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. Обработку загрузки выполняют экспортированные функции beforeStart и afterStarted.

Инициализаторы 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.gz и app.bundle.br.

Поместите код C# в Program.cs проекта Server непосредственно перед строкой, которая задает index.html (app.MapFallbackToFile("index.html");) в качестве резервного файла, чтобы предоставить ответ на запрос файла пакета (например, 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).