はじめに
- 画像のlazyloadしたかった時につまづいたので覚え書き
- 同じ悩みを持つ人がいらっしゃったら1mmでも参考になれば幸いです。
環境
- OS : Windows Home 10
- ブラウザ : Chrome
- 言語 : Vue(2.x)
やりたいこと
以下2点がやりたいこと
- lazyloadを使った画像の表示
- lazyload中によくあるローダー(クルクル回るやつ)を表示させたい
状況
- S3などのクラウドにある画像データをダウンロードストリーミングしているとブラウザの表示までにかなり時間を要する
- これを「必要な分だけ表示する」ためにlazyloadを導入した
※「lazyloadとは?」という状態から始めたので、参考にさせていただいた記事を引用します。
- lazyload
https://paralux.co.jp/blog/358
イメージ画像があってわかりやすかったです。
※ついでに「こんなやつね」くらいのイメージを捉えるためにローダーの参考も引用。
- ローダー
https://projects.lukehaas.me/css-loaders/
やったこと
画像のローダー表示自体よくある処理なので
「JavaScript lazyload ローダー」
「Vue lazyload ローダー」
等の検索キーワードで検索をしてみた
問題点
検索結果に出てこない、、、意外だった。
「lazyloadの導入方法」や「lazyload中に固定画像を表示する方法」はあるが、「+ローダーの表示」まで至る記事がなかった
対策
ということで、自作を決意
表示するモノ
表示するモノは以下の通り
- 本画像…lazyloadで読み込みたい画像
- ローダー…よくあるクルクル回るアレ
- ダミー画像…lazyload中に読み込みたい画像。今回はローダーを表示するので透明画像を使用
- エラー画像…読み込み失敗時の画像。よくあるNO IMAGE画像を使用
方針
「やりたいこと」に沿って該当ページへの初回アクセス時からの流れを整理すると
- 本画像読み込み中…ローダーを表示(同時にダミー画像を表示)
- 本画像読み込み完了…ローダー非表示→本画像表示
- 本画像読み込み失敗…ローダー非表示→エラー画像表示
準備
lazyloadを実装するには自分が調べた限り、3つの方法があった(他にもあったらすいません)
1, JSライブラリlazyload による制御
2, HTML属性loading
による制御
3, Vue用ライブラリvue-lazyload による制御
今回はVueを使っていることもあり、vue-lazyload を使用
実は、、、
実は当初、 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はこちらを参考にさせていただきました。
以下が1~3が既に終わった状態です。
前提としてVue-CLIをインストールして新プロジェクトを作成したタイミングから始めています。
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以外にもオプションがあったり、オプションを書かずに各コンポーネントで個別でオプションをかけたり、、、詳しくはドキュメント参照
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
のパスは各々以下から取得しています。
<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, ローダーの作成
- これは先ほど掲載した参考サイトのソースコードを頂戴してきます。
- 今回はサイトの左上から右に2番目のローダーを表示します。
<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
に取り込んでおく
...
<script>
export default {
...
// 追加
components: {
loader-component
},
}
</script>
5, ローダーの制御
方法としては専用プロパティを設定しローダーを表示/非表示をtrue / falseで制御
する形をとりました。要は
-
loading = true
でローダー表示 -
loading = false
でローダー非表示
という整理
なので、まずはdataプロパティにローダーの制御用プロパティloading
を設定
...
<script>
export default {
...
// 追加
data: () {
return {
loading: false,
}
}
}
</script>
また、loading
の値によって動的にローダーを表示/非表示を制御するため、ローダーにloading
を紐づける
<img
v-lazy="item.imageUrl"
>
// 追加
<loader-component
v-if="loading"
>
</loader-component>
そして、まずは表示時の制御。
これはページ全体の読み込み時でいいのでmounted()
を追加し、ここでローダーを表示させる
<script>
export default {
...
// 追加
mounted: () {
this.loading = true;
}
...
}
</script>
そして非表示時の制御。
これはVueのloadイベントを発火させて制御します。
ちょっと困ったこと
ただ、こっから僕が困ったことです。
ローダー表示と同じように以下のように@loadを追加してonLoad
メソッドを呼び出すようにして
<img
v-lazy="item.imageUrl"
// 追加
@load="onLoad"
>
<loader-component
v-if="loading"
>
</loader-component>
その際に呼び出すメソッドonLoad
メソッドでローダーを非表示にすると、
<script>
export default {
...
// 追加
methods: {
onLoad(event) {
this.loading = false;
},
}
...
}
</script>
ダミー画像表示のタイミングで@loadイベントが発火してローダーが非表示になるという挙動をしました。
原因
原因はonLoadメソッド内でデバックしたらわかったんですが、lazyloadのデフォルトの挙動として@loadイベントは、「ダミー画像の読み込み完了時」と「本画像の読み込み完了時」の合計2回呼び出されることでした。
なので、ブラウザの動きとしては画像の読み込み中に
ローダーを表示
→すぐにダミー画像を読み込みローダーが非表示
(ローダーが一瞬表示→透明画像の表示(何も表示されない)、という状態)
という状態になった、という状況です。
対策
@loadイベントは合計2回呼び出されますが、幸いにも各々呼び出されるDOMがダミー画像用、本画像用で分かれていたことから以下のようにしました。
<script>
// 追加
import * as image from './config/image';
export default {
...
methods: {
onLoad(event) {
// 追加
if (event.target.src !== image.DUMMY_URL) {
this.loading = false;
}
},
}
...
}
</script>
要は
となり、ダミー画像読み込み完了時のイベントはsrc属性がDUMMY_URL(config/image.jsで設定済)であるのでこれをスルーし、本画像読み込み完了時にloadingをfalseにすることでローダーを非表示にする、という制御が可能になりました。
結果
これで当初の予定である、
- 本画像読み込み中…ローダーを表示(内部的にはダミー画像を表示)
- 本画像読み込み完了…ローダー非表示→本画像表示
- 本画像読み込み失敗…ローダー非表示→エラー画像表示
これを実現出来ました。
補足
いくつか補足です。
エラー時の挙動について
謎な部分がありました。
本画像が読み込めなくなったエラー発生時、以下のnoImageUrl
を表示する設定にしており、ここまでの実装でエラー画像の表示は機能はしていた。
...
Vue.use(VueLazyload, {
loading: dummyURL,
error: noImageUrl, // エラー画像
});
...
なので、@error
でエラーイベントの取得ができるだろうと思ったので@error
でonError
メソッドを呼び出すようにして
<img
v-lazy="item.imageUrl"
@load="onLoad"
// 追加
@error="onError"
>
<loader-component
v-if="loading"
>
</loader-component>
onError
メソッドの中でconsole.log
を構えていたが、、
<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
属性のみ書いてもブラウザの挙動上は問題ないので、ここで妥協しようとも考えました。
ただ、この記事によると
lazyload.jsにはダミー画像を自動的に生成するプログラムが組み込まれている
とあり、src
がなくとも実装できますが
imgタグにsrc属性を指定しないとW3Cのチェックに引っかかってしまう
とのことでした。
実際に「W3Cのチェックに引っかかってしまう」とはなんなのか検索しましたが、特に関連記事が見つからずでした。
とはいえ、不確定要素のあるモノを実装するのは怖いのと、他に選択肢があったので、今回は「src
を実装しない」という実装方法は却下しました。
おわりに
「lazyloadとは何ぞ?」から始まりましたが、まだ慣れていないVueの処理の流れを理解し、またイベント内での処理を書くことで融通の利く処理が書けるんだな、と色々と個人的に学びが多い作業でした。
正直、記事を書いていて「そこまで大した処理ではないからわざわざ記事になっていないだけかな。。。」と思いましたが、どうせならと思い投稿させて頂きました。
「このほうがいいんじゃないか」等々、忌憚のないご意見お待ちしております。
読んでいただいた方へ、最後までお読みいただきありがとうございました。