Turbolinksによる絶え間ないWeb音楽プレーヤーの実現

8651 ワード

Webページの切り替え時に音楽プレーヤーの無停止再生を実現するには、SPA、pajax、iframeといういくつかのソリューションがよくあります.
以上、何種類も使ったことがないので、今日は自分がプロジェクトで試したソリューションをTurbolinksで紹介します.このスキームは、従来の非前後端分離ウェブサイトに使用することができる.
Demo
プロジェクトアドレス:GitHub
音楽と画像はhey-Audioから来て、感謝します!
曲は網易雲音楽mp 3アドレスから変換された.歌の情報は交流鑑賞にのみ提供されます.網易雲音楽は試聴のみで、商用には使用できません.
Notes
Turbolinks
まずTurbolinksテクノロジーを理解してみましょう.Railsプロジェクトに付属しているテクノロジーですが、JavaScriptのライブラリなので、他のバックエンド言語でも使用できるはずです.バックエンドにも対応する論理がありますが、後で説明します.
一言でその役割を要約すると、ラベル(明示的に必要としない限り)のクリックイベントをすべて引き継ぎ、クリックするとデフォルトのhtmlリクエストをajaxリクエストに変更します.実際には伝統的なサイトをスパに近いものにしましたが、元の書き方をほとんど変えず、極めて侵入性が低いです.
そのメリットは何でしょうか.比較してみましょう.
Turbolinksがない場合、ページAでリンク(ページBと仮定)をクリックすると、ブラウザは通常のhtml要求を開始し、サーバからページBのhtmlコードを取得し、このhtmlコードに含まれるJavaScript/CSS/Assetsのファイルを解析し、ダウンロードし、JavaScriptコードを解析して実行し、ページBを表示します.
一般的に、ページAとBのhtml headコードは同じであり、含まれるJavaScript/CSSも同様であることを意味し、これらのファイルはページAのロード時にダウンロードされ、ブラウザにキャッシュされているため、ページBをロードする際、再ダウンロードに時間がかかる可能性はあまりないが、JavaScriptを再解析するのに時間がかかる(profileを待つ).
ただし、JavaScript/CSSを再ダウンロードして解析する時間はあまり長くはありませんが、ページAのメモリ状態をページBに保つことができないという問題もあります.ページBがページAを置き換えると、メモリ内のページAのデータが完全に空になります.例えば、ページAのJavaScriptコードには、グローバル変数window.globalA = 'haha'が保存されており、ページBがロードされると、この値は消去される.
クッキーやlocalStorageでページAのいくつかの状態をページBに伝えることができると思いますが、ほとんどの場合は可能ですが、私たちが実現したいオーディオの無停止再生ではだめです.
Turoblinksは上記の3つの問題を解決することができます.リンクをクリックすると、ajaxリクエストが開始され、サーバからhtmlコードが取得され、head部分が現在のページと同じ場合、head部分が解析されます.
  • JavaScript/CSS/Assets
  • は再ダウンロードされません.
  • JavaScript/CSS
  • は再解析されません.
  • 現在のページのメモリ
  • は消去されません.
    △完全に正確ではないが、大体論理はそうである.
    スクリーンショットを2枚見てみましょう.
    最初のスクリーンショットは、Turoblinksを使用していない場合、リンクをクリックするたびに、さまざまなリソースが再ダウンロードされます.
    2枚目のスクリーンショットは、Turbolinksを使用した場合、チェーンをクリックすると、ダウンロードしたリソースを再ダウンロードすることはありません.
    RailsプロジェクトではTurbolinksはデフォルトで使用されていますが、使いたくない場合はアプリケーションから使用します.jsから削除します.
    // app/assets/javascripts/application.js
    //= require rails-ujs
    //= require activestorage
    //= require turbolinks      // ---> remove this line
    //= require_tree .
    

    インプリメンテーション
    まず、Rails+React+webpacker+react-railsを使用して、従来のサービス側レンダリングの非前後端分離サイトを実現します.どのように実現するか知りたいなら、このリンクを見てください.RailsでReactを使用してSSRを実現する実践を見ることができます.もちろん、Reactを使わなくてもいいです.私はここで一つの考えを述べます.
    最初のスクリーンショットアニメーションに示すように、このサイトには2つのページがあり、1つのページは曲のリストで、1つのページは1つの曲の詳細ページで、この2つのページの下部には音楽プレーヤーがあります.どの曲のプレイボタンをクリックしても、この曲が再生され、ページ間が切り替わると、プレーヤーが途切れないことを期待します.
    Turbolinksは、ページを切り替えるときにメモリの状態を維持するのに役立つので、audio elementと現在の状態をwindowグローバルオブジェクトに保存するだけで、新しいページにジャンプしてから読み取ります.that's all.
    AudioPlayerのコードは以下の通りです.
    componentDidMount() {
      if (!window._globalAudioEl) {
        window._globalAudioEl = document.createElement('audio')
      }
      this.audioElement =  window._globalAudioEl
    
      this.audioElement.addEventListener('canplay', this.onCanPlay)
      this.audioElement.addEventListener('play', this.onPlay)
      this.audioElement.addEventListener('pause', this.onPause)
    
      window.addEventListener('play-audio', this.playAudioEventHandler)
    
      // recover state
      if (window._globalAudioState) {
        this.setState(window._globalAudioState, () => {
          this.state.playing && this._setInterval()
        })
      }
    }
    
    componentWillUnmount() {
      // save current state to window before it is unmounted
      window._globalAudioState = this.state
    
      window.removeEventListener('play-audio', this.playAudioEventHandler)
    
      this._clearInterval()
    
      this.audioElement.removeEventListener('canplay', this.onCanPlay)
      this.audioElement.removeEventListener('play', this.onPlay)
      this.audioElement.removeEventListener('pause', this.onPause)
    
      // don't pause it
      // this.audioElement.pause()
      this.audioElement = null
    }
    

    特別な場合
    Turbolinksはリンクのみを処理でき、Turbolinksはではなくclick event listenerを最上位レベルのwindowオブジェクトにバインドするため、次のような状況でTurbolinksは処理できません.
  • のclick handlerでevent.stopPropagation()が実行すると、イベントのバブルが阻止され、Turbolinksはこのイベント
  • を受信できなくなる.
  • フォームFormによって開始されたGET要求
  • フォームFormによって開始するPOST要求
  • 私たちはそれらを解決する方法を考えなければなりません.
    まず、第1の状況を見ると、event.stopPropagation()を呼び出してイベントのバブルを阻止した後、要求はデフォルトのhtml要求に戻り、event.preventDefault()を呼び出してデフォルトの操作を阻止し、Turbolinks.visit(url) APIを呼び出してajax要求を手動で発行することができる.
    コード:
    songClick1 = (e) => {
      // will stop event propagate to window, so turbolinks can't handle this link
      e.stopPropagation()
    }
    
    songClick2 = (e) => {
      e.stopPropagation()
    
      e.preventDefault()
      window.Turbolinks.visit(e.target.getAttribute('href') + '?click')
    }
    
    This link execute event.stopPropagation() :
    {firstSong.title}
    
    Resolution :
    {firstSong.title}
    

    効果の比較:
    詳細は、event.stopPropagation()でTuroblinksが無効になったことは当然のことだと思いますが、実際にはReact合成イベント(SytheticEvent)のバブルではなく、オリジナルイベント(NativeEvent)のバブルではなく、Turbolinksがオリジナルイベントを処理しています.理由を知りたいなら、この文章を見てください.DropdownのReact実装から学びました.
    2つ目のケースでは、フォームFormによって開始されたGETリクエストについて、この例では、titleによって対応する曲を検索する検索フォームを作成します.
    2つの解決策があります.
  • 上記のように、FormのonSubmitイベントでは、呼び出しevent.preventDefault()がデフォルトのコミットをブロックし、その後、呼び出しTurbolinks.visit(url) APIがajax要求を手動で送信する.
  • GETタイプのFormをリンク
  • に変換する.
    サンプルコード:
    queryUrl = () => {
      return `/songs?q=${this.state.queryStr}`
    }
    
    submitQuery = (e) => {
      e.preventDefault()
      window.Turbolinks.visit(this.queryUrl())
    }
    
    render() {
      ...
          

    Speical Case 2 - Get Form

    Origin Get Form :
    Resolution 1 : use window.Turoblinks.visit API
    this.setState({queryStr:e.target.value})}>
    Resolution 2 : convert form to link
    this.setState({queryStr:e.target.value})}> search
    ... }

    効果の比較:
    3つ目のケースでは、フォームFormによって開始されたPOSTリクエストについて、この例では曲titleを変更するフォームを作成します.解決策は異常に簡単だが、背後にある原理は言う価値がある.
    まず解決策を見ると、formの属性にdata-remote(またはdate-remote={true})を付けるコードは1行しかありません.全体のコードは次のとおりです.
    Resolution: data-remote Form
    

    効果の比較を見てみましょう.
    なぜdata-remoteの属性を付けてOKになったのか、不思議な感じではありませんか.ここにはもう一つのJavaScriptライブラリが機能しています.rails-ujsもrailsがデフォルトで有効になっています.このライブラリは多機能で、ここではdata-remote属性がtrueのformフォームのすべてのリクエストをajaxリクエスト(xhrはリクエストがajaxであることを示す)に変換する役割を果たしていますが、このときTurbolinksは参加していません.前に言ったことを覚えていますか.チェーンだけで、他のことは気にしません.
    しかし、これは第1のステップにすぎず、一般的にPOST要求の結果は302応答であり、ブラウザは応答ヘッダからターゲットアドレスを取り出し、この新しいアドレスにアクセスする.
    サービス側のコード:
    # songs_controller.rb
    def update
      @song = Song.find params[:id]
      @song.update(title: params[:title])
      redirect_to @song
    end
    

    ここで、通常のPOST要求に対する応答:
    私たちのajaxリクエストもこのような応答を得た場合、ジャンプは自動的に実現できません.幸いなことに、Railsはインテリジェントで、POST要求がajaxによって開始されたことを検出し、Turbolinksが有効になった場合、redirect_to @songでは302ではなくJavaScriptコードを返します.
    Turbolinks.clearCache()
    Turbolinks.visit("http://localhost:3000/songs/21", {"action":"replace"})
    

    ajaxリクエストはこの応答を得た後、応答タイプがtext/javascriptであるため、このJavaScriptコードを実行し、このJavaScriptはTuroblinksのvisit APIを呼び出したが、このAPIは私たちが前に何度も使ったことがあり、実際にHTML 5 history APIを呼び出してリフレッシュのないジャンプを実現した.
    これも前述したTurbolinksはJavaScriptライブラリですが、バックエンドの協力が必要です.