- Header Parallax Transition
- swipe gestureの改善
- componentCanSwipe
- componentWillExit
- Page Stackは膨大化になるのでは?
- BrowserのNative Swipe Gestureとコンフリクトしない?
- ブラウザー環境ではPage Stackを使う意味あるのか?
- Functional Componentでは動けるか?
- 結論
この記事は、LINE証券の開発メンバーが、LINE証券のフロントエンドにまつわる様々なトピックや、開発中に得た知見を共有する【LINE証券 FrontEnd】 シリーズの2記事目です。
はじめに
こんにちは、sunderlsです。2年前Page Stackについての記事「LINEマンガ:Page Stackを使ってサクサクなページ遷移を実現できました」を出しました。LINE証券でもこのアプローチをとっています。
この記事ではPage Stackはどう進化したのか、どんな課題を抱えているのかを説明します。
Header Parallax Transition
Page StackはNative Appのように、ページ遷移してもDOMを消さずに順序通りにStackされる仕組みです。DOM(Document Object Model)は消さないため、前のページに戻る時は瞬時に行われ、チラつく問題も解消されます。以下はDemoです。
よく見たら、Native Appと少し違います。Native Appの場合は、ヘッダーの部分は違うトランジションをつけているケースが多いです。Page Stackでも、ヘッダーとボディーを別々のコンポーネントにすれば、同じエフェクトが得られます。LINE証券ではこう実装して、こんな綺麗なトランジションを作りました。
swipe gestureの改善
Page Stackは最初からSwipe Gestureをサポートしていましたが、実装が甘かったです。指の移動距離だけをみて、ナビゲーションをトリガーしていました。Native Appの場合は、移動速度も見ているケースが多いので、私たちで改善してみました。実装はシンプルです。
private onTouchEnd = () => {
const distance = touch.clientX - touchStartClientX
const velocity = distance / elapsedTime
if (distance > distanceThreshhold || velocity > velocityThreshhold) {
// OK, trigger history.back()
}
}
componentCanSwipe
Swipe Gestureはいい感じに対応したが、一つの問題に直面しました。「全てのページにSwipe Gestureを適用することはよくない」。複数のページを跨いでるユーザーの入力機能を想像してください。前のページに戻る時に、入力がある場合は確認ダイアログを出すのはよくありますね。こんな時にSwipe Gestureを提供すると、ユーザーの操作がややこしくなります。
各ページでSwipe Gestureの適用を指定できるよう、Page Stackに `componentCanSwipe`のサポートを追加しました。仕組みはシンプルです。Swipe Gestureは完全にPage Stackに制御されているため、発動する前に、Page Stackにある一番上にあるページコンポーネントの`componentCanSwipe`を見れば、トランジションを中断することができます。
// PageStack
canSwipe = () => {
// 最後のページだったら、Swipe Gestureは発動しない
if (componentStackLength < 2) {
return false
}
// トランジションの途中でもSwipe Gestureを無効に
if (transitionLayerCount > 0) {
return false
}
// 一番上のページのプロパーティーをみる
return topComponent.componentCanSwipe
}
// Page Component では簡単にswipe gesture適用かどうか指定できる
class Page extends React.Component {
componentCanSwipe = true
render() {}
}
componentWillExit
すでに気づいたかもしれませんが、上記の対策はSwipe Gestureを禁止することを可能にしたが、離脱確認ダイアログを出していません。SPAではない場合は、`onbeforeunload`を使って簡単に対応できます。SPAの場合は、react-routerは `<Prompt/>`を提供していますが、native UIの`window.confirm`などしかサポートしていません。ここでUIの綺麗さに妥協したくないから、私たちは頑張りました。
Page Stackは唯一history changeをみているコンポーネントなので、history changeをみて独自の確認UIを出すことが可能です。そこに追加したのは、`componentWillExit`です。中身はざっくりこんな感じです。
// PageStack
async componentDidUpdate(prevProps: Props) {
if (this.props === prevProps) return
// POP の場合
if (this.props.history.action === 'POP') {
const lastPoppedLocation = {
...prevProps.location
}
// 一番上のページはexitを許容しているかどうか
if (await topComponent.componetnWillExit()) {
// 許容している場合は、そのまま何もしなくていい
} else {
// もし許容していない場合は、前popされたhistoryを再構築する必要がある
// history.pushは呼ばれる
// history.pushの一連を動作を中断するために、このフラッグを設置する
this.isPreventingGoBack = true
prevProps.history.push(lastPoppedLocation)
return
}
this.popStack()
} else {
// もしこのフラッグが設置される場合は
// page stackが入れたhistoryなので、無視する
if (this.isPreventingGoBack) {
this.isPreventingGoBack = false
return
}
this.pushStack(...)
}
}
`componentWillExit`はasyncなので、どんなUIでも出せます。
Page Stackは膨大化になるのでは?
はい、DOMは消さないからです。Web Appのサービスロジックにより、問題になるかは別々です。Native Appも同じ問題を持っているが、OSレベルでちゃんと対応されています。Page Stackではsliding windowを実装すれば、ある程度似ている仕組みを作ることができます。その実装は最初のバージョンからすでに含まれています。
Page Stackには `maxDepth`と`componentShouldSleep`、 `componentShouldAwake`のフックをサポートしています。例えば、maxDepthを超える時に、一番下にあるページコンポーネントの`componentShouldSleep` が呼ばれ、DOM削除の処理を入れることができます。
// PageStack
async componentDidUpdate(prevProps: Props) {
if (this.props.history.action === 'POP') {
if (this.componentStack.length > this.maxDepth) {
this.componentStack[this.componentStack.length - this.maxDepth].componentShouldAwake()
}
} else {
if (this.componentStack.length > this.maxDepth) {
this.componentStack[this.componentStack.length - this.maxDepth].componentShouldSleep()
}
}
}
BrowserのNative Swipe Gestureとコンフリクトしない?
いい質問です。今までのPage Stackは主にブラウザーではなくてwebviewをターゲットにしていました。Browser環境の場合は、二つの壁があります、native swipe gesture と navigation button。
対応方法は模索してきました。
Navigation Buttons
Page Stackはhistory changeをみているので、バックワードボタンは問題ありません。フォーワードボタンについては、結構ややこしくて、個人的にはダミーのhistoryを入れて禁止したほうがいいと思います。
Native swipe gesture.
フォーワードボタンより、Native Swipe Gestureを利用しているユーザが多いでしょう。何もしなければ、Page Stackのトランジションと重複します。
この問題を解決するため、色々試しました。結論から言うと、ブラウザー環境ではPage StackのSwipe Gestureとトランジションを無効にするのは一番適切な対応だと思います。Page StackのCSSコードをいじれば、簡単にできます。
トランジションがないのはちょっと寂しいが、Native Swipe Gestureがあるので、逆にいい感じになります。
ブラウザー環境ではPage Stackを使う意味あるのか?
いい質問です。上記の対応したら、Page Stackを使っても、使わない場合と比べると見える変化がないようです。
確かにそうですが、Page Stackを使うメリットは他にあります。
- 「戻る」は瞬時に行われる. Page Stackは基本的にDOM消さないので、他のアプローチと比べて「戻る」のは一番早いです。
- チラつきはない. 横スクロール位置とかのUI状態は自動的に維持されます。
- Native Appの考えと似ている. Web Appを開発している私たちはできる限りにNativeに近づけようとしているので、考え方も近づけたほうが目的達成しやすいかと思います。
Functional Componentでは動けるか?
現在のPage Stackは`withStack`を経由して、ページコンポーネントにライフサイクルメソッドを有効化にしていますが、Functional Componentではref使えないので、Page Stackは動きません。2年前では大して問題ではないが、今はhooksの時代でFunctional Componentはほとんどになるため、動かないのは問題になっています。
Function ComponentでPage Stackはどう動くべきなのか、まだ模索中です。もし興味のある方、私たちと一緒にその方法を見つけてみませんか??
採用:フロントエンドエンジニア / LINE Financial / 証券
その方法が見つかるまで、ワークアラウンドとして`<PageStackGlue/>`を使って、Functional ComponentでもPage Stackを動かせます。
// PageStackGlueはproxyみたいにlifecycle hookを提供する
const PageStackGlue = withStack(
class extends React.PureComponent {
componentDidTop() {
if (this.props.componentDidTop) {
this.props.componentDidTop()
}
}
componentDidEnter() { ... }
componentDidHide() {... }
componentCanSwipe() { ... }
render() {
return null
}
}
)
// これでfunctional componentでも使える
const A = () => {
return <div>
<PageStackGlue componentCanSwipe />
</div>
}
まとめ
私たちはPage Stackを積極的に利用しています。便利さを得るとともに煩わしいこともついてきます。しかし、常に自分の限界を突破することが、フロントエンドの開発の醍醐味でしょう。
この記事は、【LINE証券 FrontEnd】シリーズの第2回です。次回以降もLINE証券メンバーが様々なトピックをお届けします。お楽しみに。
【採用情報】
- フロントエンドエンジニア / LINE Financial / 証券 https://linecorp.com/ja/career/position/1102
- フロントエンドエンジニア / フロントエンド開発センター https://linecorp.com/ja/career/position/475