OnsenUIのv-ons-lazy-repeatを実用的に使って見る

はたです。
最近monacaを利用してハイブリットアプリを開発してます。
jsに関してはVue.jsですね。Angular、Reactなど選択肢はありますが個人的にVue.jsが好きです。はい。
今回は開発の中でv-ons-lazy-repeatを利用する部分があったのでその実装メモになります。

## v-ons-lazy-repeat

https://ja.onsen.io/v2/api/vue/v-ons-lazy-repeat.html

>このコンポーネント内で描画されるアイテムのDOM要素の読み込みは、画面に見えそうになった時まで自動的に遅延され、 画面から見えなくなった場合にはその要素は動的にアンロードされます。 このコンポーネントを使うことで、パフォーマンスを劣化させること無しに巨大な数の要素を描画できます。

アプリでよく見かける無限スクロールみたいな感じですね。
自前で軽く実装するのは簡単ですが、パフォーマンスを最大限考慮した実装となるとなかなか難しいものです。OnsenUIのv-ons-lazy-repeatは引用の通りDOM要素のロード遅延やアンロードをパフォーマンスのことを考えいい感じ行ってくれるとても便利なコンポーネントです。

## 実装サンプル

```
<template id="main">
  <v-ons-page>
    <v-ons-toolbar>
      <div class="center">Lazy Repeat</div>
      <div class="right">
        <v-ons-toolbar-button @click="$emit('refresh')">
          Refresh
      </v-ons-toolbar-button></div>
    </v-ons-toolbar>

    <v-ons-list>
      <v-ons-lazy-repeat
        :render-item="renderItem"
        :length="1000"
      >
      </v-ons-lazy-repeat>
    </v-ons-list>

  </v-ons-page>
</template>

<div id="app"></div>
```

```
new Vue({
  el: '#app',
  template: '#main',
  data() {
    return {
      renderItem:
        i => new Vue({
          template: `
            <v-ons-list-item :key="index">
              #{{ index }}
            </v-ons-list-item>
          `,
          data() {
            return {
              index: i
            };
          }
        })
    };
  }
});
```

なるほど。
Vueインスタンスを生成する関数と生成数をコンポーネントに渡せばv-ons-lazy-repeatのほうでうまくコントロールしてくれるわけです。

ですがこれはあくまで実装サンプルです。
データ内容やサイズ数が固定であり実用的ではありません。

多くのユースケースでは

- データはajaxなど非同期で取得する
- データのサイズは取得したデータ数に応じるので可変である

つまりv-ons-lazy-repeatに渡すデータは可変データであり
固定値で書かれているサンプル実装をうまく利用して値を動的に渡せるように書き換えてあげる必要があります。

## 実用的な実装

下記のケースを想定して実装してみます。

### やりたいこと

- アプリのホームからカードのリストを表示させる

### コンポーネント
- カードの表示にはv-ons-lazy-repeatを利用する
- ホームはHomeというコンポーネントにする
- カードのリストはItemsというコンポーネントにする
- カードはv-ons-cardを利用する

(Cardsにしないのはv-ons-card以外のコンテンツを考慮しています）

### 表示させるデータ

- サーバサイドのAPI経由で非同期に取得する
- APIから結果データはキャッシュできるようにする

### 状態管理

- vuexを利用する

ある程度責務に応じたコンポーネントは作成し、状態管理はVuexを利用します。
ItemsコンポーネントはHomeコンポーネント以外からも利用されることを考え、再利用できる形にします。

```
vuex/types.js
vuex/store.js
mixin.js
pages/Items.vue
Home.vue
```

```vuex/types.js
export const FETCH_ITEMS = 'FETCH_ITEMS'
```

```vuex/store.js
import {
  FETCH_ITEMS
} from './types'

import axios from 'axios'
import { setupCache } from 'axios-cache-adapter'

const cache = setupCache({
  maxAge: 3600
})
const api = axios.create({
  adapter: cache.adapter
})

function fetch_items(url) {
  // エラー処理は省きます
  return api({url: url, method: 'get'}).then((response) => {
    if (response.status == 200) {
      return response.data
    }
    return []
  })
}

export default {
  modules: {
    items: {
      strict: true,
      namespaced: true,
      state: {
        home: []
      },
      getters: {
        home: state => state.home
      },
      actions: {
        async [FETCH_ITEMS]({ commit }, type) {
          let result = await fetch_items('APIのURL')
          commit(FETCH_ITEMS, { items: result.items, type: type })
        }
      },
      mutations: {
        [FETCH_ITEMS](state, data) {
          state[data.type] = data.items
        }
      }
    }
  }
}
```

```mixin.js
import Items from './pages/Items'

export default {
  components: {
    'items': Items
  },
  computed: {
    exists() {
      this.items.length ? true : false
    }
  }
}
```

```Home.vue
<template>
<v-ons-page>
  <items v-if="exists" :size="items.length">
    <template slot-scope="slotProps">
      <v-ons-card>
        <div class="title">{{items[slotProps.index].name}}</div>
       </v-ons-card>
    </template>
  </items>
</v-ons-page>
</template>

<script>
import { mapActions, mapGetters } from "vuex"
import { FETCH_ITEMS } from "./vuex/types"
import mixin from "./mixin"

export default {
  mixins: [mixin],
  computed: {
    ...mapGetters("items", { items: "home" })
  },
  methods: {
    ...mapActions("items", [FETCH_ITEMS])
  },
  created() {
    this[FETCH_ITEMS]("home")
  }
}
</script>
```

```pages/Items.vue
<template>
<v-ons-page>
  <v-ons-list>
    <v-ons-lazy-repeat
      :render-item="renderItem"
      :length="size"
    >
    </v-ons-lazy-repeat>
  </v-ons-list>
</v-ons-page>
</template>

<script>
import Vue from "vue"

export default {
  props: ["size"],
  data() {
    return {
      renderItem: i => {
        let _this = this
        return new Vue({
          render: createElement => {
            return createElement(
              "div",
              [
                this.$scopedSlots.default({
                  index: i
                })
              ],
              _this.$scopedSlots.default
            )
          }
        })
      }
    }
  }
}
</script>
```

### 解説

#### v-ons-lazy-repeat

```
      renderItem: i => {
        let _this = this
        return new Vue({
          render: createElement => {
            return createElement(
              "div",
              [
                this.$scopedSlots.default({
                  index: i
                })
              ],
              _this.$scopedSlots.default
            )
          }
        })
      }
```

v-ons-lazy-repeatのrenderItem関数は上記のようになっています。
再利用のことを考えrenderItem内にはコンポーネントの詳細な生成は記述していません。
詳細な生成は親コンポーネントであるHome.vueにスロットを利用してここに注入しています。

https://jp.vuejs.org/v2/guide/components-slots.html

例えばFavorite.vueなど考えた時に、v-ons-lazy-repeatで生成したいコンテンツ内容はHome.vueとは違ってくると思います。その違いは親コンポーネントで定義し、子のコンポーネントはv-ons-lazy-repeatのみに依存させれば再利用性はアップします。
またv-ons-lazy-repeat内のindexは参照したいデータの配列の添字です。

```
    <template slot-scope="slotProps">
      <v-ons-card>
        <div class="title">{{items[slotProps.index].name}}</div>
       </v-ons-card>
    </template>
```

スコープ付きスロットを利用して子コンポーネントの値(配列の添字)を親コンポーネントから参照できるようにしています。

#### vuex/mixin

https://vuex.vuejs.org/ja/guide/state.html
https://vuex.vuejs.org/ja/guide/getters.html
https://vuex.vuejs.org/ja/guide/mutations.html
https://vuex.vuejs.org/ja/guide/actions.html

上記を参考に、vuexのガイドに基本従っています。
また、共通したい処理はmixinに定義しています。共通したい処理が明確な場合mixinさせたほうが無駄なロジックを書かずにすみます。

```Home.vue
  computed: {
    ...mapGetters("items", { items: "home" })
  },
  methods: {
    ...mapActions("items", [FETCH_ITEMS])
  },
  created() {
    this[FETCH_ITEMS]("home")
  }
```

スプレッド演算子とmapActionsにより

```
...mapActions("items", [FETCH_ITEMS])
```

 `this.FETCH_ITEMS()` を `this.$store.dispatch('items/FETCH_ITEMS')` にマッピングしています。

computedも同じくマッピングさせています。

```
...mapGetters("items", { items: "home" })
```

`this.items` を `this.$store.getters.items.home` にマッピングですね。
それぞれ第一引数にはnamespaceのitemsを指定しています。

computedなのでstoreの値が更新されればコンポーネント側のthis.itemsにも反映されます。

そして実際のデータ取得のトリガーになるのは

```
  created() {
    this[FETCH_ITEMS]("home")
  }
```
になります。
マッピングされたアクションを経由してデータ取得を行います。

```vuex/store.js
      actions: {
        async [FETCH_ITEMS]({ commit }, type) {
          let result = await fetch_items('APIのURL')
          commit(FETCH_ITEMS, { items: result.items, type: type })
        }
      },
      mutations: {
        [FETCH_ITEMS](state, data) {
          state[data.type] = data.items
        }
      }
```

処理の見通しをよくするためにasync/awaitを利用しています。
vuexのお作法では非同期処理はアクションで行い、ステートの更新はミューテーション経由で行います。
typesとしてactionsやmutationsの関数名を外だし定数化して、アプリケーション全体で何のミューテーション/アクションが可能か一目で理解できるようにします。

API通信には定番のaxiosを利用し、
キャッシュに関してはaxios-cache-adapterを利用しています。

### おわりに

v-ons-lazy-repeatを利用してコンポーネントや周辺の実装を実用的にしてみました。

- 親コンポーネントからの注入を意識し、スロットを利用する。
- 状態管理のVuexを利用しデータフローを単方向にする

今回はOnseUIのv-ons-lazy-repeatのお話でしたが少し範囲を広げてVuexのことまで紹介しました。

機能の追加やコンポーネント数が増えれば実装や管理コストが増大します。
そのコストを局所化し抑えるためには再利用や状態管理を考え実装しなければいけません。

幸いVue.jsの周辺はドキュメントが豊富で参考になるベストプラクティスも多くあります。
それらをチェックしてよりよいハイブリットライフを過ごしたいですね。