vaguely

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

【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 2

はじめに

平成 <--> 令和 をまたいでしまいましたが、続きです。
(令和になりましても引き続きよろしくお願いいたします (..)_ )

FormData を使ってファイル送信する場合の課題として、ファイルの容量制限が、少なくともデフォルトだとかなり厳しいことが挙げられます。

テキストファイル程度なら良いのですが、動画などになってくると数十 MB を超えたりします。

これをそのまま FormData として Body に入れて渡そうとすると BadHttpRequestException が発生します。

info: Microsoft.AspNetCore.Server.Kestrel[17]
      Connection id "XXXXXXXXXXXXX" bad request data: "Request body too large."
Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large.
   at Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException.Throw(RequestRejectionReason reason)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1MessageBody.ForContentLength.OnReadStarting()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.TryStart()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.MessageBody.ConsumeAsync()
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequestsAsync[TContext](IHttpApplication`1 application)

Microsoft Docs によれば容量が大きいファイルのアップロードには stream を使う、という話があります。

ということで、これを試してみることにします。

Stream を使って PDF をアップロードする(失敗)

まぁ失敗というかなんというか。

サンプルでは Razor が使われていて、 View と Controller (コードビハインド?) で直接データのやり取りができると。

そのため Body に格納して渡すのではなく、 MemoryStream で直接メモリーに対象のファイルデータを読み込んでいるようでした。

まぁ素直に Razor 使えばよいのですが、今回は View 部分を完全に切り離せるようにしたいため、別の方法を探ります。

で、その方法って?

で、その方法なのですが、そもそも C# (Controller) 側が空の状態であっても、 Request の Body に格納して送信した時点で先ほどの Exception が発生するため、 View 側で何とかする必要がありそうです。

結局どうしたか

あれこれ調べてみたのですが、結局のところはファイルを分割して送信 -> C# (Controller) 側でひとまとめにする、という処理が必要になるようでした。

ということでこの方法を試してみます。

ファイルを分割してアップロード

ファイルの分割

ファイルを指定のサイズで分割する方法の一つに、 FileReader の readAsArrayBuffer を使うものがあります。

readAsArrayBuffer でファイルの内容を ArrayBuffer として読み込み、それを指定サイズ(バイト)ごとに分割して送信します。

mainPage.ts

exports.uploadFile = async function uploadFile(){
    const fileField = document.getElementById("upload_file_form") as HTMLInputElement;

    if(fileField === null ||
        fileField.files === null ||
        fileField.files.length <= 0){
        return;
    }
    // 本当は保存先のディレクトリ作成とか必要だと思います.

    const reader = new FileReader();
    let buffer: Uint8Array;

    // 約 8MB (実際は 7.62MB でした)ずつ分割.
    const size = 8000000;

    // 送信は前回と同じく FormData に.
    let formData: FormData;
    const file = fileField.files[0];

    // 内容が読み込まれたらファイル送信開始.
    reader.onload = async function(e: ProgressEvent) {
        
        // 参考リンクでは e.target.result になっているものがありますが,
        // 見つからなかったので FileReader から取得.
        const result = reader.result;
        if(result === null){
            console.error("result is null");
            return;
        }
        // 読み込んだ内容から Uint8Array を作り、指定サイズごとに分割 -> 送信.
        buffer = new Uint8Array(result as ArrayBuffer);
        let failed = false;   
        let fileIndex = 0;

        for(let i = 0; i < buffer.length; i += size){
            failed = false;
            formData = new FormData();
            formData.append('file', new Blob([buffer.subarray(i, i+size)]))
            await fetch('files/pdf', {
                method: 'POST',
                body: formData,
                headers: {
                    "FileName": file.name,
                    'FileIndex': fileIndex.toString(),
                }
            })
            .catch(error => {
                console.error('Error:', error);
                failed = true;
            });
            if(failed){
                console.log('failed');
                return;
            }
            fileIndex = fileIndex + 1;
            
            // このタイミングでプログレスバーをいじったりすると良さそうです.
        }

        // ファイルを送信し終わったらひとまとめにする処理を実行.
        var endFormData = new FormData();
        endFormData.append('fileName', file.name);
        await fetch('files/pdf/finished', {
            method: 'POST',
            body: endFormData,
        })
        .then(response => console.log('ok'))
        .catch(error => console.error('Error:', error));
    }
    // ファイルの内容を読みこむ.
    reader.readAsArrayBuffer(file);
}
  • 最初 PDF で試していたため URL が PDF になっていますが、手元にあるファイルサイズの関係で PDF 以外のファイルも使っていますがお気になさらず。。。

Uint8Array について

new Uint8Array(result as ArrayBuffer) のような形で生成している Uint8Array 。

見た目的には C# の List のように、中の値をコピーして新しい配列を作っているのかな? とも思ったのですが、↓ Uint8Array.prototype.buffer を見ると元の ArrayBuffer への参照を保持しているようにも思えます。

この引数について、ファイルサイズが正しい場合中身が間違っていてもエラーが発生せずに 分割 -> 送信 と処理が進んでしまうため、マージ完了後に初めてファイルが正しくないことに気づく、という落とし穴があるため注意が必要です。
(参考にしたサイトで使われていた ProgressEvent.target.result が見つからなかったのですが、代替となるデータを渡し間違えてえらい苦労をしましたorz )

Header について

fileField.files[0] をそのまま渡していた場合と異なり、ArrayBuffer として分割したデータからはファイル名が取得できません。

そのため何らかの方法で送信する必要があります。

また分割データを順番通り復元するには、送信したデータの番号も送ってやる必要があります。

Body の FormData に追加することもできますが、今回は Header に入れることにしました。

理由は試したときに Body に複数データをセットした場合のサーバー側での受け取り方がわからなかったのと、エンティティヘッダーでは「メッセージボディの内容を記述する」とあったためです。

分割されたデータの受け取り

サーバー側です。

前回と同じく引数を IFormFile にして受け取ることも可能です。

が、 Microsoft Docs も参考にカスタムクラスで受け取ってみることにしました。

UploadFile.cs

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

public class UploadFile{
    [FromForm(Name="file")]
    public IFormFile File{get; set;}
}

HomeController.cs

using System.Collections.Generic;
using System.Text;
using System;
using System.IO;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.FileProviders;
using Microsoft.Net.Http.Headers;
using System.Net.Http;
using System.Linq;

namespace Controllers {

    public class HomeController : Controller {
        private static readonly string TmpLargeFileDirectory = @"C:/Users/XXX/workspace/DownloadUploadSample/files/tmp/";

        [HttpPost]
        [Route ("/files/pdf")]
        public async Task< IActionResult> UploadPdfFile (UploadFile uploadFile) {
            
            string fileName = Request.Headers["FileIndex"] + "_" + Request.Headers["FileName"];
            if(uploadFile?.File == null){
                Console.WriteLine("failed");
                return BadRequest();
            }
            
            using(var stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)){
                await uploadFile.File.CopyToAsync(stream);
            }
            return Ok();
        }
    }
}

ほぼ前回と同じです。

カスタムのクラスを使うことで、複数データを渡す場合も名前指定で受け取ることができます。

今回は端折ってますが、ファイルアップロードの開始時点でファイルの保存先を作る、同時に別のユーザーがアップロードしたときに混ざらないようにするなどの処理が実際には必要になります。

Request.Body

データの受け取りについて、 fetch では Body にセットしているのだから、 Request.Body から受け取れないかな~と思ってみたのですが、データをうまく変換できず、断念しました。

ファイルをひとまとめにする

最後に出力されたファイルを一つにまとめます。

ローカルのファイルを読み込む・書き出す辺りは以前ローカルのファイルを読み込んだときの流用です。

HomeController.cs

~省略~
[HttpPost]
[Route ("/files/pdf/finished")]
public async Task< bool> FinishUploading(string fileName){

    List< byte> readBytes = new List< byte>();
    using(PhysicalFileProvider provider = new PhysicalFileProvider(TmpLargeFileDirectory)){
        foreach(IFileInfo fileInfo in provider.GetDirectoryContents(string.Empty)
            .Where(f => f.IsDirectory == false)
            .OrderBy(f => {
                string[] fileNames = f.Name.Split('_');
                int.TryParse(fileNames[0], out int index);
                return index;
            })){
            using(Stream reader = fileInfo.CreateReadStream()){
                int fileLength = (int)fileInfo.Length;
                byte[] newReadBytes = new byte[fileLength];
                reader.Read(newReadBytes, 0, fileLength);
                readBytes.AddRange(newReadBytes);
            }
        }
    }
    using (FileStream stream = new FileStream(TmpLargeFileDirectory + fileName, FileMode.Create)) {
        await stream.WriteAsync(readBytes.ToArray(), 0, readBytes.Count);
    }
    // 本当はこの後分割保存したファイルを削除した方が良いと思います.
    return true;
}
~省略~

おわりに

一応これでサイズが大きなファイルでもアップロードできるようにはなりました。

ただ、ファイルを分割 -> 保存 してもう一度読み込み -> マージして保存 とするのは無駄が大きいように思います。

下記は Rails + nginx ではありますが、最終 Rails ではなく nginx の機能を使って速度を上げています。

これを考えると、もう少し別の方法も考慮した方が良いように思いました。

冒頭でスキップしてしまいましたが、せっかくの ASP.NET Core なので、 Razor を使ってみるのも良いかもしれませんね。
(本当は今回試すつもりでしたが、分割アップロードで躓いて心が折れました。という言い訳)

【ASP.NET Core】【TypeScript】PDF のダウンロード・アップロード 1

はじめに

(いつものごとく) とある事情で PDF をダウンロード・アップロードする機能が欲しかったため、試してみたら苦労した、という話です。

以前 ASP.NET Core でローカルの特定のディレクトリにあるファイルをアクセス可能にしたことがありましたが、今回も同様に、特定のディレクトリに PDF ファイルを置き、それをダウンロードする機能と、そのディレクトリにファイルをアップロードする機能を作ってみます。

exports が見つからない

のっけから話の腰を折り曲げるわけですが。

今回の検証用に ASP.NET Core プロジェクトを作り、 npm install で TypeScript や webpack などを追加したところ、以前 webpack でグローバル関数を HTML からみられるようにするために使っていた 「 exports.greeting() = async function greeting(){ ~ } 」の exports が見つからないというエラーに遭遇しました。

結局のところ、 @types/node をインストールすることで解決しました。

npm install --save @types/node

型が無いからエラーに、というのはわかるのですが、これまではなぜエラーになっていなかったのか。。。

準備

まずは準備から。

PDF ファイルはプロジェクトと同じ場所に置いておきます。

で、 Static file として公開します。

Startup.cs

using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;

namespace DownloadUploadSample
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseStaticFiles();
            
            app.UseStaticFiles(new StaticFileOptions {
                FileProvider = new PhysicalFileProvider(
                    Path.Combine(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample", "files")),
                RequestPath = "/files"
            });
            
            app.UseMvc();
        }
    }
}

HomeController.cs

using Microsoft.AspNetCore.Mvc;

public class HomeController: Controller{
    [Route("/")]
    public IActionResult Index(){
        return View("./Views/Index.cshtml");
    }
}

ダウンロードもアップロードも Index.cshtml から実行することにします(個別で作るの面倒なので)。

PDF をダウンロードする

まずはファイルに対する Link を設定して、ダウンロードできるか試してみます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' target="_brank">PDF< /a>

結果としては、ダウンロードされるのではなくブラウザの PDF Viewer で表示されました。

一応 target="_brank" も外してみましたが、単に開いているタブで表示されるだけで、ダウンロードされることはありませんでした。

download 属性を追加する

モダンなブラウザであれば、下記のように download 属性をつけることで、強制的にダウンロードさせることができます。

Index.cshtml

< a href='http://localhost:5000/files/sample.pdf' download>PDF< /a>

めでたしめでたしといいたいところですが、この属性、 IE だと( ver.11 でも)無視されます/(^o^)\

またか。。。

※なぜか家の IE 11 で確認したところ問題なくダウンロードのポップアップが表示されました。。。

なんでや。。。orz

でも話が終わってしまうので、できなかった体で進めます。

msSaveOrOpenBlob

IE 、 Edge で使うことのできる、 msSaveOrOpenBlob を利用してダウンロードする方法も試してみました。

ほぼ上記のコピペではあったものの、ファイルがダウンロードできることが確認できました。

なぜこれを選ばなかったかというと、下記 2 つの理由によります。

  1. IE 、 Edge でしか使う必要がない (しかも Edge は download 属性に対応しているためこの用途では不要)
  2. 「ダウンロードして開く」「保存する」のポップアップがファイルダウンロード後に表示される

2についてはリンククリック時にポップアップを出して、スピナーなどを出して。。。という機能を用意すれば良いのですが、 UI 的にこだわりたい場合はともかく、ただダウンロードできれば良いレベルの場合も、というのはちょっとツラいですね。

Web ブラウザーが表示対応しているばかりにこんなことに。。。(いつもは便利に使わせていただいております (..)_ )

ASP.NET Core の機能でダウンロード

いったん JavaScript で何とかするのをあきらめて、今回は ASP.NET Core 側で何とかしてみることにしました。

HomeController.cs

~省略~
[Route("/files/pdf")]
public IActionResult DownloadPdfFile(){
    return DownloadFile(@"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files", "sample.pdf");
}
private FileResult DownloadFile(string filePath, string fileName)
{
    IFileProvider provider = new PhysicalFileProvider(filePath);
    IFileInfo fileInfo = provider.GetFileInfo(fileName);
    Stream readStream = fileInfo.CreateReadStream();
    string mimeType = "application/pdf";
    return File(readStream, mimeType, fileName);
}
~省略~

ファイルを Stream に変換し、 FileResult として返すことで PDF として扱われなくなる、ということなのでしょうか。

やっぱりやりたいことに対して大げさすぎる気がしないでもないですが、ともあれ PDF がダウンロードできるようになりました。

FormData を使って PDF をアップロードする

ASP.NET Core のドキュメントを見ると、ファイルのアップロードには フォームデータとしてアップロードする方法と、 Stream でアップロードする方法があるとのこと。

あまりファイル容量が大きい場合は向かないとのことですが、まずはフォームデータを試してみたいと思います。

Index.cshtml

~省略~
< input type='file' id='upload_file_form'>
< button onclick='Page.uploadFile()' accept='pdf'>send< /button>
< script src='./src/js/main.bundle.js'>< /script>

mainPage.ts

exports.uploadFile = async function uploadFile(){
    // HTMLInputElement.files で 参照ボタンを押して選択されたファイルが取得できる.
    const fileField = document.getElementById("upload_file_form") as HTMLInputElement;

    if(fileField === null ||
        fileField.files === null ||
            fileField.files.length <= 0){
        return;
    }
    // フォームに入れて fetch で送る.
    const formData = new FormData();
    formData.append('file', fileField.files[0]);

    await fetch('files/pdf', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .catch(error => console.error('Error:', error))
    .then(response => console.log('Success:', JSON.stringify(response)));
}

HomeController.cs

~省略~
[HttpPost]
[Route("/files/pdf")]
public async Task UploadPdfFile(IFormFile file){
    if(file == null){
        return false;
    }
    // 受け取ったファイルを FileStream に変換し、ファイル名を変更して保存.
    using(var stream = new FileStream(
            @"C:\Users\example\OneDrive\Documents\workspace\DownloadUploadSample\files\" +
                DateTime.Now.ToString("yyyyMMddHHmmss") + 
                    file.FileName, FileMode.Create)){
        await file.CopyToAsync(stream);        
    }

    return true;
}
~省略~

なぜか fetch の option として、 headers: { "Content-Type": "application/pdf" } を渡すと IFormFile が null になってしまうなど、しっくり来ていないところも多いですが、とにかくファイルを送信することはできました。

ただ、ファイルの容量が数十 MB など大きくなってくると、 ASP.NET Core 側でエラーが発生してしまいます。

ググったところ、この容量制限を緩和することはできるようですが、 ASP.NET Core のドキュメントでも挙げられている通り Stream を使うのが良さそうです。

ということで、次回は Stream を使ったアップロードの話。。。の予定。

コンパイルを自動化したい話 + (Pikaday + TypeScript) で IE でも DatePicker を表示したい話

はじめに

ここまで tsc による TypeScript -> JavaScript 変換、 webpack によるバンドルや PostCSS -> CSS 変換など、ツールによるコンパイルをいくつか試してきたわけですが、正直これを手動でやり続けるのは大変です。

ということで、これを自動で実行してくれるようにしてほしかった話と、前回の DatePicker の話の続きです。

npm scripts で自動的にコンパイル

npm scripts を使って面倒なコンパイル処理をもう少し簡素化できるようです。

npm scripts 以外にも自動で処理を実行してくれるツールは色々あるようですが、全く別のツールを導入しなくて良い、ということと、今回やりたいことは十分実現できそう、ということで今回はこれでいくことにしました。

とりあえず npm scripts でコンパイルを実行

まずはこれまで実行してきた下記二つのコマンドを npm scripts で実行してみます。

  • npx webpack
  • npx postcss pcss/*.css -c postcss.config.js -d src

これらを package.json に scripts として追加します。

package.json

{
    
    "scripts": {
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    
    "devDependencies": {
        ~省略~
    }
}

で、あとはターミナルから下記のように実行すれば OK です。

npm run css

webpack の方は

npm run webpack

まとめて実行

今は二つですが、今後増えてくるとやっぱり面倒なので、一括で実行したいところ。

npm-run-all を使ってみます。

npm install --save-dev npm-run-all 

package.json

{
    "scripts": {
        
        "all": "run-s css webpack",
        
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src",
        "webpack": "npx webpack"
    },
    "devDependencies": {
        ~省略~
    }
}

これで npm run all とすれば両方実行してくれます。

自動で実行してほしい

2 回実行するものが 1 つにまとまって、少しは楽になったもののやっぱり変更のたびに都度実行するのはちょっと。。。

Rider で tscコンパイルを、ファイルの変更を感知して自動で実行してくれたように、 webpack や PostCSS も自動実行してほしいですね。

通常 npm scripts で実現するためには、 watch を使うことができます。

npm install --save-dev watch

ですが、 webpack 、 PostCSS の場合、 watch を使わなくてもオプションとして watch の機能を持っています。

ということで、 package.json を下記のように変更するだけで、ファイルの変更の監視、コンパイルの自動実行をしてくれるのでした。

package.json

{
    "scripts": {
        "all": "run-s css webpack",
        "css": "npx postcss pcss/*.css -c postcss.config.js -d src -w",
        "webpack": "npx webpack -w"
    },
    "devDependencies": {
        ~省略~
    }
}

別でターミナルを立ち上げて実行

今回の開発は Visual Studio Code (以下 VS Code )を使っているのですが、 VS Code ではメニューからターミナル( PowerShell )を立ち上げることができます。

コンパイルや npm install をする場合はこれを使うのが便利なのですが、上記の処理については別で PowerShell などを立ち上げて実行するのが良いかと思います。

理由は明示的に処理を終了するまでずーっと監視し続けるため、他のコマンドが打てなくなるためです。

IE11 での input type='date'

前回の話で、 DatePicker を表示するのは input の type を date にするだけ。

えらい簡単だな~凄いな~と思っていたわけですよ。

IE で確認するまでは。

まぁ当然のように動かない、と/(^o^)\

自分で作るのは厳しそうなので、良さげなものないかな~と思っていたところ、 Pikaday を見つけました。

シンプルに使えそうとか良いところは色々あるわけですが、とりわけ IE の対応が ver.7 からというのがありがたい (..)_ (今回は IE7 には対応していませんが)

ということで試してみます。

準備

とにかくインストールから。

色々方法はあるようですが、 npm install することにしました。

npm install --save-dev pikaday

CSS の読み込み

Pikaday を使うにあたり、必要なファイルは 2 つです。

  • Pikaday.js
  • Pikaday.css

JavaScript の方は import なりなんなりすれば利用できるので問題ありません。

ただ、 CSS をどうするか。

ファイル自体は node_modules > pikaday > css に置かれています。

HTML から node_modules 以下のパスを指定して読み込む、または手動あるいは npm scripts でコピーという手もなくはないですが、自分で設定する CSS ファイル程更新は頻繁ではないでしょうし、かといって放置というわけにもいかずいまいち。

などと思っていたら、 postcss-import を使う方法が見つかりました。

これを使うことで、 JavaScript の import / export で分割したファイルを webpack でひとまとめにするのと同じようなことができます。

postcss-import を使う

上記リンク参照というところではありますが、一応ここにも書き残しておきます。

まずはインストール。

npm install --save-dev postcss-import

PostCSS から使用するので、 postcss.config.js に追加します。

postcss.config.js

module.exports = {
    plugins: [
        
      require('postcss-import')(),
      
      require('autoprefixer')({
        "grid": true,
        "browsers": [
          "last 2 versions"
        ]
      }),
      require('precss'),
    ]
  }

で、表示するページで使用する CSS に import を追加してやります。

page.css

@import '../node_modules/pikaday/css/pikaday.css';

~省略~

Pikaday の README と違ってパスが ../ になっているのは、 page.css が pcss というディレクトリ内にあるからです(想定されている CSS ファイルとパスが異なる)。

これでコンパイルすると、 page.css の内容に pikaday.css が追加されたものが出力されます。

HTML から node_modules 以下のパスを指定する場合、 ASP.NET Core (というか Web アプリ側)の静的ファイルの公開範囲をいじる必要が出てきますが、 PostCSS のコンパイル時だけ参照するのであれば問題はぐっと減ってくれそうです。

なお、何らかの理由で pikaday.css の内容と他のファイルを分けたい場合は、 @import ~ だけ書いた

Pikaday を使う

Pikaday を使う場合、対象の要素を指定する、日付のフォーマットを指定する、といった初期化処理を行う必要があります。

< script src="pikaday.js">< /script>
< script>
    var picker = new Pikaday({ 
        field: document.getElementById('datepicker') 
    });
< /script>

注意点としては document.getElementById('datepicker') が実行されるより前に該当の DOM 要素がロードされている必要があるため、この処理は HTML の下部に書く必要がある、ということです。

フォーマットを変更する

デフォルトだと下記のデモのように曜日や月が英語で表示されます。

例えばこれを日本語に変更したい場合、初期化処理時に指定することができます( 項目は pikaday.js の defaults で初期値が設定されています)。

で、これらを DatePicker 使うたびに設定するのはツラすぎるので、クラスにまとめてみました。

import Pikaday from 'pikaday';

class CustomDatePicker{
    public static getPikaday(elementName: string): Pikaday{
        const today = new Date();
        const yearFrom = 1999;
        const yearTo = today.getFullYear() + 1;

        return new Pikaday({
            field: document.getElementById(elementName),
            firstDay: 1, // 月曜始まりにする
            minDate: new Date(yearFrom.toString() + '-1-1'), // 選択できる最小範囲
            maxDate: new Date(yearTo + '-12-31'),
            yearRange: [yearFrom, yearTo], // 選択できる年の範囲。 1999 - 一年後までとする
            yearSuffix: '年', // 2019年 と表示されるようにする
            showMonthAfterYear: true, // 2019年 四月 と表示されるようにする
            i18n: {
                previousMonth : '前月',
                nextMonth     : '次月',
                months        : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
                weekdays      : ['日曜','月曜','火曜','水曜','木曜','金曜','土曜'],
                weekdaysShort : ['日','月','火','水','木','金','土'],
            }
        })
    }
}
export {CustomDatePicker}

Pikaday を import する

@types/pikaday のインストールが必要そうです。

npm install --save-dev @types/pikaday

field で渡せる要素

getElementById となっている通り、一つの Pikaday インスタンスで設定できる要素は一つだけのようです。

ということで、 ID は外から渡せるようにしています。

複数ファイルをバンドルして出力する

例えば今回の CustomDatePicker を ページのスクリプトとは別にバンドルしたい場合。

webpack.config.js の entry で複数パスを渡してやることと、 output で名前が重複しないよう名前を指定する必要があります。

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',

    entry: {
        'main':'./ts/mainPage.ts',
        'datePicker':'./ts/customDatePicker.ts',
    },
    
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: '[name].bundle.js',
        path: path.resolve(__dirname, 'src/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};

課題としては、(今回使っていませんが) SourceMap を生成すると一つ分しか生成されない、ということと、 npm run all で TypeScript を変更した場合に処理がうまく動いていない気がする、という二点は把握しています。

この辺りは解決したら追記、または別記事で投稿します。

flexbox でレイアウトを組みたい話

はじめに

とりあえずベンダープレフィックスの件は解決できました(多分)。

ということで、 flexbox を使ってレイアウトを組んでみたいと思います。

まずはレスポンシブデザインにはしないことにして、かつベンダープレフィックス無しでの指定に対応している・個人的に一番よく使っている Firefox をベースに作ってみたいと思います。

Autoprefixer や IE は?というところですが、まずはレイアウトを組んで、そのあと対応としたいと思います。
(その方法が良いのかどうかはわかりませんが)

外枠を作る

まず各エリアの外枠を作ってみることにします。

mainPage.html

< !DOCTYPE html>
< html lang="jp">
    < head>
        < meta charset="utf-8">
        < title>main
        < link rel="stylesheet" type="text/css" href="../src/page.css" >
    < /head>
    < body>
        < header id="global_header">
        < /header>
        < section id='page_title'>page title< /section>
        < div id='container'>
            < section id='search_area'>
            < /section>
            < section id='item_view'>
                < div id='search_result'>
                < /div>
                < div id='image_view'>
                < /div>
            < /section>  
        < /div>
    < /body>
< /html>

page.css

html{
  height: 100%;
  width: 100%;
  margin: 0px;
}
body{
  height: 100%;
  width: 100%;
  margin: 0px;
  min-width: 900px;
  min-height: 600px;
}
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
}
#page_title{
  width: 100%;
  height: 5%;
  background-color: aqua;
}
#container {
  display: flex;
  flex-direction: row;
  width: 100%;
  height: 90%;
  background-color: gray;
}
#search_area{
  background-color: pink;
  flex: 1;
}
#item_view{
  flex: 3;
}
#search_result{
  height: 50%;
  background-color: darkgoldenrod;
}
#image_view{
  height: 50%;
  background-color: greenyellow;
}

表示結果はこちら。

f:id:mslGt:20190419230542j:plain

何となくで書いた割には良い感じですね( ´∀`)bグッ!

中のアイテムを追加していく

では中身を追加していきます。

まずは header から。

タイトルとボタンを追加してみます。

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < button id='header_share'>Share< /button>
< /header>
~省略~

結果はこちら。

f:id:mslGt:20190419230638j:plain

。。。謎のスペースが生まれています。

この理由は先ほど追加した h1 。

margin-top がデフォルトで追加されているらしく、その分が白いスペースとして表示されていたようです。

ということで、 h1 の margin-top を 0 にします。

ついでに margin-left 、 テキストカラーも変えておきます。

page.css

~省略~
header h1{
  color: whitesmoke;
  margin-top: 0;
  margin-left: 10px;
}
~省略~

両端に並べたい

今のままだと文字とボタンが左寄せで表示されているので、これを両端に並べてみます。

page.css

~省略~
header{
  width: 100%;
  height: 5%;
  background-color: cornflowerblue;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
}
~省略~

mainPage.html

~省略~
< header id="global_header">
    < h1>Hello world!< /h1>
    < nav id='header_share_area'>
        < button class='header_share'>Share1< /button>
        < button class='header_share'>Share2< /button>
    < /nav>
< /header>
~省略~

justify-content: space-between を使うことで、子どもの要素を両端 → 中間の順で並べてくれます。

f:id:mslGt:20190419230751j:plain

左側のレイアウト

左の領域は検索条件を指定する領域にしたいと思います。

ということで、思い付きで要素を並べます。

mainPage.html

~省略~
< section id='search_area'>
    < div id='search_frame'>
        < div id='search_title'>検索< /div>
        < div id='search_query'>
            < label id='search_title_query'>キーワードで検索< /label>
            < input type='text' id='search_query_text'>< /input>
        < /div>
        < div id='search_check'>                        
            < label>チェック< /label>
            < ul>
                < li>
                    < input type='checkbox' value='search_availale'>available< /input>
                < /li>
                < li>
                    < input type='checkbox' value='search_availale_2'>available2< /input>
                < /li>
            < /ul>
        < /div>
        < div id='release_data'>
            < label>発売日< /label>
            < input type='date' value='search_relase_date_from'>< /input>
            < label>~< /label>
            < input type='date' value='search_relase_date_to'>< /input>
        < /div>
        < div id='search_submit'>
            < button id='search_button'>検索< /button>
        < /div>
    < /div>
< /section>
~省略~

そのまま表示するとこんな感じ。

f:id:mslGt:20190419230913j:plain

これを縦に並べていきたいと思います。

テキストボックス

とりあえず簡単そうかつ上の要素であるテキストやテキストボックスから。

page.css

~省略~
#search_frame{
  width: 90%;
  height: 90%;
  margin: 5%;
  background-color: white;
}
#search_title{
  text-align: center;
  color: aliceblue;
  background-color: green;
  height: 4%;
  margin-top: 2%;
}
#search_query{
  height: 10%;
  padding-top: 5%;
  width: 80%;
  margin-left: 10%;
}
#search_query input{
  width: 100%;
}
~省略~

input (テキストボックス) の幅を 100% にしているのは、他の要素の幅と合わせたかったためです。

チェックボックス

次はチェックボックス

横に並んでいるため、まず縦並びにする必要があります。

最初は下記のように flexbox を使って並べようと考えていました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < input type='checkbox' value='search_availale'>available< /input>
    < input type='checkbox' value='search_availale_2'>available2< /input>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;

  display: flex;
  flex-direction: column;
}
~省略~

結果はこの通り。

f:id:mslGt:20190419231003j:plain

チェックボックスの □ とラベル部分が別々に並べられてしまいました/(^o^)\

flexbox を使って正しく並べる方法もあるだろうとは思うのですが、今回はリストを使って並べてみることにしました。

mainPage.html

~省略~
< div id='search_check'>                        
    < label>チェック< /label>
    < ul>
        < li>
            < input type='checkbox' value='search_availale'>available< /input>
        < /li>
        < li>
            < input type='checkbox' value='search_availale_2'>available2< /input>
        < /li>
    < /ul>
< /div>
~省略~

page.css

~省略~
#search_check{
  height: 14%;
  width: 80%;
  margin-left: 10%;
}
#search_check ul{
  list-style: none;
  padding-top: 0;
  margin-top: 0;
  padding-left: 0;
}
#search_check li{
  margin-top: 1%;
}
~省略~

ul において、 list-style を none にすることで・をなくし、デフォルトでは左のラインより右に表示されてしまうので、 padding-left を 0 にしています。

残り(テキストボックス、ボタン)

残りの日付入力用のテキストボックスと、検索ボタンも配置します。

mainPage.html

~省略~
< div id='release_data'>
    < label>発売日< /label>
    < input type='date' value='search_relase_date_from'>< /input>
    < label>~< /label>
    < input type='date' value='search_relase_date_to'>< /input>
< /div>
< div id='search_submit'>
    < button id='search_button'>検索< /button>
< /div>
~省略~

page.css

~省略~
#release_data{
  display: flex;
  flex-direction: column;
  height: 18%;
  width: 80%;
  margin-left: 10%;
}
#release_data input{
  width: 100%;
}
#search_submit{
  width: 80%;
  margin-left: auto;
  margin-right: 10%;
  text-align: right;
}
#search_submit button{
  width: 80px;
}
~省略~

ボタンがなかなか右寄せできずにあれこれ試していたのですが、 text-align で一発という。。。

ラベルはともかく、 text-align が何に有効なのかを把握しておかないと地味にハマりそうです。

結果

結果としてこうなりました。

f:id:mslGt:20190419231105j:plain

もう少しデザイン的にこだわった方がとかある気はしますが、いったんこれで止めて次に進めることにします。
(白い枠の部分はもう少し縦を短くしても良さそうですが)

ほとんど触れませんでしたが、 input の type を date にするだけで、 DatePicker まで表示してくれるのはすごいですね!

f:id:mslGt:20190419231147j:plain

ただ、これブラウザごとに違いがあったりするようです
(下記は 2017 年の情報であり、 Firefox を含めた今のモダンブラウザは違う状況だとは思いますが)。

。。。ん? ブラウザごとの違い。。。?

PostCSS 、 Autoprefixer の力を借りたかった話

はじめに

相変わらず HTML や TypeScript と戯れているわけなんですが、動きだけでなく見た目の部分も重要です。

で、まず全体の配置を指定したいと思ったのですが、これには CSS Grid を使うのが良さそうな気がしました。

で、調べてみると IE11 でも使うことができるらしい、と。

サンプルをコピペで動かしてみると、確かに IE11 でも正しく表示されている。。。! いいですね(゚∀゚)!!

(割と新しめの技術 + IEググるとだいたいどれも情報がいっぱい出てくるあたり、皆様の苦労が感じられます)

一点、サンプルの中に登場する Autoprefixer というものが気になりました。

今回必要となる -ms~ といったベンダープレフィックスを自動でつけてくれるとのこと(また不要なものを外してもくれるらしいです)。

じゃあこれも使ってみようか、と試してみたのが今回の話です。

コンパイラーを選ぶ

サイト上でコピペするとベンダープレフィックスをつけてくれるものもあるのですが、ちょっと試すのはともかく開発中やり続けるのは少々大変ですね。

https://autoprefixer.github.io/

ということで、 TypeScript における tsc などのように、コマンドなどで自動実行してくれるものが欲しいところ。

すると、 Autoprefixer は単体でというよりは AltCSS と呼ばれる CSS を便利にしたもののプラグインとして提供されているらしい、というところがわかりました。

じゃあそれも使っちゃえ!ということで、 Autoprefixer を使った情報が比較的多く、かつ Node.js でコンパイルできる PostCSS を使ってみることにしました。

コンパイル方法多すぎ問題

※ 手段が豊富なことは色々な利用シーンに対応してくれる、ということで、素晴らしいことであり、否定しているわけではありません。
 念のため。

PostCSS の情報を調べると、非常にたくさんのコンパイル方法が紹介されています。

が、素人には選べない。。。

WebPack

とりあえず TypeScript で使っているし、ということで WebPack を試してみることにしたのですが、失敗しました。

というのも、 PostCSS のファイルを JavaScript(TypeScript) から呼び出しているわけではなかったためです。

じゃあ他は。。。とググった結果、 postcss-cli を使うことにしました。

ということでインストールします。

 npm install --save postcss postcss-cli precss autoprefixer

PostCSS ファイルを postcss-cliコンパイルする

WebPack と同じように、まずプロジェクト直下に設定ファイルを作成します。

postcss.config.js

module.exports = {
    plugins: [
      require('precss'),
      require('autoprefixer')
    ]
  }

Autoprefixer と合わせて require で指定しているのは、 PostCSS から CSS に変換してくれる(多分) PreCSS です。

下記のコマンドでコンパイルすることができます。

npx postcss 変換前の PostCSSファイルパス -c postcss.config.js -d 出力先のディレクトリパス

または

npx postcss 変換前の PostCSSファイルパス -c postcss.config.js -o 出力先のファイルパス

とりあえず変換用にファイルを作る

pcss/page.css

.container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 50px 1fr 50px;
}
p {
    span {
        grid-column: 1;
        grid-row: 2;
        color: deepskyblue;
    }
}

PostCSS とわかるように p span を親子関係にし、 Autoprefixer を試すため、 grid を使っています。

これをコンパイルしました。

npx postcss pcss/*.css -c postcss.config.js -d src

そして動かない Autoprefixer

実行した結果は下記のようになりました。

src/page.css

.container {
  display: grid;
  grid-template-columns: 200px 1fr;
  grid-template-rows: 50px 1fr 50px;
}
p span {
        grid-column: 1;
        grid-row: 2;
        color: deepskyblue;
    }

むぅ。。。 PreCSS が通常の CSS に変換していることは確認できましたが、 ベンダープレフィックスが付いていない。。。

あれこれ調べてみた結果、(なぜか) postcss.config.js を下記のように変更すると正しく動作しました。

postcss.config.js

module.exports = {
    plugins: [
      require('autoprefixer')({ "grid": true }),
      require('precss')
    ]
  }

grid を明示的に true にする必要があるようです。

また、これも理由がわからないのですが、 require('precss') を Autoprefixer より上に書くと、 Autoprefixer は効くのに PreCSS が効かない、という問題が発生しました。

腑に落ちないところは多くありますが、とにかくベンダープレフィックスも自動でつけられるようになりました。

やったね。

src/page.css

.container {
  display: -ms-grid;
  display: grid;
  -ms-grid-columns: 200px 1fr;
  grid-template-columns: 200px 1fr;
  -ms-grid-rows: 50px 1fr 50px;
  grid-template-rows: 50px 1fr 50px;
}
p span {
        -ms-grid-column: 1;
        grid-column: 1;
        -ms-grid-row: 2;
        grid-row: 2;
        color: deepskyblue;
    }

flexbox の場合

こうして CSSGrid が使えるようにはなったのですが、どうもレイアウトを組んでいく内に、これ flexbox の方が合ってるのでは? と思い始めたので、 flexbox に切り替えることにしました。

具体的なところは次回にまわしますが、 grid の場合と同じく Autoprefixer が効かない、という問題が発生したため、 postcss.config.js を下記のように変更しました。

postcss.config.js

module.exports = {
    plugins: [
      require('autoprefixer')({
        "grid": true,
        "browsers": [
          "last 2 versions"
        ]
      }),
      require('precss')
    ]
  }

フロントエンドエンジニアの方々の苦労や工夫の賜物であるとはいえ、いっぺんに取り入れすぎて消化不良、というところ。。。

とはいえ、少しずつでも理解していきたいところです。

ASP.NET Core + TypeScript ってみる 3

はじめに

今回は少~しだけ fetch の話と、 webpack を使った import / export の話です。

全然 ASP.NET Core が出てこない? 

。。。(..)_

fetch について

IE 対応

※前回使用した fetch は、 Firefox などでは問題なく動作するのですが、 IE ではまた fetch が見つからないとエラーになります。

ということで、 polyfill 版である whatwg-fetch を使用することにします。

npm install --save-dev whatwg-fetch

下記のように使うことができます。

import 'whatwg-fetch'

public async function loadBooks(): Promise< Array< Book>> {
        return await window.fetch("/books",
            {
                mode: 'cors',
            })
            .then((response) => response.json())
            .then((myJson) => JSON.parse(JSON.stringify(myJson)) as Array< Book>);
    }

(互換性モード?知らない子ですね)

import については後述します

fetch のオプション

fetch API の中で特に気になるのが、第二引数として渡しているオプション。

ここについて調べて。。。と思ったのですが、 MDN に詳しく載っているのでそこ参照ということで。。。

ただ、例えば mode や credentials といった設定で、特にログイン時にどうなるの?といった疑問はあるため次回以降で色々試してみたいと思います。

webpack を使う

さて、次は取得した Book データを Table に出力したいのですが、そろそろコード量が多くなってきました。

C# でクラスを分割するように、 import / export を使うことで TypeScript のコードも分割することができます。

その。。。はずだったのです。

WebAccessor.ts

class Book {
    public id: number = -1;
    public authorId : string = "";
    public name: string = "";
    public available: boolean = false;
}
class WebAccessor {
    public async loadBooks(): Promise< Array< Book>> {
        return await fetch("/books",
            {
                mode: 'cors',
            })
            .then((response) => response.json())
            .then((myJson) => JSON.parse(JSON.stringify(myJson)) as Array< Book>);
    }
}
export {WebAccessor, Book};

一部ラムダ式に変更してはいるものの、やっていることは前回と同じです。

Page.ts

import {WebAccessor} from "./WebAccessor";

async function greeting(){
    var accessor = new WebAccessor();
    let books = await accessor.loadBooks();
    if(books !== null &&
        books.length > 0){
        alert(books[0].name);
    }
    else{
        alert('failed');
    }
}

npx tsc -b のコンパイルは問題なく完了します。

で、これを実行するとどうなるか。

ReferenceError: exports is not defined

/(^o^)\

どうやら、 import / export がサポートされているのは ECMAScript 6 からで、今回のように古いバージョンに対応するためには RequireJS などを使って動的に解決するか、 webpack などを使って依存する JavaScript のファイルを一つのファイルにまとめる(バンドルする)必要があるようです。

そこで前から気になっていたこともあり、 webpack を使ってみることにしました。

準備

↑などを参考に、まずは必要なものをインストールします。

npm install --save-dev webpack webpack-cli ts-loader

設定ファイルをプロジェクト直下に作成します。

tsconfig は JSON ですが、こちらは普通の JavaScript ファイルなのでちょっと注意が必要?かもしれません。

webpack.config.js

var path = require('path');

module.exports = {
    mode: 'development',
    entry: './wwwroot/ts/Page.ts',
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                use: 'ts-loader',
                exclude: /node_modules/
            }
        ]
    },
    resolve: {
        extensions: [ '.tsx', '.ts', '.js' ]
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js')
    }
};
  • mode: 無くても実行はできますが、警告が出るため素直に development / production を指定します。
  • entry: Page.ts をベースに、 ts-loader を使って依存している '.tsx' 、 '.ts' 、 '.js' ファイルをバンドルするようです。
  • output: 出力先のファイル名、ディレクトリを指定しています。

実行、そして次の課題

↓のコマンドを実行すると、 wwwroot > js に bundle.js というファイルが出来上がります。

npx webpack

後は、 HTML で読み込む JavaScript ファイルを bundle.js に置き換えます。

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/bundle.js">< /script>
< /body>
< /html>

これを実行すると...

ReferenceError: greeting is not defined

/(^o^)\

どうやら、デフォルトでグローバル変数や関数は定義できない(呼べない?)ようになっているようです。

なんとか解決

結局このように書くことで無事呼ぶことができるようになりました。

webpack.config.js

~省略~
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js'),
        library: 'Page',
        libraryTarget: 'umd'
    }
};

Index.html

~省略~
< body>
    Hello
    < button onclick="Page.greeting()">Message< /button>
    < script src="../js/bundle.js">< /script>
< /body>
< /html>

動いた~、は良いのですが、この library とか libraryTarget って何なのでしょうか。

output.library について

によると、 JavaScript (今回は TypeScript ですが)の関数をライブラリとして外部( HTML )に公開することができる仕組みである、とのこと。

webpack のドキュメントを見ると、書き方がいくつかあり (libraryTarget で指定)、今回使用した umd の他、 var 、 amd が紹介されています。

例えば libraryTarget として var を指定した場合、下記のようになります ( Index.html は同じです)。

webpack.config.js

~省略~
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'wwwroot/js'),
        library: 'Page',
        libraryTarget: 'var'
    }
};

Page.ts

import {WebAccessor} from "./WebAccessor";

async function greeting(){
    try {
        var accessor = new WebAccessor();
        let books = await accessor.loadBooks();
        if (books !== null &&
            books.length > 0) {
            alert(books[0].name);
        }
    }catch (e) {
        alert(e.message)
    }
}
var Page = greeting();

TypeScript だと書き方が異なるということなのか、 amd の方法は静的解析の時点でエラーになってしまい、うまくできませんでした。

ドキュメントが読み込めていないのだと思いますが、 var と umd それぞれの利点がよく分かっていないため、なんとなく umd の方がまとまってて良さそうだな~くらいに思ってはいます。

参照

ASP.NET Core + TypeScript ってみる 2

はじめに

さくさく進めていきますよ。

とりあえず環境は整ったっぽい(本当はバンドルとか minify とあるだろうけど)ので、実際に動かしてみますよ。

。。。とその前に

IE11 で async/await を使う

もう平成も終わるというのに。。。

まぁお察しくだしあ。くだしあ。

async/await を使ってみる

まず雑に async/await を使って TypeScript のコードを変更してみます。

Page.ts

async function greeting(){
    await setTimeout(() => {
        alert("hello");
    }, 1000);
}

これで greeting() が呼ばれた後 1 秒後にアラートが表示されるはずです。

が、 tsc -b を実行するとエラーが発生します。

wwwroot/ts/Page.ts:1:16 - error TS2705: An async function or method in ES5/ES3 requires the 'Promise' constructor.  Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your `--lib` option.

1 async function greeting(){

Found 1 error.

Promise が無いからライブラリに追加しろと。

↑のような情報を参考に、 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": ["dom", "es2015"],                             /* Specify library files to be included in the compilation. */
    
    ~省略~
  }
}

これでコンパイルできるようになり、 Firefox などでは問題なく動作するようになりました。

ただし、 IE11 ではまだ Promise が見つからないとエラーになります( target を es3 にしても同様)。

↑を参考に、 Promise-X.X.X.min.js を追加で読み込むことで IE11 でも問題なく動くようになりました。

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="https://www.promisejs.org/polyfills/promise-7.0.4.min.js">< /script>

    < script src="../js/Page.js">< /script>
< /body>
< /html>

IE11 だけでなく、互換性表示を有効にしている場合でも問題なく動作することが確認できました。

ありがたやありがたや(..)_

fetch でアイテムを Get する

まずは CRUD ! 。。。と思ったのですが、まず先行してサーバーにアクセスする部分を書いてみたいと思います。

せっかくなので async / await を使って。

↑ などを見ると、 fetch または axios を使うのが良さそうです。

まずは fetch を試してみます。

https 問題再び

以前触れた気がしますが、現状 ASP.NET Core ではデバッグ実行するとデフォルトで https://localhost:5001 を開きます。

で、何も考えずに下記のようなコードを書いたところ、 loadBooks() でエラーになりました orz

Page.ts

async function loadBooks(): Promise< string>{
    return await fetch("http://localhost:5000/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            
            console.log(myJson);
            return myJson;
        });
}

async function greeting(){
    let message = await loadBooks();
    alert(message);
}

エラーの内容は下記の通り。

混在アクティブコンテンツ “http://localhost:5000/books” の読み込みをブロックしました[詳細] Page.js:41:45
TypeError: NetworkError when attempting to fetch resource.

↑によると、 HTTP リクエストと HTTPS リクエストが混在してしまうとセキュリティ上の問題が発生しうるためエラーになる、と。

なるほど。

というわけで、下記のように変更しました。

Page.ts

async function loadBooks(): Promise< string>{
    return await fetch("/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            
            console.log(myJson);
            return myJson;
        });
}
~省略~

なお Controller (サーバー側)はこちら。

HomeController.cs

        ~省略~
        [Route("/books")]
        [Produces("application/json")]
        public List< Book> GetBooks()
        {
            return new List< Book>
            {
                new Book
                {
                    Id = 1,
                    AuthorId = 1,
                    Name = "Sample",
                    Available = false,
                }
            };
        }
        ~省略~

Book.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;
using Newtonsoft.Json;

namespace EfCoreNpgsqlSample.Models
{
    [Table("Book")]
    public class Book
    {
        [Key]
        [JsonProperty("id")]
        public int Id { get; set; }
        [JsonProperty("authorId")]
        public int AuthorId { get; set; }
        [JsonProperty("name")]
        public string Name { get; set; }
        [JsonProperty("available")]
        public bool Available { get; set; }       
    }
}

JSON から Model に変換

細かいところはさておき、とりあえずここまでのコードでサーバーから送られた Book クラスのリストを受け取ることができました。

で、せっかくの TypeScript なので、型を持ったデータとして扱いたいですね。

Page.ts

class Book{
    public id: number = -1;
    public authorId : string = "";
    public name: string = "";
    public available: boolean = false;
}
async function loadBooks(): Promise< Book[]>{
    return await fetch("/books",
        {
            mode: 'cors',
        })
        .then(function(response) {
            return response.json();
        })
        .then(function(myJson) {
            var newBooks = JSON.parse(JSON.stringify(myJson)) as Array< Book>;
            return newBooks;
        });
}

async function greeting(){
    let message = await loadBooks();
    alert(message[0].name);
}

結果を見るとシンプルですね。

注意点としては下記の二つぐらいでしょうか。

1. 2 つ目の then の myJson は直接 JSON に変換できない

response.json() の戻り値は Promise< any> です。

で、 2 つ目の myJson は any になるわけですが、これをそのまま JSON.parse に渡すとエラーになります。

そのため、 JSON.stringify でいったん string に変換して渡しています。

2. as T でキャストに失敗しても null にならない

C# に慣れすぎた。。。orz

C# の as は、キャストに失敗すると null になります。

で、そのオブジェクト(今回は newBooks )にアクセスすれば、 NullReferenceException になります。

が、 TypeScript では null にならず、下記は NullReferenceException ではなく、エラーも発生しません。

// 本当は Array< Book>
var newBooks = JSON.parse(JSON.stringify(myJson)) as Book;
            
if(newBooks == null){
    console.log("null death 1");
}
if(newBooks === null){
    console.log("null death 2");
}
// undefined. ただしエラーは発生しない.
var name = newBooks.name;

メソッドを呼んだ場合はエラー( undefined )が発生しますが、最初 JSON からの変換に失敗しているのかと確認に時間を費やしてしまいました/(^o^)\

C# と同じように書ける部分がありつつも、違うところは当然あるので、しっかり見分けないとハマりこんでしまいそうです。

次回は fetch の option ( fetch() の第二引数)からスタートの予定。

あと、 DOM 周りも触っておきたいところ。