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


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

Примечание.

Это не последняя версия этой статьи. Актуальная версия — см. версию этой статьи для .NET 9.

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

Эта версия ASP.NET Core больше не поддерживается. Дополнительные сведения см. в политике поддержки .NET и .NET Core. Актуальная версия — см. версию этой статьи для .NET 9.

Это важно

Эта информация относится к предварительному выпуску продукта, который может быть существенно изменен до его коммерческого выпуска. Корпорация Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых в отношении информации, предоставленной здесь.

Актуальная версия — см. версию этой статьи для .NET 9.

В этой статье объясняется, как обеспечить возможность облачных 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 библиотеки приложения, упакованные в один файл, чтобы их можно было скачать вместе.

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

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

Любой подход, принятый для обхода ограничения безопасности, должен тщательно рассматриваться для его последствий безопасности. Перед принятием подхода в этой статье рекомендуется продолжить изучение темы с специалистами по безопасности сети в вашей организации. Альтернативы, которые следует рассмотреть, включают:

  • Включите устройства безопасности и программное обеспечение безопасности, чтобы разрешить сетевым клиентам загружать и использовать точные файлы, необходимые приложению Blazor WebAssembly .
  • Переключитесь с Blazor WebAssembly модели размещения на модель размещенияBlazor Server, которая поддерживает весь код приложения на сервере и не требует загрузки 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.org.

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

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

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

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

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

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

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

  • Задача MSBuild для преобразования файлов публикации.
  • Пакет NuGet с целями MSBuild, который включается в процесс публикации, преобразует выходные данные и определяет один или несколько файлов расширения публикации (в данном случае единственный пакет).
  • Инициализатор 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.jswwwroot в папку проекта пакета, где {NAME} заполнитель является идентификатором пакета. Например, файл для пакета Майкрософт называется 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.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# в проект Server в Program.cs непосредственно перед строкой, которая задает резервный файл в 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).