pjax第5回 「Barba.js」
応用編
少々複雑な処理を行ううえで、トップページとそれ以外のページで、例えばTransitionの処理を変更したい、加えて、下層ページのうち特定のページでのみ特定の処理を加えたいといったケースが出てくることもあると思います。そんな時、前述のBaraba.BaseView
を使用するのも良いですが、もう一つ方法があります。
<main id="barba-wrapper"> <article id="page1" class="barba-container" data-namespace="page1 sub">
<main id="barba-wrapper"> <article id="page2" class="barba-container" data-namespace="page2 sub">
このようにdata-namespaceには複数の値を記述することもできます。
これにより、namespace:subがある時はfooの処理、かつnamespace:page1があればbar、あるいはnamespace:page2があればbaz…といったような処理分岐が可能です。
Barba.Pjax.getTransition = function() { let transitionObj; const namespace = Barba.HistoryManager.prevStatus().namespace; if (namespace === 'index') { transitionObj = anime1; }else if(namespace.indexOf('sub') !== -1){ // namepace:subは下層ページすべてに付与すればindexかそれ以外のページかの判定に。(elseでの判定でOK) if(namespace.indexOf('page1') !== -1){ //下層ページかつpage1のnamespaceを持つ場合 transitionObj = anime2 }else if(namespace.indexOf('page2')){ //下層ページかつpage2のnamespaceを持つ場合 transitionObj = anime3 }else{ //それ以外の下層ページの場合 transitionObk = anime4 } } return transitionObj; };
複数のnamespaceを使用する際は文字列の比較ではなく、indexOf()
等を使用します。(※同じ文字列が入るnamespaceを使用しないよう注意。例:data-namespace=”page1 page”のようにしてしまうと”page”が検出できてしまう為)
また、ここではBarba.Pjax.getTransition
で処理分岐させましたが、他のパート(Ex.Transitionのメソッド内等)での分岐でもOKです。
では、ここまでのおさらいも兼ねて、第一回目に紹介したこの動画の冒頭(0:00〜0:25)のような
- indexページから下層ページへの移動
- 下層ベージ間の移動
- 下層ページからindexページへの移動
でそれぞれ違った演出を行う、といった指定を行ってみましょう。
では、実践…その前に
様々な演出を加える際、セレクタ操作を行うよりも、styleをいじるほうが良い場合が多々あると思います。
Nativeでstyle操作をする際は結構面倒なので、直感的に使用できる便利関数をあらかじめ用意しておくと良いでしょう。
function css( element,styleObj ){ if(typeof styleObj === 'object'){ let prop = []; for (var key in styleObj) { prop.push( key+ ':' + styleObj[key] ); } prop.join(';'); element.setAttribute( 'style', prop ); }else{ throw new Error( "css() 2nd arguments must be object." ); } } // Using const elem = document.getElementById('foo'); css( elem, {position:'absolute',top: 0, left:'100px'} );
このような関数を用意しておくと、NativeでもjQueryの.css()
のような感覚でstyle操作ができます。
element.styleではなくelement.setAttributeで
Nativeでの最もオーソドックスなstyle指定はelement.style
です。しかし、これは一度に一つずつしか指定することができず、何よりも指定するごとに再レンダリング、レイアウト等が発生するパフォーマンスの悪い方式です。(※fragmentを使わずに複数のDOMを追加するのと同じようなもの)
そのため、複数のstyleを指定する場合は再レンダリング等が一度しか発生しないようsetAtrribute
で一回でまとめて指定しましょう。
※パフォーマンスを最重視するならcss()の内部処理もfor inをforに、pushやjoinを使わず、単純な文字列連結だけで行ったほうがいいかもしれません。
実践
※header.php,footer.phpが別に用意してあると仮定
<?php $pjax = isset($_SERVER['HTTP_X_BARBA']); if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/header.php'; } ?> <main id="barba-wrapper" aria-live="polite"> <article id="home" class="barba-container" data-namespace="index"> <!-- 省略 --> <?php include dirname(__FILE__).'/aside.php'; ?> </article> </main> <?php if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/footer.php'; } ?>
<?php $pjax = isset($_SERVER['HTTP_X_BARBA']); if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/header.php'; } ?> <main id="barba-wrapper" aria-live="polite"> <article id="page1" class="barba-container" data-namespace="sub page1"> <!-- 省略 --> <?php include dirname(__FILE__).'/aside.php'; ?> </article> </main> <?php if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/footer.php'; } ?>
<?php $pjax = isset($_SERVER['HTTP_X_BARBA']); if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/header.php'; } ?> <main id="barba-wrapper" aria-live="polite"> <article id="page2" class="barba-container" data-namespace="sub page2"> <!-- 省略 --> <?php include dirname(__FILE__).'/aside.php'; ?> </article> </main> <?php if( !$pjax ){ include $_SERVER['DOCUMENT_ROOT'].'/footer.php'; } ?>
<?php global $home_dir; // $home_dirはhomeのURLを格納してある(※本来はrequire.phpなど設定ファイルを用意しheader.phpなどで読み込み) $url = $_SERVER['REQUEST_URI']; if(preg_match('/page1/',$url)){ $next_link = 'page1.php'; $case = 1; }elseif(preg_match('/page2/',$url)){ $prev_link = 'page1.php'; $case = 2; }else{ $case = 0; // indexページ時 } if($case !== 0){ ?> <nav id="contents-nav"> <ul> <li><a class="pj back" href="./">>> Back</a></li> </ul> <div id="pager"> <?php if($case === 2): ?> <a class="pj pager prev" href="<?php echo $home_dir,$prev_link; ?>" title="Prev">Prev</a> <?php endif; if($case === 1): ?> <a class="pj pager next" href="<?php echo $home_dir,$next_link; ?>" title="Next">Next</a> <?php endif; ?> </div> </nav> <?php } ?>
ページによってリンクを出し分けたい際などはこのようなaside.phpのようにページによってリンクを変更する処理を書いたphpなどを用意すると便利です。
indexか下層ページかで出力するHtmlの構造を変更したり一つのファイルで各ページ毎のカードリンクなどを出し分けるなど色々と応用が効きます。
#barba-wrapper{ position: relative; width: 100%; min-height: 100vh; margin:0 auto; background: #4a4a4a; overflow: hidden; -webkit-perspective: 1000px; perspective: 1000px; } #barba-wrapper.no-perspective{ -webkit-perspective: none;perspective: none; } .barba-container{ background: #fff; } /* Animations */ .goOut{ -webkit-transform: translate3d(100%,0,0); transform: translate3d(100%,0,0); -webkit-transition:-webkit-transform .6s cubic-bezier(0.8, 0, 0.43, 1); transition:-webkit-transform .6s cubic-bezier(0.8, 0, 0.43, 1); transition: transform .6s cubic-bezier(0.8, 0, 0.43, 1); } .comeIn{ -webkit-transform-origin: 50% 50%; transform-origin: 50% 50%; -webkit-animation: come .6s cubic-bezier(0.8, 0, 0.43, 1); animation: come .6s cubic-bezier(0.8, 0, 0.43, 1); } .flip .barba-container{ width: 100%; height: 150vh; overflow-y: hidden; -webkit-transform-origin: 50% -20%; transform-origin: 50% -20%; -webkit-transform: rotateX(-5deg); transform: rotateX(-5deg); -webkit-transition:-webkit-transform .4s cubic-bezier(0.8, 0, 0.43, 1); transition:-webkit-transform .4s cubic-bezier(0.8, 0, 0.43, 1); transition:transform .4s cubic-bezier(0.8, 0, 0.43, 1); } .barba-container:after{ content: ''; opacity: 0; transition: opacity .3s ease; z-index: -1 } .flip .barba-container:after{ width: 100%; height: 100%; position: absolute; top: 0; left: 0; background: -webkit-linear-gradient(top, rgba(0,0,0,0) 0%, rgba(0,0,0,.3) 20%, rgba(0,0,0,.7) 100%); background: linear-gradient(to bottom, rgba(0,0,0,0) 0%,rgba(0,0,0,.3) 20%,rgba(0,0,0,.7) 100%); opacity: 1; z-index: 1 } @keyframes come { 0% {opacity: 0;transform: scale(.9)} 100% {opacity: 1;transform: scale(1);} } @-webkit-keyframes come { 0% {opacity: 0;-webkit-transform: scale(.9);} 100% {opacity: 1;-webkit-transform: scale(1);} }
indexから下層ページへの遷移演出はperspectiveを使って遠近感を出します。ただし、親要素にperspectiveの指定がある場合、子要素のposition指定は無視される仕様があるため、position指定が必要な時には.no-perspectiveでperspectiveを無効化するなどの処理が必要になります。
ご覧の通り、演出にはcss Animationを使用していることも多いです。
'use strict'; const UI = {}; function css(){ // 省略 } function pageCheck(){ if( document.getElementById('home') ){ PageEvents.index(); }else{ document.getElementById('barba-wrapper').classList.add('no-perspective'); if( document.getElementById('page1') ){ PageEvents.second(); }else if( document.getElementById('page2') ){ PageEvents.third(); }else{ PageEvents.elseFunc(); } } } const PageEvents = { index: () => { if(document.getElementById('barba-wrapper').classList.contains('no-perspective')){ document.getElementById('barba-wrapper').classList.remove('no-perspective') } UI.container = document.getElementById('barba-wrapper'); // 省略 }, second: () => { // 省略 }, third: () =>{ // 省略 }, elseFunc: () => { // 省略 } } function pjaxSetting(){ // indexから下層ページへの遷移時のTransition const Expand = Barba.BaseTransition.extend({ start: function() { UI.container.classList.add('flip') Promise .all([ this.newContainerLoading, this.goTop() ]) .then(this.finish.bind(this)) }, goTop: function( time,distance ){ const def = Barba.Utils.deferred(); const dur = (time) ? time : 600; const dts = (distance) ? distance : 80; Velocity( document.body, 'scroll',{duration:dur, offset: dts, easeing: 'easeInOutQuart',complete: function(){ def.resolve(); }}); return def.promise; }, finish: function(){ const _this = this; const newPage = this.newContainer; css( newPage,{visibility: 'visible',} ); Velocity( newPage,'scroll', { duration: 400, offset: -300, delay: 100, complete: function(){ Velocity( newPage, { rotateX: '0deg', translateY: UI.windowH },{ duration:0, delay:300 } ) } } ); let timer = setTimeout(function(){ document.getElementById('barba-wrapper').classList.remove('flip') _this.goTop( 10,0 ); newPage.removeAttribute('style'); clearTimeout(timer); _this.done(); },1400); } }); // 下層ページからindexへの遷移時のTransition const SlideGone = Barba.BaseTransition.extend({ start: function(){ Promise .all([ this.newContainerLoading, Expand.goTop(400,0) ]) .then(this.goSlide.bind(this)) }, goSlide: function() { const _this = this; const newPage = this.newContainer; css( newPage, {visibility : 'visible'}); _this.oldContainer.classList.add('goOut'); let timer = setTimeout(function(){ newPage.classList.add('comeIn'); clearTimeout(timer); _this.done(); },400); } }); // 下層ページ間の遷移時のTransition const Pager = Barba.BaseTransition.extend({ start: function(){ Promise .all([this.newContainerLoading,Expand.goTop(400,0)]) .then(this.pageSlide.bind(this)) }, pageSlide: function(){ const _this = this; const dataset = _this.newContainer.dataset; const newPage = _this.newContainer; const direction = (dataset.namespace.indexOf('page1') !== -1) ? 100 : -100; const styles = { visibility: 'visible', position: 'fixed', width: '100%', top: 0, left: -direction+'%' }; css( newPage, 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.Pjax.start(); Barba.Prefetch.init(); // linkチェック用設定 Barba.Pjax.originalPreventCheck = Barba.Pjax.preventCheck; Barba.Pjax.preventCheck = (event, element) => { if(!element) return; if(element.classList.contains('pj')){ return true; }else{ return false; } } // pjax時linkをクリックしたかの判定用 let lastElementClicked; Barba.Dispatcher.on('linkClicked', (element) => { lastElementClicked = element; }); // pjax後の処理 Barba.Dispatcher.on('transitionCompleted', () => { pageCheck(); }); // Transitionのdirection一元管理 Barba.Pjax.getTransition = () => { let transitionObj = Expand; // デフォルトのTransitionを定義 const namespace = Barba.HistoryManager.prevStatus().namespace; //遷移前のdata-namespaceを取得 if (namespace === 'index') { transitionObj = Expand; }else{ if( lastElementClicked && lastElementClicked.classList.contains('pager') ){ transitionObj = Pager }else{ transitionObj = SlideGone } } return transitionObj; }; } function resizeing(){ UI.windowW = window.innerWidth; UI.windowH = window.innerHeight; } function init(){ pjaxSetting(); pageCheck(); window.addEventListener('resize',resizeing); } window.addEventListener('DOMContentLoaded', init );
おおまかな処理の流れはこれまでに紹介した通りです。
pjaxまわりの設定を一つの関数にまとめ、そこに複数のTransitionもすべて記述しています。
特筆すべきとしては、#barba-wrapperに対してのセレクタ制御や.barba-containerへのstyle制御をすべて各Transition内で行うというところでしょうか。それぞれのTransitionによって必要なセレクタやstyleプロパティが違っているので各個に処理をするほうが良いでしょう。
setTimeout使用後は必ずclearTimeout()すること。
setTimeoutは使用するとタイマー終了後もメモリに存在し続けます(GCされない)。その為、pjaxサイトではsetTimeoutがメモリリークの要因になる為、GCされるように必ず使用後にclearTimeout()しましょう。
ここでは左右へのスライドに対応させるのが1ページずつだったため、Pagerのdirection判定をnamespaceで判定しました。 しかし、この方法では複数ページに渡る場合この方法ではできません。
複数ページへ対応させたい場合はPager.pageSlide()
のdatasetの取得時にnewContainerからではなくBarba.HistoryManager.prevStatus()
(一つ前のHistoryのページ)から情報(namespaceも可)を取得し、Barba.HistoryManager.currentStatus()
(現在ページ)の情報と比較してdirectionを決めるという指定を入れると良いでしょう。(※各ページのnamespaceに数値を入れておいて大小で判定するなど。もちろん他にも方法がありますが、例として)
まとめ
Barba.jsは単純なpjaxを実装したいだけならやや大掛かりすぎるきらいがあります。 反面、各設定が細かく分けられているため、少し凝った処理や複雑な処理を行うには便利なつくりになっています。
工夫と発想次第で様々なものが実現できるでしょう。ぜひ、色々なアイデアを試してみてください!
Comments