Vue.js でヒーロームービー

背景

L/P, ランディングページ で表示する大きい画像をかっこよく、ヒーローイメージと言ったりするらしい。 容量の大きい動画を再生させてみようと思った。 ヒーロームービーとか言っていいのだろうか、検索したら出てこなかったからそんな言葉はないと思う。 でもたまに動画が使われている。

SVG の アニメーションを使って最初の 10 数秒間は、カーテンをかけて逃げてみようと思った。 PC の時はうまくいくけど、スマホの時は SVG アニメーションがカクカクして、うまくいかない。

おそらく動画のダウンロードの処理と被ってるのだろうと推定、遅延ロードの処理を入れてみるといくらか改善された。 それでも完璧ではなかったのでさらに SetTimeout を使って数秒遅らせた。

大容量の動画をダウンロードしている時に SVG でアニメーションを組むのは厳しいのかもしれない。 普通の CSS アニメーションに逃げる手を切り替えてみようか... SVG もそれなりに面白いのでとりあえず高速で数秒動かして、そのあと CSS にして時間を稼ぐと言うのでもいいかもしれない。

方針

基本的な実装は Google 先生の資料を参考にする。めちゃくちゃありがたい... 理解はしていない... けど、とにかく精読した。

<video autoplay muted loop playsinline width="610" height="254" poster="one-does-not-simply.jpg">
  <source data-src="one-does-not-simply.webm" type="video/webm">
  <source data-src="one-does-not-simply.mp4" type="video/mp4">
</video>

見慣れない source タグ

source
<source> タグは、動画や音声などのメディアファイルのURLや種類を指定する際に、<video>~</video> などのメディア要素の中で子要素として使用します。 <source> は、それ自身では何も表しません。

document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });
  }
});

概要は以下のブログで掴んだ。いつも本当にお世話になります。

実装 - 前半

最初の3行からしてキツイ。

document.addEventListener("DOMContentLoaded", function() {
  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

  if ("IntersectionObserver" in window) {
    ...
});

1. DOMContentLoaded

この部分は mounted に記述した。

document.addEventListener("DOMContentLoaded", function() {
  ...
}

DOMContentLoaded も、その代替となりそうな load も、beforeCreate や created などのどのライフサイクル で引っ掛けようとしても、一切発火しなかった。

2. [].slice.call

これは浅いコピーを作成しているらしい。

  var lazyVideos = [].slice.call(document.querySelectorAll("video.lazy"));

別途 Qiita に書いた。

3. "IntersectionObserver" in window

これはグローバル変数に登録されているか確認するためのもの。

  if ("IntersectionObserver" in window) {
    ...
  }

実装 - 中盤

    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
        ...
    });

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });

4. IntersectionObserver.__prototype__.observe

遅延読み込みをする video タグを監視対象に追加しておく。

    lazyVideos.forEach(function(lazyVideo) {
      lazyVideoObserver.observe(lazyVideo);
    });

5. IntersectionObserver

厄介なのがそこで使われている IntersectionObserver と言うもの。 いつ発火し、発火した時に呼び出されるコールバック関数の引数 entries には、 上記で observe メソッドで追加された DOM が代入される。

    var lazyVideoObserver = new IntersectionObserver(function(entries, observer) {
      ...
    }

どうも、ビューポートとの交差判定を検知しているらしいけど、詳しいことはわからない。そのことも上のブログでわかった。

Intersection​Observer​.root - MDN Web docs
The IntersectionObserver interface's read-only root property identifies the Element whose bounds are treated as the bounding box of the viewport for the element which is the observer's target. If the root is null, then the bounds of the actual document viewport are used.

後半

      entries.forEach(function(video) {
        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);
        }
      });

6.

video には video タグの DOM が格納されている。こう言うの正確になんて表現すればいいんだろう?ここからさらに for 文で children を参照しているのは、video タグの中の source タグを参照しているため。

        if (video.isIntersecting) {
          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;
            }
          }

子要素だから source タグの DOM しか返さないのかなと思ったけど、それ以外にも色々と返してくる。なにを返してるのかよくわからない...

Parent​Node​.children
Node.children は、子要素ノードの生きた HTMLCollection を返す、読み取り専用プロパティです。

6.1. children 属性

もう少し言うと、以下の流れが全くよくわからない。 for ... in でキーを取り出して、そこから値を取り出していると言うことか... children には値 DOM オブジェクトが入って言うんじゃないのか...

          for (var source in video.target.children) {
            var videoSource = video.target.children[source];
6.2. 型判定と src 属性への代入

こうとまでしないといけないのか... data-src と言う擬似的な要素に叩き込んでいたものを本番用の属性に入れ替える。

            if (typeof videoSource.tagName === "string" && videoSource.tagName === "SOURCE") {
              videoSource.src = videoSource.dataset.src;

7. load

ただし入れ替えただけでは動作しないので load を実行する。

          video.target.load();
          video.target.classList.remove("lazy");
          lazyVideoObserver.unobserve(video.target);

TODO: entries になにが渡されるか確認。

1) entries には observe で登録したオブジェクト 2) 発火のタイミングはスレッシュホールドで記述したもの 3) children

entries の中のビデオタグ, video タグの中の source タグを取得か

いまいま感じていること

このくらいの snippest であれば、移植性を維持しておきたいから、どうしても Vue.js に頼らない形で書きたいと思うんだけど、なにをするにしても VuePress を使ってしまっている都合上そうすることができない。OSS の変化が鬼のように速いフロントエンドでのベストプラクティみたいなのってなんなんだろう...