pjax第5回 「Barba.js」
連載モノとなっております。
以前のものをご覧になっていない方はそちらからご覧になると、より理解が深まると思います。
- 第1回 「これからpjaxを使う人に知っておいてほしいこと」
- 第2回 「How to use pjax?」
- 第3回 「pjaxでのイベント処理」
- 第4回 「jquery-pjax」
- 第5回 「Barba.js」
Barba.jsの特徴
Barba.jsは豊富なAPIが用意されており、様々な処理を加えることができます(API Documentation)。加えて、Prefetchによる、(体感的に)非常に高速な遷移が可能である点も特筆すべきポイントです。 また、jQueryとの互換性もあり、jQueryベースでのコードでも使用することができます。(今回は基本Nativeベースでの実装のみ紹介するため、割愛。)
ただ、個人的には最大の特徴と言えば『pjax時、oldContainer(遷移前のpjaxエリア)とnewContainer(遷移後のpjaxエリア)がDOM上に同時に存在するタイミングがある』ということです。 これにより、幅広い演出の表現が可能となります。
豊富なAPI
Barba.jsでは全てのAPIが”Barba”を名前空間とした形式になっており、それを拡張するかたちで使用します。
// Dispatcher API Barba.BaseCache.foo Barba.BaseTransition.foo Barba.BaseView.foo Barba.Dispatcher.foo Barba.HistoryManager.foo Barba.Pjax.foo Barba.Prefetch.foo Barba.Utiles.foo
第二回を書いた頃は勘違いしていたのですが、必須DOMの#barbar-wrapper
,.barba-container
のセレクタの名称はBarba.Pjax APIにより自由に変更可能でした…失礼いたしました。。
Barba.Pjax.Dom.wrapperId = 'foo'; Barba.Pjax.Dom.containerClass = 'bar';
このように、#barba-wrapperをBarba.Pjax.Dom.wrapperId
、.barba-containerをBarba.Pjax.Dom.containerClass
に記述することで任意のセレクタを利用できるようになります。(※「#」や「.」は省くこと)
Prefetch
Barba.Prefetch.init()
を記述することで使用できるようになります。
Prefetchはリンクにカーソルが乗った際、リンク先をあらかじめリクエストする仕組みです。通常、リンクへのカーソルオン〜クリックまで、おおよそ100ミリ秒〜数百ミリ秒ほどのタイムラグが生じます(※認知から操作までのタイムラグ)。
そのタイムラグの間にリクエストを処理してしまえば、クリックのタイミングで読み込みが進んでいる(or完了している)=すぐに遷移可能ということですね。
ただし、注意点としてBarba.jsはデフォルトでは全てのリンク(aタグ)に対して働きます。
つまり、外部へのリンクやファイルへのリンクの場合でもPrefetchを行おうとしてしまい、エラーを吐いてしまうということです。(※何も指定していなければ画像等のモーダルにもBarba.jsは働く)
そのため、Barba.Pjax.preventCheck
によってリンクのチェックを行うことをお勧めします。
Barba.Pjax.start(); // init()でも起動するが、Documentationではstart()を推奨。 Barba.Prefetch.init(); Barba.Pjax.originalPreventCheck = Barba.Pjax.preventCheck; // For Example Barba.Pjax.preventCheck = (event, element) => { if(!element) return; // element(link)が存在しなければこれ以後の処理を行わない(=popstate時などはリンクチェックしない) if(element.classList.contains('pj')){ return true; // .pjを持つリンクならpjaxを行う }else{ return false; // それ以外ならpjaxしない } }
Barba.Pjax.preventCheck
はpjaxの際、必ず呼ばれるメソッドなのでpopstate時の処理分岐を忘れずに。(記述しなければエラーで止まります)
また、色々条件分岐ではじくのも良いですが、あれこれ指定するのが面倒な場合は上記のようなホワイトリスト方式(指定した条件以外すべてはじく)がシンプルでおすすめです。(Html側の記述は若干面倒かもしれませんが)
多彩な遷移アニメーションを
Barba.BaseTransition
を拡張し、複数の遷移アニメーションを用意することができます。そしてBarba.Pjax.getTransition
によって、どのTransitionを使用するかを管理します。
Nativeでの実装なら、基本として何かしらのアニメーションライブラリを使うのが妥当でしょう。
TweenMax, anime.js, velocity.jsあたりがよく利用されるライブラリかと思います。が、今回はvelocity.jsで説明していきます。
特にどれを使っても問題はありません。TweenMaxがこの中では最もおすすめですが、そこまで色々しないのであれば、ちょっと大掛かりすぎるかなというところと、anime.jsはスムーズスクロールがちょっと微妙なので、個人的にバランスのいいvelocity.jsをよく使います。(※といってもvelocity.jsにはtransformに独特の仕様がありますが…)
Transition(遷移アニメーション)を指定する際はBarba.BaseTransition.extend()
にメソッド形式で記述していきます。
const foo = Barba.BaseTransition.extend({ // Object Methods.. });
注意:当記事内のサンプルコードでBarba.BaseTransition.extend
内のメソッドではアロー関数( () => {} )ではなく、必ずfunction式を使用しましょう。
(※thisの値が変わってしまい、上手く動作しない)
ごくシンプルな遷移では下記のように、newContainerLoadingに対して.then()
を使って処理を行えば良いでしょう。
const Animation1 = Barba.BaseTransition.extend({ start: function(){ this.newContainerLoading.then( this.finish.bind(this) ); }, finish: function(){ Velocity( document.body, 'scroll', {duration: 700,easing: 'easeInOutExpo', complete: function(){ this.done(); }} ); } });
Barba.jsは遷移時にページトップへのスクロールを行う仕様がありません。そのため、基本として自前で実装する必要があります。
また、velocity.jsではscrollに対しdocument.bodyを指定するとブラウザ間の差異(body or html)を吸収してくれるため、実装が楽です。
newContainerとoldContainerはDOMオブジェクトなので、JavaScriptで直接操作できます。
いくつかの処理を加える場合、このようにPromiseを利用し、.all()
,.then()
といったPromiseメソッドで処理するとそれぞれのメソッドを非同期で細かくハンドリングできます。
const bar = Barba.BaseTransition.extend({ start: function(){ Promise .all([ this.newContainerLoading, this.process(), this.process2() ]) .then( this.finish.bind(this) ) }, process: function(){ // some code }, process2: function(){ // some code } finish: function(){ // some code } });
ちょっと複雑な遷移演出
上記画像にあるような遷移演出を実装して見ます。
oldContainerとnewContainerが同時に存在することを活用し、スライダーのような遷移アニメーションを作成してみましょう。(DemoのNext/Prev Demoのようなもの)
ポイントとしては、newContainerは#barba-wrapper内にappendされる=.barba-containerが二つ同階層に並ぶため、oldContainerとnewContainerを視覚上並べたい場合は、position:absolute(or fixed)
を指定する、というところです。(+.barba-containerはwidthを画面幅にしつつ背景色等を指定しておく)
let lastElementClicked; Barba.Dispatcher.on('linkClicked', function(element) { lastElementClicked = element; }); const PageSlide = Barba.BaseTransition.extend({ start: function(){ Promise .all([this.newContainerLoading,this.goTop()]) .then(this.pageSlide.bind(this)) }, goTop: function(){ const def = Barba.Utils.deferred(); Velocity( document.body, 'scroll', {duration:400, easeing: 'easeInOutQuart',complete: function(){ def.resolve(); }}) return def.promise; }, pageSlide: function(){ const _this = this; const newPage = _this.newContainer; const direction = ( lastElementClicked.classList.contains('prev') ) ? -100 : 100; // 固定ヘッダーなどがある場合(かつモバイル時固定ヘッダーのheightが変わる) //const topPos = (isMobile) ? 60 : 80; const styles = 'visibility: visible; position: fixed; width: 100%; top: 0(topPos);left: '+ Number(-direction)+'%;'; newPage.setAttribute('style',styles); Velocity( this.oldContainer, {translateX: direction+'%' },{duration:600, easeing: 'easeInOutExpo'}); Velocity( newPage, { left: 0},{duration: 600, easeing: 'easeInOutExpo', complete: function(){ newPage.removeAttribute('style') _this.done(); } }) } });
Barba.jsにおけるpromiseは(一応)独自実装的な部分があり、Barba.Utils.deferred()
によってDeferredオブジェクトを生成し、promiseを返す、という使用方法になっています。(別に普通のPromiseを使っても何ら問題はありませんが)
まずgoTop()でページトップへスムーズスクロールし、それが完了+newContainerの読み込みが完了した後、pageSlide()の処理(newContainerをposition:fixedでoldContainerの横に並べる+両方を横移動)→oldContainer破棄、newContainerのstyle属性削除…という処理です。
oldContainerとnewContainerを移動させる際、双方のdurationとeasingは同じ指定にしましょう。
また、親である#barba-wrapperを動かしたほうが処理はシンプルにはなりますが、style削除の際、妙な挙動を起こす(チラつくなど)する為、素直にoldContainerとnewContainerそれぞれを処理したほうが無難です。
oldContainerはtranslate、newContainerはleftで横移動させているのは、velocity.jsの特徴で、velocity.jsでtransformプロパティをアニメーションさせたい場合、 transformプロパティは常にVelocity()
で指定しなければアニメーションせず、いきなり最終フレームに移動してしまう為です。(※style属性を直接追加した場合やcss側で指定している場合アニメーションがうまくいかないという何とも言えない仕様)
当然と言えば当然ですが、他にアニメーションを作成していて、同じ処理だった場合、他のオブジェクトからメソッドを利用することができます。
const Animation = Barba.BaseTransition.extend({ start: function(){ Promise .all([ this.newContainerLoading, this.goTop() ]) .then(this.finish.bind(this)) }, goTop: function(){ const def = Barba.Utils.deferred(); Velocity( document.body, 'scroll', {duration: 700,easing: 'easeInOutExpo',complete: function(){def.resolve()}} ); return def.promise; }, finish: function(){ // 省略 } }); const PageSlide = Barba.BaseTransition.extend({ start: function(){ Promise .all([ this.newContainerLoading, Animation.goTop() ]) .then(this.pageSlide.bind(this)) }, // 省略 });
※同様に外部からBarba.BaseTransition.extend
内のメソッドへ引数を渡すこともできるため、柔軟な実装が可能です。
ちょっと突っ込んだ話
Documentationでは、Transition(遷移アニメーション)を利用する際はBaraba.BaseView
を拡張してね、と書いてありますが
All the transitions need to extend the Barba.BaseView object.
確かに、Baraba.BaseViewを拡張することで、細かなタイミングでの処理が可能にはなります。しかしdata-namespaceを基準とするため、複数のnamespaceを必要とするようなサイトでは、そのnamespace毎にBaraba.BaseViewを指定しなければならず、非常に冗長な記述になります。
const foo = Barba.BaseView.extend({ namespace: 'foo', onEnter: function() { // some code }, onEnterCompleted: function() { // some code }, onLeave: function() { // some code }, onLeaveCompleted: function() { // some code } }); foo.init(); // この記述をサイト内に存在するnamespaceの個数分記述しなければならない
(※メソッドは必要なもの以外省略可)
namespaceの個数分、こんなコードを追加していくのは正直面倒すぎます…。それに、そこまでページ毎に読み込み時、離れる時に違った処理が必要になるか?と言われればおそらくそんなことは無いのではないでしょうか?
実際の使用感で言っても、『じゃあBarba.Pjax.getTransition
は何のためにあるの??』といった感じになることと、
内部処理の順番としては
- Barba.Pjax.getTransition
- Barba.Dispatcher.on(‘initStateChange’)
- Barba.BaseView.extend.onLeave
- Barba.Dispatcher.on(‘newPageReady’)
- Barba.BaseView.extend.onEnter
- Barba.Dispatcher.on(‘transitionCompleted’)
- Barba.BaseView.extend.onLeaveCompleted & Barba.BaseView.extend.onEnterCompleted
の順で呼び出されます。
つまるところ、Barba.Pjax.getTransition
でnamespace毎のTransitionを、pageCheckで各ページ毎の処理をそれぞれ一元管理し、Barba.Dispatcher.on(‘newPageReady’)やBarba.Dispatcher.on(‘transitionCompleted’)でpageCheck関数へ処理を回すほうがシンプルかつメンテナンス性の高いコードになると考えます。(※newPageReadyを使えばonEnterより早いタイミングで処理が始まる)
Baraba.BaseView
はTransitionそのものを呼び出すよりも、特定のページでTransitionとは別に、あるいはTransitionと並行して何かを処理したい場合にのみ使用する感じのほうが個人的にはおすすめです。
Barba.Pjax.start(); Barba.Dispatcher.on('transitionCompleted', function(){ pageCheck(); }); Barba.Pjax.getTransition = function() { var transitionObj = anime1; var namespace = Barba.HistoryManager.prevStatus().namespace; if (namespace === 'index') { transitionObj = anime1; }else if(namespace === 'bar'){ transitionObj = anime2 }else{ transitionObj = anime3 } return transitionObj; };
Barba.jsでの完全部分読み込み
jquery-pjaxではリクエストヘッダーに_pjax
があったようにBarba.jsにもx-barba
ヘッダーが用意されています。
<?php /* x-barbaは"yes" or nullなのでisset()で判定 */ $pjax = isset($_SERVER['HTTP_X_BARBA']); // $_SERVERなので"HTTP_"プレフィックス+全大文字+ハイフンはアンダースコアへ変更。 if( !$pjax ){ <!DOCTYPE html> <html lang="ja"> <!-- 省略 --> <?php } ?> <main id="barba-wrapper"> <div class="barba-container" data-namespace="foo"> <!-- --> </div> </main> <?php if( !$pjax ){ ?> <footer id="footer"> </footer> <script></script> </body> </html> <?php } ?>
このように記述することでpjaxで読み込んでくるhtmlを完全にpjaxエリアのみにすることができます。Prefetchと組み合わせれば非常に快適に遷移可能になります。
…あまりに長くなってきたので、ここまでのまとめとなる『応用編』は次ページへ!
Comments