こんにちは、エンジニアの野島(@daumkuchen)です。
弊社で企画・制作した、⾃然エネルギーの爆発的普及を実現する新会社「PowerX」のサイトを先日ローンチしました。
⾃然エネルギーの爆発的普及を実現する新会社「PowerX」のサイトを企画・制作しました。
— Bascule Inc. - Project Design Studio (@Bascule_Inc) 2021年8月25日
「洋上で発電した電力を蓄電し、船で陸地に運ぶ」という規格外なビジョンを持ったベンチャー企業のパワーとスケール感を、全面にプレゼンテーションするサイトになっています。https://t.co/s9e9uEXkfM
こちらのサイトはフロントエンドをNuxt.jsで構築しており、その中でスクロール連動の演出実装にチャレンジしました。
そこから技術的な見どころを少しご紹介させていただきます。
サイトの見どころ
スムーススクロール + 無限スクロール
「スムーススクロール」については、ブラウザのデフォルトのスクロール機能は使っていません。
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);
今でも明確な答えは分からずじまい... 困った...困った...
...!!!
そんな答えの分かるエンジニアをバスキュールは探しています
使うフレームワークは自分で決められる!個人の裁量が大きいバスキュールに来てみませんか!? フロントエンドからバックエンド、肩書きの垣根を超えて活躍したい方はぜひ一度お話ししましょう!