vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

ASP.NET Core + TypeScript ってみる 1

はじめに

今回からちょっと間(挫折するか落ち着くまで) フロントエンドに挑戦してみます。

今のトレンドで言えば、以前触れた Angular のようなフレームワークを駆使して SPA ! 。。。って時代ももう落ち着いてきた?というところですが、今回はあえてフレームワークは使わず、 TypeScript + HTML + CSS(こっちはもう少し何か試すかも) で試してみたいと思います。

理由は使う先で Node.js が使えるとは限らないことと、(プライベートは良いのですが)頻繁にアップデートできるとは限らないことです。

まぁ後者については、 TypeScript 使っていいのか?という気がしないでもないですが。

準備

とにかく準備を進めていきますよ。

サーバーアプリは ASP.NET Core MVCこのへん で使っていたプロジェクトを再利用しています。

Node.js

で、まず Node.js なのですが、 Windows だと nodist などのバージョン管理マネージャーを使うのが良さそうですね?

ただ、何も考えず Node.js をそのままインストールしてしまったので、今回はこれでいきます。

TypeScript

Rider の Terminal か PowerShell などでプロジェクト直下に移動し、 npm で TypeScript 、 tsc をインストールします(先に空で package.json 作っておいた方が良い?)。

npm install --save-dev typescript tsc

package.json の中身はこうなりました。

package.json

{
  "devDependencies": {
    "tsc": "^1.20150623.0",
    "typescript": "^3.4.2"
  }
}

最初グローバルにインストールしていたのですが、下記などを見るとローカルインストールの方が良さそうに思えます。

tsc --init

tsc を使って、 tsconfig.json を生成します。

npx tsc --init

今回はローカルインストールしているため、 npx tsc ~ のように実行します。

これでデフォルトの tsconfig.json が生成されます。

tsconfig.json

{
  "compilerOptions": {
    /* Basic Options */
    "target": "es5",                          /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
    // "lib": [],                             /* Specify library files to be included in the compilation. */
    ~省略~
    // "outDir": "./",                        /* Redirect output structure to the directory. */
   ~省略~
    /* Strict Type-Checking Options */
    "strict": true,                           /* Enable all strict type-checking options. */
    ~省略~
    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
    ~省略~
  }
}

省略した項目も含め、一つずつ内容を見ていきたいところですが、とりあえず動かしてみる、ということで TypeScript -> JavaScript へ変換した出力先を wwwroot/js に変更します。

tsconfig.json

{
  "compilerOptions": {
    ~省略~
    "outDir": "wwwroot/js",                        /* Redirect output structure to the directory. */
   ~省略~
  }
}

TypeScript ファイルを作る

順番が逆な気もしますが、コンパイル元となる TypeScript を用意します。

wwwroot/ts/Page.ts

function greeting(){
    alert("hello world");
}

HTML ファイルを作る

とりあえずボタン押下で JavaScript を呼ぶだけとします。

wwwroot/pages/Index.html

< !DOCTYPE html>
< html lang="jp">
< head>
    < meta charset="UTF-8">
    < title>Home< /title>
< /head>
< body>
    Hello
    < button onclick="greeting()">Message< /button>
    < script src="../js/Page.js">< /script>
< /body>
< /html>

wwwroot 以下の静的ファイルを有効にする

UseStaticFiles で wwwroot 以下の静的ファイルを有効にしておきます。

Startup.cs

using EfCoreNpgsqlSample.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace EfCoreNpgsqlSample
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddDbContext(options =>
                options.UseNpgsql("Host=localhost;Database=BookStore;Username=postgres;Password=XXX"));

            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseMvc();
        }
    }
}

これで下記のような構成になりました。

EfCoreNpgsqlSample
 L EfCoreNpgsqlSample
  L bin
  L Controllers
   L HomeController.cs
  L Models
   L Author.cs
   L Book.cs
   L Store.cs
   L EfCoreNpgsqlSample.cs
  L Properties
   L launchSettings.json
  L wwwroot
   L ts
    L Page.ts
   L pages
    L Index.html
  L appsettings.Development.json
  L appsettings.json
  L EfCoreNpgsqlSample.csproj
  L package.json
  L package-lock.json
  L Program.cs
  L Startup.cs
  L tsconfig.json

Controllers や Models など今回登場していないものもありますが、このへん 参照ということで。
( HomeController.cs については後で触れます)

TypeScript のコンパイル

TypeScript をコンパイルして、 JavaScript ファイルを作ります。

ここでも tsc を使用します。

npx tsc -b

これで Wwwroot > js 以下に Page.js が出力されます。

Page.js

"use strict";
function greeting() {
    alert("hello");
}

【Rider】変更時に自動でコンパイルしてほしい

このコンパイル作業、毎回手動で実行するのは結構大変です。

Gulp や npm などを使って自動でコンパイルすることもできますが、今回は JetBrains Rider の力を借りることにします。
(内部的にはこれらのツールを使っているかもですが)

  1. File > Settings... > Languages & Frameworks > TypeScript を開く
  2. Node interpreter に Node.js のパスを、 TypeScript に プロジェクト > node_modules にある TypeScript のパスを指定する
  3. Recompile on changes にチェックを入れる

f:id:mslGt:20190411072957p:plain

これで TypeScript のファイルを変更すると、自動でコンパイルされるようになります。

注意点として、 tsc -b のようにフォルダ・ファイルの生成は実行してくれない、というものがあります。

そのため、 TypeScript のファイルを追加したときは tsc -b を実行し、ファイルを生成しておく、というのが良さそうです。

静的ファイルのルーティング

さて、 ASP.NET Core で静的ファイルを有効にすると、 wwwroot 以下のファイルは全て外部からアクセスできるようになります。

例えばログインユーザーのみに見せたいなど、 Controller クラスを使ってルーティングをしたい場合はどうすれば良いでしょうか。

ここで静的ファイルのパスを Route として指定しても効果はありませんでした。

HomeController.cs

        ~省略~
        [Route("/")]
        [Route("/Home")]
        [Route("/pages/Index.html")]
        public string Index()
        {
            return "hello";
        }
        ~省略~

これを実行しても、 wwwroot > pages > Index.html が表示されます。

まぁせっかくの ASP.NET Core なので、 Razor を使って View として返してやれば解決するのですが、今回は使わずに頑張ってみることにします。

UseStaticFiles でパス指定

Views の下に置いて。。。など試してみたのですが、結局 wwwroot 直下を公開するのではなく、その下の階層を公開し、ルーティングを行いたいファイル( HTML )を wwwroot 以下の別階層に置く、というのが良さそうという結果になりました。

Startup.cs

        ~省略~
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "js")),
                RequestPath = "/js"
            });
            
            app.UseMvc();
        }
    }
}

これで localhost:5000/js/~ のファイルは表示されますが、 localhost:5000/pages/~ などのファイルは表示されなくなりました。

例えば wwwroot/css など他にも公開したい階層がある場合は、 app.UseStaticFiles をもう一つ追加します。

あとは、 Controller を使って表示されなくなった HTML ファイルを表示するようにします。

HomeController.cs

        ~省略~
        [Route("/")]
        [Route("/Home")]
        [Route("/pages/Index.html")]
        public IActionResult Index()
        {
            return File("pages/Index.html", "text/html");
        }
        ~省略~

これでルーティングが効くようになりました。

いったん切ります。

参照