Como integrar Vue com ASP.NET Core usando extensão SPA
Tags: vue, dotnet, dotnetcore, spa
Published: 2019-11-02 13:37:03

<strong>AVISO</strong>
<small>Estou usando .NET Core 3.0 e Visual Studio 2019 v16.3.2, porém, acredito que quase tudo esteja disponível no .NET Core 2.x.</small>


## Introdução

Single Page Application (SPA) é uma tecnologia muito importante para desenvolvedor front-end.

No ASP.Net Core, existe a funcionalidade de integração com SPA.
Você pode vê-la ao criar um novo projeto do tipo ASP.NET Core Web Application.

![print02](https://thepracticaldev.s3.amazonaws.com/i/r19m6e3pzmlnosy3ckw0.JPG)

Os três templates no fundo são "Angular", "React.js" e "React.js e Redux", utilizados para desenvolver web APIs (usando ASP.NET Core) e SPAs (usando o framework selecionado) em um projeto, como demonstrado abaixo:

![print03](https://thepracticaldev.s3.amazonaws.com/i/4eulxo7kysbd130xtolp.JPG)

No Solution Explorer haverá uma chamada _ClientApp_ pertencente à aplicação SPA. É possível desenvolver usando Visual Studio ou qualquer editor de sua preferência, como o Visual Studio Code.

Se quiser debuggar a aplicação, pressione "F5". O Visual Studio irá executar o servidor de desenvolvimento para a SPA e para o ASP.NET Core, configurando a comunicação entre ambos.
Também serão executados comandos como "npm install" automaticamente.

![print04](https://thepracticaldev.s3.amazonaws.com/i/sz0n1hxal2hixbv71isa.JPG)

Parece perfeito, mas você deve estar pensando: "Onde está o Vue que eu gosto tanto?". 
É o que veremos a seguir.

## Criando um projeto ASP.NET Core Web Application

Vamos primeiro criar um projeto ASP.NET Core Web Application usando o template de API:

![print05](https://thepracticaldev.s3.amazonaws.com/i/0ff77refwz0unpf4czp9.JPG)

Dentro da pasta do projeto, abra um terminal e execute o comando abaixo para criar um projeto Vue usando o Vue-CLI:

```shell
vue create client-app
```

Caso não tenha instalado o Vue-CLI, acesse o link abaixo:

[https://cli.vuejs.org/guide/installation.html](https://cli.vuejs.org/guide/installation.html)

## Editando o arquivo do projeto para realizar a integração

Edite o arquivo _.csproj_ manualmente com o seguinte código:

```xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <RootNamespace>YOUR-PROJECT-NAME-HERE</RootNamespace>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
    <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
    <IsPackable>false</IsPackable>
    <SpaRoot>client-app\</SpaRoot>
    <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0-preview6.19307.2" />
  </ItemGroup>

  <ItemGroup>
    <!-- Don't publish the SPA source files, but do show them in the project files list -->
    <Content Remove="$(SpaRoot)**" />
    <None Remove="$(SpaRoot)**" />
    <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
  </ItemGroup>

  <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
      <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
  </Target>

  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
      <DistFiles Include="$(SpaRoot)dist\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

</Project>
```

Depois de editar o arquivo, o projeto em Vue será construído com o projeto em ASP.NET Core.

## Adicionado configuração de conexão

Último passo. Crie uma classe chamada _VueHelper_ para configurar a conexão entre o servidor de desenvolvimento e a aplicação em Vue:

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.SpaServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace NetCore3_Vue
{
    public static class VueHelper
    {
        // default port number of 'npm run serve'
        private static int Port { get; } = 8080;
        private static Uri DevelopmentServerEndpoint { get; } = new Uri($"http://localhost:{Port}");
        private static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(30);
        // done message of 'npm run serve' command.
        private static string DoneMessage { get; } = "DONE  Compiled successfully in";

        public static void UseVueDevelopmentServer(this ISpaBuilder spa)
        {
            spa.UseProxyToSpaDevelopmentServer(async () =>
            {
                var loggerFactory = spa.ApplicationBuilder.ApplicationServices.GetService<ILoggerFactory>();
                var logger = loggerFactory.CreateLogger("Vue");
                // if 'npm run serve' command was executed yourself, then just return the endpoint.
                if (IsRunning())
                {
                    return DevelopmentServerEndpoint;
                }

                // launch vue.js development server
                var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
                var processInfo = new ProcessStartInfo
                {
                    FileName = isWindows ? "cmd" : "npm",
                    Arguments = $"{(isWindows ? "/c npm " : "")}run serve",
                    WorkingDirectory = "client-app",
                    RedirectStandardError = true,
                    RedirectStandardInput = true,
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                };
                var process = Process.Start(processInfo);
                var tcs = new TaskCompletionSource<int>();
                _ = Task.Run(() =>
                {
                    try
                    {
                        string line;
                        while ((line = process.StandardOutput.ReadLine()) != null)
                        {
                            logger.LogInformation(line);
                            if (!tcs.Task.IsCompleted && line.Contains(DoneMessage))
                            {
                                tcs.SetResult(1);
                            }
                        }
                    }
                    catch (EndOfStreamException ex)
                    {
                        logger.LogError(ex.ToString());
                        tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex));
                    }
                });
                _ = Task.Run(() =>
                {
                    try
                    {
                        string line;
                        while ((line = process.StandardError.ReadLine()) != null)
                        {
                            logger.LogError(line);
                        }
                    }
                    catch (EndOfStreamException ex)
                    {
                        logger.LogError(ex.ToString());
                        tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex));
                    }
                });

                var timeout = Task.Delay(Timeout);
                if (await Task.WhenAny(timeout, tcs.Task) == timeout)
                {
                    throw new TimeoutException();
                }

                return DevelopmentServerEndpoint;
            });

        }

        private static bool IsRunning() => IPGlobalProperties.GetIPGlobalProperties()
                .GetActiveTcpListeners()
                .Select(x => x.Port)
                .Contains(Port);
    }
}

```

Adicione a função _AddSpaStaticFiles_ no método _ConfigureServices_ do arquivo _Startup.cs_ para ter suporte à SPA:

```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSpaStaticFiles(options => options.RootPath = "client-app/dist");
}
```

E adicione as funções _UseSpaStaticFiles_ e _UseSpa_ no método _Configure_:

```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Other code
    (...)

    // add following statements
    app.UseSpaStaticFiles();
    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "client-app";
        if (env.IsDevelopment())
        {
            // Launch development server for Vue.js
            spa.UseVueDevelopmentServer();
        }
    });
}
```

## Executando a aplicação

Abra a seção _Depurar_ na página de propriedades do projeto e remova o conteúdo do campo "Iniciar navegador":

![print06](https://thepracticaldev.s3.amazonaws.com/i/woasw27npgfg9e7pi0sw.JPG)

Pressione "F5" novamente para ver a página inicial do Vue.

![print07](https://thepracticaldev.s3.amazonaws.com/i/6p200ga00vd81mx12wuz.JPG)

É hora de se conectar com a API. Crie o arquivo _ValuesController.cs_ na pasta _Controllers_ contendo um simples método GET:


```csharp
using Microsoft.AspNetCore.Mvc;

namespace NetCore3_Vue.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(new string[] { "value1", "value2" });
        }
    }
}
```

Edite o arquivo _HelloWorld.vue_ dentro do projeto em Vue para exibir o resultado da requisição: 


```html
<template>
	<div>
		<div :key="r" v-for="r in this.results">{{ r }}</div>
	</div>
</template>

<script>
	export default {
		name: 'HelloWorld',
		data() {
			return {
				results: []
			};
		},
		async created() {
			const r = await fetch('/api/values');
			this.results = await r.json();
		}
	};
</script>
```

Execute a aplicação novamente e este será o resultado:

![print09](https://thepracticaldev.s3.amazonaws.com/i/kzt7maajz67qf22ene6n.JPG)

## Conclusão

Integramos um projeto web em ASP.NET Core com um projeto em Vue usando extensões SPA do ASP.NET Core.

Para que houvesse conexão entre ambos os projetos, foi necessário realizar alterações no arquivo de configuração do projeto em .Net Core, assim como, uma classe auxiliar foi criada para gerenciar a aplicação em Vue.

## Referências

Artigo original: [How to integrate Vue.js and ASP.NET Core using SPA Extension](https://techcommunity.microsoft.com/t5/Windows-Dev-AppConsult/How-to-integrate-Vue-js-and-ASP-NET-Core-using-SPA-Extension/ba-p/698648)

Projeto completo no GitHub: [https://github.com/lucianopereira86/NetCore3-Vue](https://github.com/lucianopereira86/NetCore3-Vue)