Vue.jsおよび.NET Core 3の開発

# 概要

.NET Core 3.0 の Webアプリケーション(API テンプレート)で Vue.jsをフロントエンドにして開発を行う方法。 React や Angular のテンプレートはすでに公式から出ています。(Vue.js もコマンドラインからの作成ならできたはず)

# 前提条件


- vue-cli ( npm install -g @vue/cli )
- .NET Core 3.0 (https://dotnet.microsoft.com/download/dotnet-core/3.0)

# 手順

ASP.NET Core Web アプリケーションを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/91224218-0482-a00c-076d-086da28802d3.png)

テンプレートは API を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/40f5c487-7a67-a566-91b7-d13ad8ca7d1a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/39388f58-88df-b066-08db-be61a0ccc803.png)

プロジェクトフォルダに移動し、vue-cli を利用してVuejsのプロジェクトを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/b69ebb56-673e-69e3-0a9f-aa4d26f5f72a.png)

```
vue create client-app   
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/14465462-4067-5dd0-3f7e-aaefefd69bd9.png)
このようになります。

nuget から Microsoft.AspNetCore.SpaServices.Extensions を追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/40f24197-e03b-3ea0-052b-1e40dd27f9b9.png)

プロジェクトファイルを以下のように編集します。

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

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


  <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">

    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" />


    <ItemGroup>
      <DistFiles Include="$(SpaRoot)dist\**" />
      <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
        <RelativePath>%(DistFiles.Identity)</RelativePath>
        <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
      </ResolvedFileToPublish>
    </ItemGroup>
  </Target>

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

    <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') ">

    <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>
</Project>

```

プロジェクトに VueCoreConnections を言う名前でフォルダを作成し、その下に Connection.cs を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/247e9e4c-2b4d-1236-700f-b7f69fc62d23.png)

```csharp:Connection.cs
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 WebApplication1.VueCoreConnections
{
    public static class Connection
    {

        private static int Port { get; } = 8080;
        private static Uri DevelopmentServerEndpoint { get; } = new Uri($"http://localhost:{Port}");
        private static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(60);

        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 (IsRunning())
                {
                    return DevelopmentServerEndpoint;
                }


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

```

Startup.cs を編集します。

```csharp:Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using WebApplication1.VueCoreConnections;

namespace WebApplication1
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.  
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            // connect vue app - middleware  
            services.AddSpaStaticFiles(options => options.RootPath = "client-app/dist");
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.  
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.  
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });

            // use middleware and launch server for Vue  
            app.UseSpaStaticFiles();
            app.UseSpa(spa =>
            {
                spa.Options.SourcePath = "client-app";
                if (env.IsDevelopment())
                {
                    spa.UseVueDevelopmentServer();
                }
            });
        }
    }
}


```


以上です。 F5 でデバック実行しましょう。初回のみ npm install が実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/20ada0a2-1258-66fa-d929-b0cb5f296184.png)

## ヒント
起動時に 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/ad7fea9d-476a-9eb5-23eb-5e5755a7ea06.png)

こちらに遷移してしまう場合は プロジェクトのデバック設定を確認してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183354/b64d1e26-5bae-f292-17ea-d23595d02644.png)


# 参考

- https://www.c-sharpcorner.com/article/getting-started-with-vue-js-and-net-core-32/
- https://blog.okazuki.jp/entry/2019/06/17/132755