Поделиться через


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

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

Примечание.

В этом руководстве рассматриваются среды, которые блокируют загрузку и выполнение БИБЛИОТЕК DLL клиентами. В .NET 8 или более поздней версии Blazor используется формат файла Webcil для решения этой проблемы. Дополнительные сведения см. в статье Размещение и развертывание ASP.NET Core Blazor WebAssembly. Многопартийное объединение с помощью экспериментального пакета NuGet, описанного в этой статье, не поддерживается для Blazor приложений в .NET 8 или более поздней версии. Инструкции из этой статьи можно использовать для создания собственного пакета 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 для кода privacy 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).