Partilhar via


Layout de implantação para aplicações hospedadas do ASP.NET Core Blazor WebAssembly

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 10 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e do .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Este artigo explica como habilitar implantações hospedadas Blazor WebAssembly em ambientes que bloqueiam o download e a execução de arquivos de biblioteca de vínculo dinâmico (DLL).

Observação

Esta orientação aborda ambientes que impedem que os clientes baixem e executem DLLs. No .NET 8 ou posterior, Blazor usa o formato de arquivo Webcil para resolver esse problema. Para obter mais informações, consulte Hospedar e implantar ASP.NET Core Blazor WebAssembly. O agrupamento de várias partes usando o pacote NuGet experimental descrito por este artigo não é suportado para Blazor aplicativos no .NET 8 ou posterior. Você pode usar as diretrizes neste artigo para criar seu próprio pacote NuGet de agrupamento de várias partes para .NET 8 ou posterior.

Blazor WebAssembly os aplicativos exigem bibliotecas de vínculo dinâmico (DLLs) para funcionar, mas alguns ambientes impedem que os clientes baixem e executem DLLs. Os produtos de segurança geralmente são capazes de verificar o conteúdo de arquivos que atravessam a rede e bloquear ou colocar em quarentena arquivos DLL. Este artigo descreve uma abordagem para habilitar Blazor WebAssembly aplicativos nesses ambientes, onde um arquivo de pacote de várias partes é criado a partir das DLLs do aplicativo para que as DLLs possam ser baixadas juntas ignorando as restrições de segurança.

Blazor WebAssembly os aplicativos exigem bibliotecas de vínculo dinâmico (DLLs) para funcionar, mas alguns ambientes impedem que os clientes baixem e executem DLLs. Em um subconjunto desses ambientes, alterar a extensão de nome de arquivo de arquivos DLL (.dll) é suficiente para ignorar as restrições de segurança, mas os produtos de segurança geralmente são capazes de verificar o conteúdo de arquivos que atravessam a rede e bloquear ou colocar em quarentena arquivos DLL. Este artigo descreve uma abordagem para habilitar Blazor WebAssembly aplicativos nesses ambientes, onde um arquivo de pacote de várias partes é criado a partir das DLLs do aplicativo para que as DLLs possam ser baixadas juntas ignorando as restrições de segurança.

Uma aplicação hospedada Blazor WebAssembly pode personalizar os seus ficheiros publicados e o empacotamento das DLLs da aplicação usando as seguintes funcionalidades:

  • Inicializadores JavaScript que permitem personalizar o Blazor processo de inicialização.
  • Extensibilidade do MSBuild para transformar a lista de arquivos publicados e definir Blazor extensões de publicação. Blazor Extensões de publicação são arquivos definidos durante o processo de publicação que fornecem uma representação alternativa para o conjunto de arquivos necessários para executar um aplicativo publicado Blazor WebAssembly . Neste artigo, é criada uma Blazor extensão de publicação que produz um pacote de várias partes com todas as DLLs do aplicativo empacotadas em um único arquivo para que as DLLs possam ser baixadas juntas.

A abordagem demonstrada neste artigo serve como um ponto de partida para os desenvolvedores desenvolverem suas próprias estratégias e processos de carregamento personalizados.

Advertência

Qualquer abordagem adotada para contornar uma restrição de segurança deve ser cuidadosamente considerada pelas suas implicações em termos de segurança. Recomendamos explorar mais o assunto com os profissionais de segurança de rede da sua organização antes de adotar a abordagem neste artigo. As alternativas a considerar incluem:

  • Habilite dispositivos de segurança e software de segurança para permitir que os clientes de rede baixem e usem os arquivos exatos exigidos por um Blazor WebAssembly aplicativo.
  • Mude do modelo de Blazor WebAssembly hospedagem para o Blazor Server modelo de hospedagem, que mantém todo o código C# do aplicativo no servidor e não requer o download de DLLs para clientes. Blazor Server também oferece a vantagem de manter o código C# privado sem exigir o uso de aplicativos de API da Web para privacidade de código C# com Blazor WebAssembly aplicativos.

Pacote NuGet experimental e aplicativo de exemplo

A abordagem descrita neste artigo é usada pelo pacote experimentalMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para aplicativos destinados ao .NET 6 ou posterior. O pacote contém destinos MSBuild para personalizar a Blazor saída de publicação e um inicializador JavaScript para usar um carregador de recursos de inicialização personalizado, cada um dos quais são descritos em detalhes mais adiante neste artigo.

Advertência

Os recursos experimentais e de visualização são fornecidos com a finalidade de coletar feedback e não são suportados para uso em produção.

Mais adiante neste artigo, a seção Personalizar o Blazor WebAssembly processo de carregamento por meio de um pacote NuGet com suas três subseções fornece explicações detalhadas sobre a configuração e o código no Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle pacote. As explicações detalhadas são importantes para entender quando você cria sua própria estratégia e processo de carregamento personalizado para Blazor WebAssembly aplicativos. Para usar o pacote NuGet publicado, experimental e sem suporte sem personalização como uma demonstração local, execute as seguintes etapas:

  1. Use uma solução hospedada existente Blazor WebAssembly ou crie uma nova solução a partir do modelo de projeto usando o Visual Studio ou passando a opção Blazor WebAssembly para o comando -ho|--hosted (). Para obter mais informações, consulte Ferramentas para ASP.NET Core Blazor.

  2. No projeto Client, adicione o pacote experimental Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.

    Observação

    Para obter orientação sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes em Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas do pacote em NuGet.org.

  3. No projeto Server, adicione um ponto de extremidade para disponibilizar o ficheiro do pacote (app.bundle). Exemplo de código pode ser encontrado na seção Servir o pacote do aplicativo de servidor host deste artigo.

  4. Publique a aplicação na configuração Release.

Personalize o Blazor WebAssembly processo de carregamento por meio de um pacote NuGet

Advertência

A orientação nesta seção com suas três subseções diz respeito à criação de um pacote NuGet do zero para implementar sua própria estratégia e processo de carregamento personalizado. O pacote experimentalMicrosoft.AspNetCore.Components.WebAssembly.MultipartBundle (NuGet.org) para .NET 6 e 7 baseia-se nas orientações desta seção. Ao usar o pacote fornecido em uma demonstração local da abordagem de download de pacote com várias partes, você não precisa seguir as orientações nesta seção. Para obter orientação sobre como usar o pacote fornecido, consulte a seção Pacote NuGet experimental e aplicativo de exemplo .

Blazor os recursos do aplicativo são empacotados em um arquivo de pacote de várias partes e carregados pelo navegador por meio de um inicializador JavaScript (JS) personalizado. Para um aplicativo que consome o pacote com o JS inicializador, o aplicativo só requer que o arquivo de pacote seja servido quando solicitado. Todos os outros aspetos desta abordagem são tratados de forma transparente.

Quatro personalizações são necessárias para como um aplicativo publicado Blazor padrão é carregado:

  • Uma tarefa do MSBuild para transformar os arquivos de publicação.
  • Um pacote NuGet com alvos do MSBuild que se integra no processo de publicação Blazor, transforma a saída Blazor e define um ou mais ficheiros de extensão de publicação (neste caso, um único pacote).
  • Um JS inicializador para atualizar o retorno de chamada do Blazor WebAssembly carregador de recursos para que ele carregue o pacote e forneça ao aplicativo os arquivos individuais.
  • Um auxiliar no aplicativo host Server para garantir que o pacote seja servido aos clientes mediante solicitação.

Criar uma tarefa do MSBuild para personalizar a lista de arquivos publicados e definir novas extensões

Crie uma tarefa MSBuild como uma classe C# pública que pode ser importada como parte de uma compilação MSBuild e que pode interagir com a compilação.

O seguinte é necessário para a classe C#:

Observação

O pacote NuGet para os exemplos neste artigo tem o nome do pacote fornecido pela Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obter orientação sobre como nomear e produzir seu próprio pacote NuGet, consulte os seguintes artigos do 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>

Determine as versões mais recentes dos pacotes para os {VERSION} espaços reservados no NuGet.org:

Para criar a tarefa MSBuild, crie uma classe C# pública estendendo Microsoft.Build.Utilities.Task (não System.Threading.Tasks.Task) e declare três propriedades:

  • PublishBlazorBootStaticWebAsset: A lista de arquivos a serem publicados para o Blazor aplicativo.
  • BundlePath: O caminho onde o pacote é escrito.
  • Extension: As novas extensões de publicação a serem incluídas na compilação.

A classe de exemplo BundleBlazorAssets a seguir é um ponto de partida para personalização adicional:

  • Execute No método, o pacote é criado a partir dos três tipos de arquivo a seguir:
    • Arquivos JavaScript (dotnet.js)
    • Arquivos WebAssembly (Wasm) (dotnet.wasm)
    • DLLs de aplicação (.dll)
  • Um multipart/form-data pacote é criado. Cada arquivo é adicionado ao pacote com suas respetivas descrições por meio do cabeçalho Content-Disposition e do cabeçalho Content-Type.
  • Depois que o pacote é criado, o pacote é gravado em um arquivo.
  • A compilação está configurada para a extensão. O código a seguir cria um item de extensão e o adiciona à Extension propriedade. Cada item de extensão contém três partes de dados:
    • O caminho para o arquivo de extensão.
    • O caminho da URL relativo à raiz da Blazor WebAssembly aplicação.
    • O nome da extensão, que agrupa os arquivos produzidos por uma determinada extensão.

Depois de atingir as metas anteriores, a tarefa MSBuild é criada para personalizar a saída de publicação de Blazor. Blazor Cuida de reunir as extensões e garantir que elas sejam copiadas para o local correto na pasta de saída de publicação (por exemplo, bin\Release\net6.0\publish). As mesmas otimizações (por exemplo, compactação) são aplicadas aos ficheiros JavaScript, Wasm e DLL que Blazor aplica a outros ficheiros.

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;
        }
    }
}

Crie um pacote NuGet para transformar automaticamente a saída de publicação

Gere um pacote NuGet com destinos MSBuild que são incluídos automaticamente quando o pacote é referenciado:

  • Crie um novo Razor projeto de biblioteca de classes (RCL).
  • Crie um arquivo de destinos seguindo as convenções do NuGet para importar automaticamente o pacote em projetos de consumo. Por exemplo, crie build\net6.0\{PACKAGE ID}.targets, onde {PACKAGE ID} é o identificador do pacote.
  • Recolha a saída da biblioteca de classes que contém a tarefa MSBuild e confirme se a saída está empacotada no local correto.
  • Adicione o código MSBuild necessário para anexar ao Blazor pipeline e invoque a tarefa MSBuild para gerar o pacote.

A abordagem descrita nesta seção usa apenas o pacote para fornecer destinos e conteúdo, o que é diferente da maioria dos pacotes em que o pacote inclui uma DLL de biblioteca.

Advertência

O pacote de exemplo descrito nesta seção demonstra como personalizar o processo de Blazor publicação. O pacote NuGet de exemplo é para uso apenas como uma demonstração local. Não há suporte para o uso deste pacote em produção.

Observação

O pacote NuGet para os exemplos neste artigo tem o nome do pacote fornecido pela Microsoft, Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle. Para obter orientação sobre como nomear e produzir seu próprio pacote NuGet, consulte os seguintes artigos do 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>

Observação

A <NoWarn>NU5100</NoWarn> propriedade no exemplo anterior suprime o aviso sobre os assemblies colocados na tasks pasta. Para obter mais informações, consulte NuGet Warning NU5100.

Adicione um .targets arquivo para conectar a tarefa MSBuild ao pipeline de compilação. Neste arquivo, os seguintes objetivos são alcançados:

  • Importe a tarefa para o processo de compilação. Observe que o caminho para a DLL é relativo ao local final do arquivo no pacote.
  • A propriedade ComputeBlazorExtensionsDependsOn anexa o destino personalizado ao pipeline Blazor WebAssembly.
  • Captura a propriedade na saída da tarefa e adiciona-a ao Extension para informar BlazorPublishExtension sobre a extensão. Invocar a tarefa no destino produz o conjunto. A lista de arquivos publicados é fornecida pelo Blazor WebAssembly pipeline no PublishBlazorBootStaticWebAsset grupo de itens. O caminho do pacote é definido usando o IntermediateOutputPath (normalmente dentro da obj pasta). Por fim, o pacote é copiado automaticamente para o local correto na pasta de saída de publicação (por exemplo, bin\Release\net6.0\publish).

Quando o pacote é referenciado, este gera um conjunto dos arquivos Blazor durante a publicação.

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>

Inicializar Blazor automaticamente a partir do pacote

O pacote NuGet aproveita os inicializadores JavaScript (JS) para inicializar automaticamente um Blazor WebAssembly aplicativo do pacote em vez de usar arquivos DLL individuais. JSOs inicializadores são usados para alterar o Blazorcarregador de recursos de inicialização e utilizar o pacote.

Para criar um JS inicializador, adicione um JS arquivo com o nome {NAME}.lib.module.js à wwwroot pasta do projeto de pacote, onde o espaço reservado {NAME} é o identificador do pacote. Por exemplo, o arquivo para o pacote da Microsoft é chamado Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. As funções beforeWebAssemblyStart exportadas e afterWebAssemblyStarted manipulam o carregamento.

Os JS inicializadores:

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);
  }
}

Para criar um JS inicializador, adicione um JS arquivo com o nome {NAME}.lib.module.js à wwwroot pasta do projeto de pacote, onde o espaço reservado {NAME} é o identificador do pacote. Por exemplo, o arquivo para o pacote da Microsoft é chamado Microsoft.AspNetCore.Components.WebAssembly.MultipartBundle.lib.module.js. As funções beforeStart exportadas e afterStarted manipulam o carregamento.

Os JS inicializadores:

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);
  }
}

Servir o pacote a partir do aplicativo do servidor host

Devido a restrições de segurança, o ASP.NET Core não serve o app.bundle arquivo. Um auxiliar de processamento de solicitação é necessário para servir o arquivo quando ele é solicitado pelos clientes.

Observação

Como as mesmas otimizações são aplicadas de forma transparente às Extensões de Publicação tal como aos arquivos do aplicativo, os arquivos de ativos compactados app.bundle.gz e app.bundle.br são produzidos automaticamente ao publicar.

Coloque o código C# no Program.cs do projeto Server imediatamente antes da linha que define o arquivo de fallback como index.html (app.MapFallbackToFile("index.html");) para responder a uma solicitação para o arquivo de pacote (por exemplo, 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);
});

O tipo de conteúdo corresponde ao tipo definido anteriormente na tarefa de compilação. O endpoint verifica as codificações de conteúdo aceitas pelo navegador e serve o arquivo ideal, Brotli (.br) ou Gzip (.gz).