[Vue]lazyload+ローダーの制御

# はじめに

- 画像のlazyloadしたかった時につまづいたので覚え書き
- 同じ悩みを持つ人がいらっしゃったら1mmでも参考になれば幸いです。

# 環境

- OS : Windows Home 10
- ブラウザ : Chrome
- 言語 : Vue(2.x)


# やりたいこと

以下2点がやりたいこと

- lazyloadを使った画像の表示
- lazyload中によくあるローダー（クルクル回るやつ）を表示させたい

## 状況
* S3などのクラウドにある画像データをダウンロードストリーミングしているとブラウザの表示までにかなり時間を要する
* これを「必要な分だけ表示する」ためにlazyloadを導入した

※「lazyloadとは？」という状態から始めたので、参考にさせていただいた記事を引用します。

- lazyload

[https://paralux.co.jp/blog/358](https://paralux.co.jp/blog/358)

イメージ画像があってわかりやすかったです。

※ついでに「こんなやつね」くらいのイメージを捉えるためにローダーの参考も引用。

- ローダー

[https://projects.lukehaas.me/css-loaders/](https://projects.lukehaas.me/css-loaders/)


# やったこと

画像のローダー表示自体よくある処理なので

「JavaScript lazyload ローダー」

「Vue lazyload ローダー」

等の検索キーワードで検索をしてみた

# 問題点

検索結果に出てこない、、、意外だった。

「lazyloadの導入方法」や「lazyload中に固定画像を表示する方法」はあるが、「＋ローダーの表示」まで至る記事がなかった

# 対策

ということで、自作を決意

## 表示するモノ

表示するモノは以下の通り

- 本画像…lazyloadで読み込みたい画像
- ローダー…よくあるクルクル回るアレ
- ダミー画像…lazyload中に読み込みたい画像。今回はローダーを表示するので透明画像を使用
- エラー画像…読み込み失敗時の画像。よくあるNO IMAGE画像を使用

## 方針

「やりたいこと」に沿って該当ページへの初回アクセス時からの流れを整理すると

- 本画像読み込み中…ローダーを表示（同時にダミー画像を表示）
- 本画像読み込み完了…ローダー非表示→本画像表示
- 本画像読み込み失敗…ローダー非表示→エラー画像表示

## 準備

lazyloadを実装するには自分が調べた限り、３つの方法があった（他にもあったらすいません）

1, JSライブラリ[lazyload](https://github.com/tuupola/lazyload) による制御

2, HTML属性`loading`による制御

3, Vue用ライブラリ[vue-lazyload](https://github.com/hilongjw/vue-lazyload) による制御

今回はVueを使っていることもあり、[vue-lazyload](https://github.com/hilongjw/vue-lazyload) を使用

### 実は、、、

実は当初、 [lazyload](https://github.com/tuupola/lazyload)を最初導入していたんです。

しかし、Vue内で生成する動的コンポーネントとの相性が悪いとの理由で 3, vue-lazyload の使用に至りました。

※尚、1, lazyload でも実装は可能ですが、lazyloadが必要なコンポーネントに都度updatedメソッドの中で`lazyload()`を読み込みしなくてはいけない点で冗長だったので却下しました。

## 実装

以下流れで実装します。

---

1, https://github.com/hilongjw/vue-lazyload のインストール

2, https://github.com/hilongjw/vue-lazyload の設定

3, テンプレートの作成

4, ローダーの作成

5, ローダーの制御

---

ただ、1 ~ 3はvue-lazyloadの基本的な使い方なのでこれができた前提で話を進めます。

※1 ~ 3は[こちら]([https://qiita.com/shin_moto/items/9870b4e450d22fe4f1d5](https://qiita.com/shin_moto/items/9870b4e450d22fe4f1d5))を参考にさせていただきました。

以下が1~3が既に終わった状態です。

前提としてVue-CLIをインストールして新プロジェクトを作成したタイミングから始めています。

---

```app.js
import Vue from 'vue';
import App from './App.vue';
// 追加
import VueLazyload from 'vue-lazyload';
// ※追加（なくてもOK）
import * as image from './config/image';
// 追加
Vue.use(VueLazyload);

// ※追加（なくてもOK）
const dummyURL = image.DUMMY_URL;
const noImageUrl= image.NO_IMAGE_URL;

// 追加
Vue.use(VueLazyload, {
  loading: dummyURL, // ※オプション
  error: noImageUrl, // ※オプション
});

// オプションを書かないのであれば以下
Vue.use(VueLazyload);

new Vue({
  el: 'body',
  components: {
    App
  }
});
```

- vue-lazyloadの設定箇所
- lazyload中とエラー発生時の画像をここで設定できるのは結構うれしい
    - 各コンポーネントにdataプロパティに各々設定しなくていい
- errorやloading以外にもオプションがあったり、オプションを書かずに各コンポーネントで個別でオプションをかけたり、、、詳しくはドキュメント参照


```config/image.js
export const DUMMY_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
export const NO_IMAGE_URL = "https://placehold.jp/30/cccccc/ffffff/300x150.png?text=no-image";
```

- 表示するダミー画像とエラー画像のパスを指定
- `DUMMY_URL`、`NO_IMAGE_URL`のパスは各々以下から取得しています。

[https://png-pixel.com/](https://png-pixel.com/)

[https://placehold.jp/](https://placehold.jp/)

```image-list.component.vue
<template>
  <div>
    <ul>
      <li
        v-for="item in items"
        :key="item"
      >
        <img
          v-lazy="item.imageUrl"
        >
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    images: {
      type: Object,
      default: {},
    },
  },
}
</script>
```
- 画像の表示箇所
- `items`というオブジェクトから`item`単位でループをまわし、`item`から表示したい`imageUrl`を取得
- `items`は親コンポーネントから`props`で受け取っている、という前提
- `v-lazy`を書くだけでロード中もエラー時も本画像も表示できてしまう
    - 実際には内部的に`load`イベント、`error`イベントを検知し`loading`属性の要素をVueで切り替えている制御方法

---

ここまでで1 ~ 3の実装は終わったのでローダーについて実装します。

### 4, ローダーの作成

- これは先ほど掲載した[参考サイト]([https://projects.lukehaas.me/css-loaders/](https://projects.lukehaas.me/css-loaders/))のソースコードを頂戴してきます。
    - 今回はサイトの左上から右に2番目のローダーを表示します。

```loader-component.vue
<template>
  <div
    class="loader"
  />
</template>

<style scoped>
.loader,
.loader:before,
.loader:after {
  border-radius: 50%;
}
.loader {
  color: #ffffff;
  font-size: 11px;
  text-indent: -99999em;
  margin: 55px auto;
  position: relative;
  width: 10em;
  height: 10em;
  box-shadow: inset 0 0 0 1em;
  -webkit-transform: translateZ(0);
  -ms-transform: translateZ(0);
  transform: translateZ(0);
}
.loader:before,
.loader:after {
  position: absolute;
  content: '';
}
.loader:before {
  width: 5.2em;
  height: 10.2em;
  background: #0dc5c1;
  border-radius: 10.2em 0 0 10.2em;
  top: -0.1em;
  left: -0.1em;
  -webkit-transform-origin: 5.1em 5.1em;
  transform-origin: 5.1em 5.1em;
  -webkit-animation: load2 2s infinite ease 1.5s;
  animation: load2 2s infinite ease 1.5s;
}
.loader:after {
  width: 5.2em;
  height: 10.2em;
  background: #0dc5c1;
  border-radius: 0 10.2em 10.2em 0;
  top: -0.1em;
  left: 4.9em;
  -webkit-transform-origin: 0.1em 5.1em;
  transform-origin: 0.1em 5.1em;
  -webkit-animation: load2 2s infinite ease;
  animation: load2 2s infinite ease;
}
@-webkit-keyframes load2 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
@keyframes load2 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(360deg);
    transform: rotate(360deg);
  }
}
</style>
```

同時に、ローダーを表示したい`image-list.component.vue`に取り込んでおく


```image-list.component.vue
...
<script>
export default {
	...
  // 追加
  components: {
		loader-component
  },
}
</script>
```

### 5, ローダーの制御

方法としては`専用プロパティを設定しローダーを表示／非表示をtrue / falseで制御`する形をとりました。要は

- `loading = true` でローダー表示
- `loading = false` でローダー非表示

という整理

なので、まずはdataプロパティにローダーの制御用プロパティ`loading`を設定


```image-list.component.vue
...
<script>
export default {
	...
	// 追加
	data: () {
		return {
			loading: false,
		}
	} 
}
</script>
```

また、`loading`の値によって動的にローダーを表示／非表示を制御するため、ローダーに`loading`を紐づける

```image-list.component.vue
        <img
          v-lazy="item.imageUrl"
        >
		// 追加
		<loader-component
			v-if="loading"
		>
		</loader-component>
```

そして、まずは表示時の制御。

これはページ全体の読み込み時でいいので`mounted()`を追加し、ここでローダーを表示させる

```image-list.component.vue
<script>
export default {
	...
	// 追加
	mounted: () {
		this.loading = true;
	}
	...
}
</script>
```

そして非表示時の制御。

これはVueのloadイベントを発火させて制御します。

### ちょっと困ったこと

ただ、こっから僕が困ったことです。

ローダー表示と同じように以下のように@loadを追加して`onLoad`メソッドを呼び出すようにして

```image-list.component.vue
        <img
          v-lazy="item.imageUrl"
          // 追加
		  @load="onLoad"
        >
		<loader-component
			v-if="loading"
		>
		</loader-component>
```

その際に呼び出すメソッド`onLoad`メソッドでローダーを非表示にすると、

```image-list.component.vue
<script>
export default {
	...
	// 追加
	methods: {
		onLoad(event) {
			this.loading = false;
		},
	}
	...
}
</script>
```

**ダミー画像表示のタイミングで@loadイベントが発火してローダーが非表示になる**という挙動をしました。

#### 原因

原因はonLoadメソッド内でデバックしたらわかったんですが、lazyloadのデフォルトの挙動として@loadイベントは、「ダミー画像の読み込み完了時」と「本画像の読み込み完了時」の**合計２回呼び出される**ことでした。


なので、ブラウザの動きとしては画像の読み込み中に

`ローダーを表示`→`すぐにダミー画像を読み込みローダーが非表示`
（ローダーが一瞬表示→透明画像の表示（何も表示されない）、という状態）

という状態になった、という状況です。

#### 対策

@loadイベントは合計２回呼び出されますが、幸いにも各々**呼び出されるDOMがダミー画像用、本画像用で分かれていた**ことから以下のようにしました。

```image-list.component.vue
<script>
// 追加
import * as image from './config/image';

export default {
	...
	methods: {
		onLoad(event) {
			// 追加
			if (event.target.src !== image.DUMMY_URL) {
				this.loading = false;
			}
		},
	}
	...
}
</script>
```

要は

- １回目の@loadイベント：`event.target.src`はダミー画像用のパス
- ２回目の@loadイベント：`event.target.src`は本画像画像用のパス

となり、**ダミー画像読み込み完了時のイベントは**src属性がDUMMY_URL(config/image.jsで設定済)であるのでこれを**スルーし**、**本画像読み込み完了時にloadingをfalseにする**ことでローダーを非表示にする、という制御が可能になりました。

## 結果

これで当初の予定である、

---

- 本画像読み込み中…ローダーを表示（内部的にはダミー画像を表示）
- 本画像読み込み完了…ローダー非表示→本画像表示
- 本画像読み込み失敗…ローダー非表示→エラー画像表示

---

これを実現出来ました。

# 補足

いくつか補足です。

## エラー時の挙動について

謎な部分がありました。

本画像が読み込めなくなったエラー発生時、以下の`noImageUrl`を表示する設定にしており、ここまでの実装でエラー画像の表示は機能はしていた。

```app.js
...
Vue.use(VueLazyload, {
  loading: dummyURL,
  error: noImageUrl, // エラー画像
});
...
```

なので、`@error`でエラーイベントの取得ができるだろうと思ったので`@error`で`onError`メソッドを呼び出すようにして

```image-list.component.vue
        <img
          v-lazy="item.imageUrl"
		  @load="onLoad"
		  // 追加
		  @error="onError"
        >
		<loader-component
			v-if="loading"
		>
		</loader-component>
```

`onError`メソッドの中で`console.log`を構えていたが、、

```image-list.component.vue
<script>
export default {
	...
	methods: {
		// 追加
		onError(event) {
			console.log(evevnt.target.src);
		},
	}
	...
}
</script>
```

結果として、`onError`メソッドに処理が入らず、正常処理と同様に`onLoad`メソッドが2回呼ばれました。

なお、冒頭に話したJSライブラリ lazyload の使用時はちゃんと`@error`でイベントが取得できたので、これはvue-lazyloadの仕様かな、と思います。

素直に`@error`イベントを読んでくれないのはちょっと扱いにくい点かな、、、と思いました。

## lazyloadでsrc属性を書かなければ挙動は問題ない

JSライブラリ lazyload 使用時に、`img`タグの`src`属性を書かずに、`data-src`属性のみ書いてもブラウザの挙動上は問題ないので、ここで妥協しようとも考えました。

ただ、[この記事]([https://alaki.co.jp/blog/?p=2527](https://alaki.co.jp/blog/?p=2527))によると

> lazyload.jsにはダミー画像を自動的に生成するプログラムが組み込まれている

とあり、`src`がなくとも実装できますが

> imgタグにsrc属性を指定しないとW3Cのチェックに引っかかってしまう

とのことでした。

実際に「W3Cのチェックに引っかかってしまう」とはなんなのか検索しましたが、特に関連記事が見つからずでした。

とはいえ、不確定要素のあるモノを実装するのは怖いのと、他に選択肢があったので、今回は「`src`を実装しない」という実装方法は却下しました。

# おわりに

「lazyloadとは何ぞ？」から始まりましたが、まだ慣れていないVueの処理の流れを理解し、またイベント内での処理を書くことで融通の利く処理が書けるんだな、と色々と個人的に学びが多い作業でした。

正直、記事を書いていて「そこまで大した処理ではないからわざわざ記事になっていないだけかな。。。」と思いましたが、どうせならと思い投稿させて頂きました。

「このほうがいいんじゃないか」等々、忌憚のないご意見お待ちしております。

読んでいただいた方へ、最後までお読みいただきありがとうございました。