音楽サーバ"Mopidy"のフロントエンドを作る:07 無限ローディングを組み込む
音楽サーバ"Mopidy"のフロントエンド「Mopidy.Finder」が出来るまで、第7回です。
今回は、Vueのプラグインを使った無限ローディングの実装を追っていきます。
Vue-infinite-loadingなるもの
まあ、誰か作ってるんちゃうの?的にぐぐったところ。
名前もそのまんま、Vue-infinite-loading
なるライブラリが散見されますね。
これが定番なのかな?
実装例の記事も、とてもシンプルでした。
pdo99.hatenablog.com
www.kabanoki.net
公式ページはこちら。
英字と中文、とあるので、中国の方でしょうか。
Get Startedのエフェクトかっこいい!
peachscript.github.io
ということで、これを導入してみることにしました!
インストール
公式ページにあるように、npmから貰って来ます。
# npm install vue-infinite-loading
TypeScript用型定義は、パッケージに含まれていました。
いやあ、最近は便利でいい!
書いてみる
その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
コンポーネントが「見えるようになった」時点で実行されます。
読み込みの流れとしては、下記のようなものです。
- ArtistStoreから新しいArtist配列を受け取る
- Artist配列を結合
- 読み込み完了判定、続きがある場合は
$state
のloaded
メソッドを、完了した場合は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; } // -- 中略 --- }
ああ、やっと実装らしい実装にたどり着きました!
中身の流れとしては、以下のようなものです。
- ジャンルIDの配列がある場合は、ジャンルIDでアーティストを絞り込んでおく。
- 合計のArtist数をとっておく。
- アーティストを名前順に並べる。
- 渡し値ページ番号に該当するアーティスト配列を取ってくる。
- 戻り値に整形して返す。
以前の記事に挙がっていた、EF-Coreで作ったデータベースから値を取ってきています。
だんだんと繋がりが出来てきました!
UIの動きをためしてみる
このコミットを実行してみると、こんな画面が出てきます。
うへえ。Bootstrap臭ぇ...。
いやいや。そのうち直すんです。
そして、Artistsのリストをスクロールしていくと...
(おわかり頂けただろうか...)
でん、と残りスクロール量が増えます。
EF-CoreもVueの描画も結構早いので、一瞬のうちにアーティストが増えてます。
もうちょっとタメがあった方が、ありがたみがあるってもんですが。
そしてAspCoreの出力ではこんな感じで、SELECT文が実行されていることがわかります。
以上、Vue-Infinite-LoadingとAspCoreを使った無限ローディング実装、でした!