かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

ASP.NET Core プロジェクトに Vue.js を入れ込んでみよう

追記

以下の環境で試しています。

  • Visual Studio 2019 16.2.0 Preview 2.0
  • .NET Core 3.0 Preview 6
  • vue cli 3.8.4

本文

最近は SPA が盛んですがサーバー側と一緒にいい感じに開発してさくっとデプロイしたい!私の場合はサーバーサイドは C# で書くんですが、ASP.NET Core の Web アプリケーションのテンプレートには Angular と React しか対応してないので、Vue.js を使いたい自分にとっては悲しい感じです。

f:id:okazuki:20190617112852p:plain

Vue.js でやってみよう

特に仕組み的には React や Angular にしか対応していないものではないと思うのでやってみようと思います。ASP.NET Core の Web アプリケーションを新規作成して、普通の MVC のアプリを作ります。.NET Core 3.0 Preview 6 ベースでやるので正式版のころには何か変わってるかもしれません。2.x 系でもある機能だから 2.x 系も似たような感じで行けると信じたい。

プロジェクトを作ったらプロジェクトのフォルダーで vue コマンドをうってプロジェクトを作りましょう。(vue コマンドがない人は vue-cli でぐぐってセットアップ!)

vue create hello-world

プロジェクトファイルを開いて hello-world フォルダー以下のファイルがデプロイされないように設定します。

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

  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="hello-world\**" />
    <Content Remove="hello-world\**" />
    <None Include="hello-world\**" Exclude="hello-world\node_modules\**" />
  </ItemGroup>
</Project>

node_modules フォルダーが無い時に npm install するような定義も追加しておきます。

  <Target Name="ExecNpmInstall" BeforeTargets="Build" Condition="'$(Configuration)' == 'Debug' And !Exists('hello-world\node_modules')">
    <Exec WorkingDirectory="hello-world\" 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="hello-world" Command="npm install" />
  <Exec WorkingDirectory="hello-world" Command="npm run build" />

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

これでプロジェクトファイルの下準備は完了かな。

SPA 拡張機能の適用

さて、次!Microsoft.AspNetCore.SpaServices.Extensions を NuGet で入れます。 .NET Core 3.0 用なのでプレビューパッケージを入れました。

まず、ConfigureServices メソッドに AddSpaStaticFiles を追加します。SPA の出力先のフォルダーを指定するので今回の場合は hello-world/dist になります。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
    });


    services.AddControllersWithViews();
    services.AddRazorPages();
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = @"hello-world/dist";
    });
}

そして Configure には UseSpaStaticFiles メソッドと UseSpa メソッドを追加します。UseSpa メソッドには、開発環境では開発用サーバーを立ち上げて完了待ちをして開発サーバーの URL を ASP.NET Core 側に教えてあげる必要があります。

あとデフォルトのコントローラ名が Home になってると index.html ではなく ASP.NET Core MVC の Home/Index に行ってしまうので UseEndpoints で controller のデフォルトの Home を消してます。

ちょっと長いな…

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        // 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.UseStaticFiles();
    app.UseSpaStaticFiles(); // これを追加!!

    app.UseCookiePolicy();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller}/{action=Index}/{id?}");
        endpoints.MapRazorPages();
    });

    // これを追加!!
    app.UseSpa(spa =>
    {
        spa.Options.SourcePath = "hello-world";
        if (env.IsDevelopment())
        {
            // 開発環境では npm run serve をして 8080 ポートへ自動的にリダイレクトしてくれるようにする。
            spa.UseProxyToSpaDevelopmentServer(async () =>
            {
                var pi = new ProcessStartInfo
                {
                    FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd" : "npm",
                    Arguments = $"{(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "/c npm " : "")}run serve",
                    WorkingDirectory = "hello-world",
                    RedirectStandardError = true,
                    RedirectStandardInput = true,
                    RedirectStandardOutput = true,
                    UseShellExecute = false,
                };
                var p = Process.Start(pi);
                var lf = app.ApplicationServices.GetService<ILoggerFactory>();
                var logger = lf.CreateLogger("npm");
                var tcs = new TaskCompletionSource<int>();
                _ = Task.Run(() =>
                {
                    var line = "";
                    while ((line = p.StandardOutput.ReadLine()) != null)
                    {
                        if (line.Contains("DONE  Compiled successfully in ")) // 開発用サーバーの起動待ち
                        {
                            tcs.SetResult(0);
                        }

                        logger.LogInformation(line);
                    }
                });
                _ = Task.Run(() =>
                {
                    var line = "";
                    while ((line = p.StandardError.ReadLine()) != null)
                    {
                        logger.LogError(line);
                    }
                });
                await Task.WhenAny(Task.Delay(20000), tcs.Task);
                return new Uri("http://localhost:8080");
            });
        }
    });
}

ここまでやったら F5 を押してデバッグ実行してみましょう。開発サーバーが立ち上がるまでのタイムアウトを20秒で仮置きしてますが、もうちょっと長くしたり、エラーメッセージのときは失敗したことがわかるような処理も入れた方が実際にもいい気がします。

f:id:okazuki:20190617130734p:plain

動いた!!ためしに App.vue を以下のように書き換えて…

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="こんにちは!!こんにちは!! ASP.NET Core + Vue.js だよ!"/>
  </div>
</template>

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

@Component({
  components: {
    HelloWorld,
  },
})
export default class App extends Vue {}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

実行してみました。

f:id:okazuki:20190617131414p:plain

ばっちりですね。

Deploy to Azure web apps

開発できたのでデプロイしてみます。Visual Studio から直接 Azure にデプロイできるので試してみました。現時点ではデプロイ時に自己完結型でデプロイしないといけない(or Web app に .NET Core 3.0 の拡張を入れないといけない)ので注意。

ということで Azure にデプロイしても動いた。

f:id:okazuki:20190617132716p:plain

まとめ

ASP.NET Core で API 作って Vue.js で画面作ってまとめてデプロイも出来そうです。