バスキュール技術ブログ

バスキュールが得意とするインタラクティブエンジニアリングを、あますことなくお届け!

Nuxt.jsを使用したスクロール連動の演出実装

f:id:bascule-dev:20210902211228j:plain

こんにちは、エンジニアの野島(@daumkuchen)です。

弊社で企画・制作した、⾃然エネルギーの爆発的普及を実現する新会社「PowerX」のサイトを先日ローンチしました。

https://power-x.jp/

こちらのサイトはフロントエンドをNuxt.jsで構築しており、その中でスクロール連動の演出実装にチャレンジしました。
そこから技術的な見どころを少しご紹介させていただきます。

サイトの見どころ

スムーススクロール + 無限スクロール

f:id:bascule-dev:20210902210931g:plain
モック

f:id:bascule-dev:20210902204916g:plain
完成

スムーススクロール」については、ブラウザのデフォルトのスクロール機能は使っていません。
DOMのコンテナをjsでアニメーションすることによって擬似的にスクロールさせる、一般的(?)に実装されるスムーススクロールと同じような作りになります。

// amount : wheelやtouchmoveの量の値
// ease : 減衰値(例:0.1)
this.scroll.value += (amount - this.scroll.value) * ease;

gsap.set('.l-scroll-container', {
  y: this.scroll.value,
});

無限スクロール」は一番下までスクロールするとmod関数によりスクロール値を0に戻す作りになっています。
カルーセルスライダーのループと同じようなロジックです。

const mod = (i, j) => {
  return (i % j) < 0 ? (i % j) + 0 + (j < 0 ? -j : j) : (i % j + 0);
}
// page_height : ページ全体の高さ
// clone_height : 無限にスクロールに見せるため1番上のsectionを複製して1番下に置く、その高さ
this.scroll.amount += (amount - this.scroll.amount) * ease;
this.scroll.value = mod(this.scroll.amount, page_height - clone_height);

まずこちらのスクロールの仕組みのモックを事前に作成し、技術的課題をチェックしたり、想定している演出の可否チェックなどを行いました。

スクロールと連動した演出の仕組み

各sectionごとにスクロール値を正規化してコンポーネントにpropsで渡しています。

<template>
  <Child
    :move="move" />
</template>

<script>
export default {
  props: {
    move: 0.0,
  },
  methods: {
    update(){
      requestAnimationFrame(this.update.bind(this));

      const value = -this.scroll.value;
      // min : sectionに入るタイミングのスクロール値
      // max : sectionから出るタイミングのスクロール値

      if (min <= value && max >= value){

        // sectionに入ったタイミングで発火!
        
      } else {

        // sectionに入っている間毎フレーム発火!

        // min、max、スクロール値を元に0~1に正規化した値を渡す
        const height = max - min;
        const scroll = (value - min);
        const move = scroll / height;
        this.move = move;
      }
    }
  }
}
</script>

実際はフラグ処理や分岐等でもっとコード量は多いので、正規化のロジックのみ取り出しています。
このように値をpropsに渡してあげることで、演出を各コンポーネント内で完結でき、作業の分担をしやすくしました。

ここの仕組みがもっと知りたい!」などあれば、コミュニケーションとっていただけるとありがたいです!

困ったけど解決したこと

watchメソッドの最適化

モックを作り、本番環境を整え、一度全ての要素を入れ込んだ状態で最初にぶつかった壁はパフォーマンスでした。
ボトルネックを探した結果、上記で子コンポーネントに渡していたmoveをwatchしていたメソッドが怪しいと感じました。
フラグを設置して、明示的にON/OFFするようにしたところ、負荷が劇的に改善されました。
おそらくmoveが書き換わらなくても、watchメソッドが裏側で動作していたと思われます。

// BEFORE
export default {
  props: {
    move: {
      type: Number,
      default: 0.0
    },
  },
  watch: {
    move: function () {
      // 演出実装
    }
  }
}
// AFTER
export default {
  props: {
    move: {
      type: Number,
      default: 0.0
    },
  },
  data() {
    return {
      isUpdate: true,
    }
  },
  methods: {
    init() {
      this.frameUpdate();
    },
    frameUpdate() {
      requestAnimationFrame(() => this.frameUpdate() );
      if (!this.isUpdate) return;
      this.isUpdate = false;
      // 演出実装
    }
  },
  watch: {
    move: function () {
      this.isUpdate = true;
    }
  }
}

今回はwatchメソッド内で重い処理を走らせていたので気づきましたが、普段は中々気づきにくいところかもしれません。

改善したいこと

nuxt generate後に生成されるhtmlのmetaが書き換わらない

/pages/配下で指定したページのmetaの内容が、書き出されたhtmlに反映されず、nuxt.config.jsの内容のままになっていることが発覚しました。
色々調査しましたが直接的な原因は分からず。
結局nuxt generate後に書き出されたhtmlをnode.jsで直接書き換えることになりました。

const LIST = [
  {
    'ja': /旧タイトル/g,
    'en': '新タイトル',
  },
  {
    'ja': /旧ディスクリプション/g,
    'en': '新ディスクリプション',
  },
  {
    'ja': 'lang="旧言語設定"',
    'en': 'lang="新言語設定"',
  },
  {
    'ja': '<meta data-n-head="1" data-hid="og:url" property="og:url" content="旧シェアURL">',
    'en': '<meta data-n-head="1" data-hid="og:url" property="og:url" content="新シェアURL">',
  }
];

function replace(path, list) {
  const data = fs.readFileSync(path, 'utf8');

  let result = data;
  list.forEach(e => {
    result = result.replace(e.ja, e.en);
  });

  fs.writeFile(path, result, 'utf8', function (err) {
    if (err) {
      throw err;
    }
  });
}

replace('./dist/index.html', LIST);

今でも明確な答えは分からずじまい... 困った...困った...

...!!!

そんな答えの分かるエンジニアをバスキュールは探しています

使うフレームワークは自分で決められる!個人の裁量が大きいバスキュールに来てみませんか!? フロントエンドからバックエンド、肩書きの垣根を超えて活躍したい方はぜひ一度お話ししましょう!

bascule.co.jp