Como integrar Vue com ASP.NET Core usando extensão SPA

lucianopereira86

LUCIANO DE SOUSA PEREIRA

Posted on November 2, 2019

Como integrar Vue com ASP.NET Core usando extensão SPA

AVISO
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.

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

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

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

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

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

vue create client-app

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

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:

<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:

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:

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:

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

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

print07

É hora de se conectar com a API. Crie o arquivo ValuesController.cs na pasta Controllers contendo um simples método GET:

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:

<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

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

Projeto completo no GitHub: https://github.com/lucianopereira86/NetCore3-Vue

💖 💪 🙅 🚩
lucianopereira86
LUCIANO DE SOUSA PEREIRA

Posted on November 2, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related