How to integrate Vue.js and ASP.NET Core using SPA Extension
Published Jun 17 2019 08:30 PM 63.7K Views
Microsoft
Spoiler
I'm using .NET Core 3.0 preview 6 and Visual Studio 2019 16.2.0 Preview 2.0.
However, I think almost all things are available in .NET Core 2.x.

Single Page Application(SPA) is really important technology for web apps developers.

In ASP.NET Core, there is SPA integration feature.

You can see it on create a new project wizard of ASP.NET Core Web application.

コメント 2019-06-18 095839.jpg

Three items at bottom are "Angular", "React.js" and "React.js and Redux."

Those are project template that can develop WebAPIs(using ASP.NET Core) and SPA(using selected framework) in one project, like below:

コメント 2019-06-18 100051.jpg

You can find a "ClientApp" folder in the Solution Explorer.

It is for SPA app. You can develop using Visual Studio, of cause also use your favorite editors like Visual Studio Code.

 

If you want debug the app, you can just do "Start Debugging."

Visual Studio will launch development server for SPA and development server for ASP.NET Core, and then add forwarding request settings from ASP.NET Core development server to SPA development server.

And also, "npm install" and other commands are executed automatically.

 

It looks perfect! But, I thought where is Vue.js? I like it.

So, I try to use the feature for Vue.js.

 

Create a ASP.NET Core Web Application project

Let's create a ASP.NET Core Web Application project using API template.

コメント 2019-06-18 101241.jpg

And go to the project folder, and then type following command to create Vue.js project using vue-cli.

vue create client-app

I selected TypeScript for development language. Because I like it. ;)

If you didn't install vue-cli, please check following document:

https://cli.vuejs.org/guide/installation.html

 

Edit the project file to build integration

Let's edit the project file like below:

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

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

After editing the file, the vue.js project will be built with the ASP.NET Core project.

 

Add port forwarding settings

Final step!! Adding to launch development server and adding a setting port forwarding, when start debugging.

I create a class for that.

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

Add a few statements to Startup.cs to support SPA. 

Add calling 'AddSpaStaticFiles' method at ConfigureServices method.

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

And add calling UseSpaStaticFiles and UseSpa methods at Configure method.

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...snip...
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });

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

Let's launch the app

Open Debug section of the project's property page, and edit "Launch browser" item to empty.

コメント 2019-06-18 113849.jpg

Press the 'F5' key. You will look a default page of vue.js page.

コメント 2019-06-18 114130.jpg

I edited HelloWorld.vue file to call a REST API that was created from the project template, like below:

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

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  public results?: string[];

  public async created() {
    const r = await fetch('/api/values');
    this.results = await r.json() as string[];
  }
}
</script>

It works fine.

コメント 2019-06-18 122237.jpg

Wrapping up

I integrated a ASP.NET Core WEB project and a Vue.js project using SPA Extensions of ASP.NET Core.

It becomes to easy to develop SPA using Vue.js with ASP.NET Core.

 

The code for this article is on GitHub: https://github.com/runceel/VueJsAndAspNetCore

28 Comments
Version history
Last update:
‎Jun 17 2019 08:30 PM
Updated by: