はじめに
スキルアップのためにGithubに公開されているプラグインのコードを読み、そこで学んだことをまとめました。
想定する読者は基本的な構文などを学んだけど次に学習する内容や学習方法に悩んでいる初学者の方になります。
実践的なコードの書き方や学習方法、次に作るもの等の参考にしていただければ幸いです。
読んだコードはcallmecavsさんの「Jump.js」というプラグインで、webサイトでスクロールアニメーションを実現できます。
基本的な使い方
基本的な使い方から見ていきます。
第一引数にスクロールのターゲットになる要素を指定することで、その要素の位置までスクロールさせることができます。指定する要素はノードかセレクタを渡すことができ、セレクタから複数の要素が見つかった場合は1つ目の要素がターゲットになります。また、数値を渡すことで現在のスクロール位置から相対の位置にスクロールします。
Jump(node)
Jump('.target')
Jump(100)
第二引数にはオプションを指定することができ、以下のように記述します。
Jump('.target', {
duration: 1000, // スクロール時間
offset: 0, // ターゲット要素の位置にoffsetの値を加えた数値がスクロール終了位置になります
callback: undefined, // スクロール終了後に呼び出されるコールバック関数
easing: easeInOutQuad, // スクロールに使用されるイージング関数
a11y: false // trueを指定するとスクロール終了後にターゲット要素のfocus属性をtrueにします
})
使用例:
ページ内リンクがクリックされた時にhref属性の値をターゲットに指定してスクロールさせる
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Test Jump.js</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jump.js/1.0.2/jump.js"></script>
</head>
<body>
<a href="#target">trigger</a>
<div id="target" style="margin-top: 600px; margin-bottom: 2000px;">target</div>
<script src="script.js"></script>
</body>
</html>
script.js
window.addEventListener('load', function() {
const trigger = document.querySelectorAll('a[href^="#"]')
for (let i = 0; i < trigger.length; i++) {
trigger[i].addEventListener('click', function(e) {
const targt = e.target.getAttribute('href')
Jump(target)
})
}
})
全体的なコードの内容
まずはざっくりとコードの内容を見ていきます。
関数内の具体的な処理は省略しています。
import easeInOutQuad from './easing.js'
const jumper = () => {
// private variable cache
// no variables are created during a jump, preventing memory leaks
let element // element to scroll to (node)
let start // where scroll starts (px)
let stop // where scroll stops (px)
let offset // adjustment from the stop position (px)
let easing // easing function (function)
let a11y // accessibility support flag (boolean)
let distance // distance of scroll (px)
let duration // scroll duration (ms)
let timeStart // time scroll started (ms)
let timeElapsed // time spent scrolling thus far (ms)
let next // next scroll position (px)
let callback // to call when done scrolling (function)
// scroll position helper
function location () {/* ・・(中略)・・*/}
// element offset helper
function top (element) {/* ・・(中略)・・*/}
// rAF loop helper
function loop (){/* ・・(中略)・・*/}
// scroll finished helper
function done (){/* ・・(中略)・・*/}
// API
function jump (target, options = {}) {/* ・・(中略)・・*/}
// expose only the jump method
return jump
}
// export singleton
const singleton = jumper()
export default singleton
いろいろと省略しましたが、大まかな内容としてはjumperという関数内で
1.処理に必要な変数を宣言
2.関数jump内で使用するヘルパー関数を定義
3.ユーザーが実際に使うことになるメインの関数jumpを定義
といったことをしています。
そして、最後に関数jumperを実行して、返り値の関数jumpをエクスポートしています。
イージング関数
はじめにインポートされているイージング関数は滑らかなアニメーションを実現するために使用されます。
以下を引数にあてることで、経過時間からアニメーション位置を求めることができます。
・アニメーションの経過時間
・アニメーションさせたい値の初期値
・アニメーションさせたい値が初期値から変動する値
・アニメーションにかかる時間
変数宣言
変数の宣言の箇所ではコメントに
no variables are created during a jump, preventing memory leaks
「ジャンプ中に変数が作成されないため、メモリリークが防止されます」と書かれています。
繰り返し呼び出される処理の外に変数を定義することで、処理の度に変数が宣言されないように工夫されていました。
また、変数ごとにコメントが書かれていてますが、用途や単位が整列されていて読みやすく、コメントの書き方として非常に参考になりました。
ヘルパー関数
具体的な処理の内容にはここでは触れず、コメントから大まかな用途を把握します。
location
アニメーション開始時点のスクロール位置を取得
top
スクロール目的地となる要素のトップ位置を取得
loop
アニメーション時に繰り返し実行される処理
done
スクロール終了時に実行される処理
エクスポート
最後のほうでは、jumperを実行して返り値のjump
オブジェクトをsingleton
という変数に代入してエクスポートしています。jump
をそのままエクスポートせずにわざわざjumper
で処理を包んでいるのは、変数やヘルパー関数をスコープ内に閉じ込め、jump
だけを返すことで、余計な変数や関数を外部からアクセスできないようにするためです。
シングルトンという聞きなれない言葉が変数名で使用されていますが、Wikpediaでは以下のように説明されています。
そのクラスのインスタンスが1つしか生成されないことを保証するデザインパターンのインスタンスが1つしか生成されないことを保証するデザインパターン
つまり、使用するときにnew Jump()
というような、新しくインスタンスを作成する記述が不要な関数です。
以下の記事で詳しい実装例などが説明されていて参考になります。
【JSでデザインパターン】シングルトン編
jump関数
処理の流れを把握するため、処理のメインとなるjump関数を見ていきます。
おおまかな流れとしては以下になります。
- オプションに値が設定されていない項目にはデフォルト値を設定
- location関数でスタート位置を取得してstartに代入
- 第一引数のtargetから終了位置を取得してstopに代入
- スタート位置から終了位置までの距離を取得してdistanceに代入
- loop関数を実行してスクロールアニメーションを開始
function jump (target, options = {}) {
// resolve options, or use defaults
duration = options.duration || 1000
offset = options.offset || 0
callback = options.callback // "undefined" is a suitable default, and won't be called
easing = options.easing || easeInOutQuad
a11y = options.a11y || false
// cache starting position
start = location()
// resolve target
switch (typeof target) {
// scroll from current position
case 'number':
element = undefined // no element to scroll to
a11y = false // make sure accessibility is off
stop = start + target
break
// scroll to element (node)
// bounding rect is relative to the viewport
case 'object':
element = target
stop = top(element)
break
// scroll to element (selector)
// bounding rect is relative to the viewport
case 'string':
element = document.querySelector(target)
stop = top(element)
break
}
// resolve scroll distance, accounting for offset
distance = stop - start + offset
// resolve duration
switch (typeof options.duration) {
// number in ms
case 'number':
duration = options.duration
break
// function passed the distance of the scroll
case 'function':
duration = options.duration(distance)
break
}
// start the loop
window.requestAnimationFrame(loop)
}
jump関数の第二引数にoptions = {}
と記述されています。
これは引数が指定されなかった場合に適応されるデフォルト値を設定しています。
第二引数が指定されなかった場合は空のオブジェクトがoptions
に代入されます。
ヘルパー関数
ヘルパー関数の具体的な処理を一つずつ見ていきます。
location
現在のスクロール位置を取得する関数になります。
// scroll position helper
function location () {
return window.scrollY || window.pageYOffset
}
window.scrollY
とwindow.pageYOffset
はどちらも現在のスクロール位置を示します。
window.scrollY
がブラウザで対応されていない場合はwindow.pageYOffset
が返ります。
それぞれのブラウザの対応状況ですが、window.scrollY
はIE以外のブラウザで対応されて、widnow.pageYOffset
はIEを含む全てのブラウザで対応されています。
pageYOffset
のほうがscrollY
より対応範囲が広く、わざわざscrollY
を優先して使用する理由については調べても明確な答えが出てきませんでしたが、こちらのサイトhttps://codeday.me/jp/qa/20190204/216459.htmlでは、「scrollY
はpageYOffset
のエイリアスであり、読みやすさを優先してscrollY
が使用されているのでは」と説明されていました。
top
引数で指定した要素のトップの位置を取得します。
// element offset helper
function top (element) {
return element.getBoundingClientRect().top + start
}
start
はスクロール開始時のスクロール位置が代入されます。
getBoundingRect
で要素のビューポートからの相対位置を取得し、windowのスクロール量を加算することで、要素の位置をwindowトップからの絶対位置で取得することができます。
loop
スクロールアニメーション時に繰り返し呼び出される関数です。
function loop (timeCurrent) {
// store time scroll started, if not started already
if (!timeStart) {
timeStart = timeCurrent
}
// determine time spent scrolling so far
timeElapsed = timeCurrent - timeStart
// calculate next scroll position
next = easing(timeElapsed, start, distance, duration)
// scroll to it
window.scrollTo(0, next)
// check progress
timeElapsed < duration
? window.requestAnimationFrame(loop) // continue scroll loop
: done() // scrolling is done
}
スクロールアニメーション時にrequestAnimationFrame
のコールバック関数として繰り返し実行されます。
アニメーション開始時からの経過時間でスクロール位置を求めてスクロールさせる処理を、requestAnimationFrame
で高速に繰り返すことで滑らかなアニメーションを実現しています。
引数にはミリ秒単位で計測された現在時刻の値を受け取ります。
初回実行時に受け取った現在時刻をグローバル変数timeStart
に代入して、二回目以降に呼び出された際の時刻と比較することでアニメーションの経過時間を求めることができます。経過時間はtimeElapsed
という変数に代入されています。
その後、イージング関数でスクロール位置を求めてスクロールさせ、アニメーション経過時間がduration
に設定した時間を経過していれば後述するdone
関数を呼び出し、経過していなければ再度loop
関数を実行します。
done
スクロール終了後に呼び出される関数です。
function done () {
// account for rAF time rounding inaccuracies
window.scrollTo(0, start + distance)
// if scrolling to an element, and accessibility is enabled
if (element && a11y) {
// add tabindex indicating programmatic focus
element.setAttribute('tabindex', '-1')
// focus the element
element.focus()
}
// if it exists, fire the callback
if (typeof callback === 'function') {
callback()
}
// reset time for next jump
timeStart = false
}
はじめにWindow.scrollToでスクロール終了位置までスクロールしています。
これは、requestAnimationFrameで繰り返し実行する間隔が処理する端末の処理能力などに依存していて、ぴったしduration
の値とおなじタイミングでスクロールアニメーションが終了するとは限らず、微妙にスクロール終了位置がずれてしまうからです。
そのずれを修正するために改めてターゲット要素の位置を指定してスクロールさせています。
例:duration
の値が5000の場合
N回目のloop
実行
timeElapsed
の値 4999 → 再度loop
を実行
N+1回目のloop実行
timeElapsed
の値 5004 → done
を実行
結果4ミリ秒分の終了位置がずれる
次に、オプションでa11y
をtrueに設定した場合の処理が記述されています。アクセシビリティを向上させるためにスクロール後、ターゲット要素をフォーカスしています。
要素にtabindex属性を指定することで、input要素やtextarea以外の要素でもフォーカスできるようになります。0以上の値をしていするとタブキーで順番にフォーカスされ、マイナスの値を指定するとタブキーではフォーカスされなくます。
次に、オプションでコールバック関数を設定して場合は、その関数を実行しています。
コールバック関数を使用することで、Jump.jsの利用者はアニメーション終了時に様々な処理を差し込むことができます。
最後に、次にJump関数が呼びだれた時のためにtimeStart
を初期化して終了です。
まとめ
変数名やコメントのつけ方、ブラウザ対応の方法など、書籍だけでは習得しづらい知見が多々あり勉強になりました。
オープンソースのコードは数えきれないほどあるので、これからも理解できるところ勉強していこうと思います。
勉強法に悩んでいる方にもおすすめです!