22
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BlazorAdvent Calendar 2019

Day 23

Blazor(client-side版)で本の情報取得サイトを作成する

Last updated at Posted at 2019-12-22

概要

Blazorを使ってカメラから本のバーコードを読み取り、情報を取得して表示する簡単なWEBアプリを作成したので、簡単に紹介します。

Demo
(※注意:iOS13のSafariで開くとLoading画面で固まってしまう場合がありました。iOS12環境だと動いたので、最新のBlazorでiOS13の対応が入っているはずですが、まだ未完全なのかもしれません。)

アプリのイメージ

下記のように、カメラで本のバーコードをスキャンすることで本の情報と中古価格を調べることができます。

book.gif

前提

.NET Core SDK 3.1.100
Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2
Visual Studio 2019 16.4.0

実装

実装していく中で、苦労した部分のメモです。

カメラの起動とバーコード認識

カメラの動画からバーコードを認識させる方法として、デバイスを操作するためにJavascriptを使っています。

当初は、videoタグを使って1から実装しようと思っていましたが、QuaggaJSといった、今回の実装したい目的に合致するライブラリが見つかったのでそちらを利用しました。

実装においては下記の記事が参考になりました。
https://qiita.com/kira_puka/items/03dc5c01bbbaffdb6e83

BlazorのレイヤとしてはJavascirptの相互連携機能を使用して、

  1. JS側でのカメラの起動とバーコードの検知のための処理を呼び出す。(引数ではBlazorコンポーネントの参照を渡す)
  2. バーコードが検知されたら、引数で渡したBlazorコンポーネントの参照を経由して検知時のメソッドを呼び出す。(JSInvokable属性の付与されたC#メソッドをJS側から呼び出す)
@inject IJSRuntime jsRuntime;

    public async Task OpenDialogAsync()
    {
        try
        {
            // カメラを起動して画面のキャプチャを開始するJS側の関数を呼び出し
            await jsRuntime.InvokeVoidAsync("barcodeScan.startCapture", DotNetObjectReference.Create(this));
        }
        catch (Exception e)
        {
            // 例外発生時にはカメラを止めて失敗イベントを発火
            await StopCapture();
            await OnFailed.InvokeAsync(e);
        }
    }


    [JSInvokable]
    public async Task CodeDetected(string code)
    {
        // バーコード検出時の処理
    }



window.barcodeScan = {
    startCapture: function(dotNetObj) {
      //ここでdotNetObjの参照の保持とQuaggaJSによるキャプチャを行う
    },
    // QuaggaJSでバーコードが検知された時に呼び出される処理 
    onDetected: function (success) {
    const isbn = success.codeResult.code;
    if (isbn.startsWith("978")) {
        if (dotNetObj) {
            // ISBNバーコードが検知されたので検知したことをBlazor側に通知
            dotNetObj.invokeMethod('CodeDetected', isbn);
        }
    }
}

WEBスクレイピング

ISBNコードから書籍情報や中古価格を取得する際に、WEB-APIが提供されているものはHTTPClientを使ってJSON情報などを取得してパースすればよいですが、無いものに関してはHTMLから情報を取得する必要があります。

一般的には、サーバー側の処理としてスクレイピングを行いますが、今回はBlzorのクライアント上から実施を行いました。

Blazorのクライアントサイド版はブラウザ上で動くため、セキュリティ的な制限なども多く、サーバ側でスクレイピングするのと比較して下記の点で苦労しました。

CORS対策

ブラウザからHTTPをリクエストを投げる際に、WEB-APIではないWEBサイトに対して投げるとそのままでは通らないため、プロキシが必要となります。
今回は下記のようなCORS用のサービスを使用しました。
https://cors-anywhere.herokuapp.com/

https://cors-anywhere.herokuapp.com/{本来のリクエストしたいURL}

とする事で、アクセスが可能になります。

S-JISエンコードのWEBサイト

S-JISエンコードのWEBサイトをHTTPClientで読み込むと、下記のようなエラーが発生しました。
Windows-31J(S-JIS)のエンコードに対応していないため発生しているエラーのようです。


System.InvalidOperationException:
 The character set provided in ContentType is invalid. Cannot read content as string using an invalid character set.
 ---> System.ArgumentException: 'Windows-31J' is not a supported encoding name.
 For information on defining a custom encoding,
 see the documentation for the Encoding.RegisterProvider method. 

.NET CoreではデフォルトではS-JISに対応していないようなので、下記の記事などを参照して、対応するパッケージを入れてエンコード指定で読み込んでみましたが、残念ながら解消できませんでした。
https://qiita.com/sugasaki/items/0639ea9ca07f1ba7a9e0

最終的にはJavascript側のFetchAPIを呼び出して、S-JISエンコードした文字列をBlazorに返す方法で対応しました。

JSでの実装は下記を参照しました。
http://var.blog.jp/archives/79094563.html


window.fetchHttp = {
    getHtmlAsync: async function (url, isEncodeSJIS) {
        const res = await fetch(url, {
            method: "GET",
            mode: "cors",
            headers: {
                "Content-Type": "text/html",
            },
        });
        if (!isEncodeSJIS) {
            // S-JIS出ない場合はそのまま返す
            return await res.text();
        } else {
            // S-JISの場合エンコードしてから返す
            const blob = await res.blob()
            const fr = new FileReader();
            return await new Promise((resolve, reject) => {
                fr.onload = eve => {
                    resolve(fr.result);
                }
                fr.onerror = err => reject(err);
                fr.readAsText(blob, "Shift_JIS");
            });
        }
    }
};

上記のJS関数をBlazorから呼び出すことでHTMLの読み込みができました。
JS側のasync/await関数を普通に呼び出せるのは便利ですね。


    public abstract class FetchClient
    {
        private readonly IJSRuntime _JSRuntime;
        public FetchClient(IJSRuntime jSRuntime)
        {
            _JSRuntime = jSRuntime;
        }

        protected virtual async Task<string> GetHtmlAsync(string url, bool isEncodeSJIS = false)
        {
            return await _JSRuntime.InvokeAsync<string>("fetchHttp.getHtmlAsync", new object[] { url, isEncodeSJIS }).ConfigureAwait(false);
        }
    }

HTMLの読み込みは、AngleSharpを使って実装しました。.NET Core対応のライブラリが普通に使えるのがBlazorの良いところですね。


        private async Task<ProductInfo> GetProductInfoFromHtml(string html)
        {
            var pinfo = new ProductInfo();
            var parser = new HtmlParser();
            var htmlDocument = await parser.ParseDocumentAsync(html);

            // get elem
            var elem = htmlDocument.QuerySelector("div.hoge");
           // do something
        }

コンポーネントにおけるスコープ

APIなどの外部から取得した情報を表示するUIコンポーネントに対して、下記のどちらの手法を取るか迷いました。

  • UIコンポーネント内に、APIのコール処理を持たせるか?
  • UIコンポーネントはデータの入れ物として扱い、配置する上位コンポーネントでAPIをコールして、結果を渡すか?

今回は結局、コンポーネントが外部と連携する事無く独立するため、そちらのほうが良いと判断して、前者を採用しました。
(この辺りはNuxtやVueにおいても、Vuexにとコンポーネント側のどちらに持たせるかで迷ってしまう点と似ていますね。)


<MatCard class="mat-card">
    @if (IsLoading)
    {
        <MatCardContent>
            //API実行中のスピナーなどを表示
        </MatCardContent>
    }
    else if (BookInfo != null)
    {
        <MatCardContent>
            //BookInfo の情報を表示
        </MatCardContent>
    }
</MatCard>

@{
    [Inject]
    IBookGet BookInfoClient { get; set; }
    bool IsLoading { get; set; } = false;
    string ErrorMessage { get; set; }
    BookInfo BookInfo { get; set; }

    public async Task DisplayInfoAsync(string isbn13)
    {
        try
        {
            BookInfo = null;
            IsLoading = true;
            base.StateHasChanged();
            // APIを呼び出して情報を取得
            BookInfo = await BookInfoClient.GetBookInfoAsync(isbn13);
        }
        catch (Exception)
        {
            ErrorMessage = "情報取得に失敗しました。";
        }
        finally
        {
            IsLoading = false;
            base.StateHasChanged();
        }
    }
}

呼び出し元

<BookInfoBox @ref="@bookInfoBox"></BookInfoBox>
@{
    BookInfoBox bookInfoBox;

    public async Task OpenDialogAsync(string isbn13)
    {
        await bookInfoBox.DisplayInfoAsync(isbn13);
    }
}

所感

ということで簡単にですが、簡単なBlazorを使ったサービスを作った際の話を紹介しました。
Blazor以外の話が多くなってしまいましたが、改めてUIやロジック周りはC#を使って書くことができるのは楽で良いと感じました。
前述のAngleSharpのように.NETのライブラリをブラウザ上で使用できる点もBlazorならでは便利な点だと思います。

ただ、VisualStudio上でデバッグができないのは少し不便でした。
現時点でも一応下記に書かれた手順で可能なようでうが、近い将来はもっとお手軽に実行できるようになるみたいなので期待しています。
https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-core-3-1/

少し変わったことをやろうとすると、Javascriptを使わないといけない部分が多く、色々と苦戦しましたが、JSを使えば何とかなるといった事もわかりました。

Blazored.LocalStorageなど、便利なOSSのライブラリなども内部ではJSを使っていたりするので、JSコードをラップしたライブラリが色々と増えて普及すれば、Blazorを使う敷居も下がりそうですね。

@jsakamotoさんが公開している下記のようなBlazorの記事を参考に、何か良いアイディアがあればいずれ個人的にも何かしらの便利なライブラリの作成も試してみたいと思います。
https://qiita.com/jsakamoto/items/a68a62c5e0c13a827da0
https://qiita.com/jsakamoto/items/4c4520a0b73d3f30d95a

22
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?