Try .NET Core

.NET Coreを動かした、試した記録を書き残します。

音楽サーバ"Mopidy"のフロントエンドを作る:07 無限ローディングを組み込む

音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第7回です。

今回は、Vueのプラグインを使った無限ローディングの実装を追っていきます。

Vue-infinite-loadingなるもの

まあ、誰か作ってるんちゃうの?的にぐぐったところ。
f:id:try_dot_net_core:20190808161330p:plain

名前もそのまんま、Vue-infinite-loadingなるライブラリが散見されますね。
これが定番なのかな?

実装例の記事も、とてもシンプルでした。
pdo99.hatenablog.com
www.kabanoki.net

公式ページはこちら。
英字と中文、とあるので、中国の方でしょうか。
Get Startedのエフェクトかっこいい!
peachscript.github.io

ということで、これを導入してみることにしました!

インストール

公式ページにあるように、npmから貰って来ます。

# npm install vue-infinite-loading

TypeScript用型定義は、パッケージに含まれていました。
いやあ、最近は便利でいい!
f:id:try_dot_net_core:20190808163404p:plain

書いてみる

そのVue-Infinite-Loadingの導入を試していたのが、この頃のコミットです。

音楽プレイヤーとしては、ジャンルはともかくアーティストやアルバムは、どれだけの数があるか分かりゃしません。

ということで、アーティストのリストに組み込んでみたのがこちら。
src/ts/Views/Finders/ArtistList.ts:

import ViewBase from '../Bases/ViewBase';
import Component from 'vue-class-component';
import ArtistStore from '../../Models/Artists/ArtistStore';
import SelectionItem from '../Shared/SelectionItem';
import Artist from 'src/ts/Models/Artists/Artist';
import Vue from 'vue';
import { default as InfiniteLoading, StateChanger } from 'vue-infinite-loading';

Vue.use(InfiniteLoading);

// -- 一部省略 --
@Component({
    template: `<div class="col-md-2 h-100">
    <div class="card h-100">
        <div class="card-header with-border bg-info">
            <h3 class="card-title">Artists</h3>
        </div>
        <div class="card-body list-scrollable">
            <ul class="nav nav-pills h-100 d-flex flex-column flex-nowrap">
                <template v-for="entity in entities">
                <selection-item
                    ref="Items"
                    v-bind:entity="entity" />
                </template>
                <!-- ここに無限ローディングコンポーネント -->
                <infinite-loading @infinite="OnInfinite"></infinite-loading>
            </ul>
        </div>
    </div>
</div>`,
    components: {
        'selection-item': SelectionItem
    }
})
export default class ArtistList extends ViewBase {

    private store: ArtistStore = new ArtistStore();
    private page: number = 1;
    private genreIds: number[] = [];
    private entities: Artist[] = [];

    // ここで読み込み
    private async OnInfinite($state: StateChanger): Promise<boolean> {
        var result = await this.store.GetList(this.genreIds, this.page);

        if (0 < result.ResultList.length)
            this.entities = this.entities.concat(result.ResultList);

        if (this.entities.length < result.TotalLength) {
            $state.loaded();
            this.page++;
        } else {
            $state.complete();
        }

        return true;
    }
}

まだ事例を読みつつトライ&エラーな状態だったため、Plugin APIで書いてます。
コンポーネントとして導入する場合は、@Componentの引数でこのようにします。

components: {
    'selection-item': SelectionItem,
    'infinite-loading': InfiniteLoading
}

またテンプレートでは、スクロールする外枠を指定しておくと、潰しが効いて便利です。

<infinite-loading
    @infinite="OnInfinite"
    force-use-infinite-wrapper=".inner-scrollbox.artist-list"
    ref="InfiniteLoading" />

それから、肝心の読み込み機能。
テンプレートでハンドルしている@infinite="OnInfinite"ですが、それはこのinfinite-loadingコンポーネントが「見えるようになった」時点で実行されます。

読み込みの流れとしては、下記のようなものです。

  1. ArtistStoreから新しいArtist配列を受け取る
  2. Artist配列を結合
  3. 読み込み完了判定、続きがある場合は$stateloadedメソッドを、完了した場合はcompleteメソッドを呼ぶ。

なんとも単純に出来るもんですね!

Artistを貰ってくるところ

では、ArtistStoreの中身はどんなだ、というと。
src/ts/Models/Artists/ArtistStore.ts:

import { default as StoreBase, PagenatedResult } from '../Bases/StoreBase';
import Artist from './Artist';

export default class ArtistStore extends StoreBase<Artist> {

    public async GetList(genreIds: number[], page: number): Promise<PagenatedResult<Artist>> {
        const result = await this.QueryGet('Artist/GetPagenatedList', {
            genreIds: genreIds,
            page: page
        });

        if (!result.Succeeded) {
            console.error(result.Errors);
            throw new Error('Unexpected Error on ApiQuery');
        }

        return result.Result as PagenatedResult<Artist>;
    }
}

APIArtist/GetPagenatedListに対して、GETクエリしています。
引数として、選択されたジャンルを示す「ジャンルID」の配列、そして要求する「ページ番号」を渡しています。

見た目を「無限ロード」と表現するとはいえ、一皮むけば単なるページング、というのは皆さまご承知のとおりです。

サーバサイドはこんなかんじ

GETクエリを受け取る、AspCore側のArtistコントローラがこちらです。
aspCore/Controllers/ArtistController.cs:

[Produces("application/json")]
[Route("Artist")]
public class ArtistController : Controller
{
    // -- 中略 --
    [HttpGet("GetPagenatedList")]
    public XhrResponse GetPagenatedList(
        [FromQuery] int[] genreIds,
        [FromQuery] int? page,
        [FromServices] ArtistStore store
    )
    {
        var result = store.GetPagenatedList(genreIds, page);
        return XhrResponseFactory.CreateSucceeded(result);
    }
}

まだまだ、パラメータのバケツリレー状態が続いていますね...。
こんどはAspCore上の、ArtistStoreに引数を渡しています。

ではその、AspCore上のArtistStoreがどんな実装か、というと。
aspCore/Models/Artists/ArtistStore.cs:

public class ArtistStore : PagenagedStoreBase<Artist>
{
    // -- 中略 ---
    public ArtistStore([FromServices] Dbc dbc) : base(dbc)
    {
    }

    public PagenatedResult GetPagenatedList(int[] genreIds, int? page)
    {
        var query = this.Dbc.GetArtistQuery();

        if (genreIds != null && 0 < genreIds.Length)
            query = query
                .Where(e => e.GenreArtists.Any(e2 => genreIds.Contains(e2.GenreId)));

        var totalLength = query.Count();

        query = query.OrderBy(e => e.Name);

        if (page != null)
        {
            query = query
                .Skip(((int)page - 1) * this.PageLength)
                .Take(this.PageLength);
        }

        var list = query.ToArray();

        var result = new PagenatedResult()
        {
            TotalLength = totalLength,
            ResultLength = list.Length,
            ResultPage = page,
            ResultList = list
        };

        return result;
    }
    // -- 中略 ---
}

ああ、やっと実装らしい実装にたどり着きました!
中身の流れとしては、以下のようなものです。

  1. ジャンルIDの配列がある場合は、ジャンルIDでアーティストを絞り込んでおく。
  2. 合計のArtist数をとっておく。
  3. アーティストを名前順に並べる。
  4. 渡し値ページ番号に該当するアーティスト配列を取ってくる。
  5. 戻り値に整形して返す。

以前の記事に挙がっていた、EF-Coreで作ったデータベースから値を取ってきています。
だんだんと繋がりが出来てきました!

UIの動きをためしてみる

このコミットを実行してみると、こんな画面が出てきます。
f:id:try_dot_net_core:20190808180445p:plain うへえ。Bootstrap臭ぇ...。
いやいや。そのうち直すんです。

そして、Artistsのリストをスクロールしていくと...
f:id:try_dot_net_core:20190808180529p:plain

(おわかり頂けただろうか...)
でん、と残りスクロール量が増えます。
f:id:try_dot_net_core:20190808180614p:plain
EF-CoreもVueの描画も結構早いので、一瞬のうちにアーティストが増えてます。
もうちょっとタメがあった方が、ありがたみがあるってもんですが。

そしてAspCoreの出力ではこんな感じで、SELECT文が実行されていることがわかります。
f:id:try_dot_net_core:20190808181240p:plain

以上、Vue-Infinite-LoadingとAspCoreを使った無限ローディング実装、でした!