JavaScript
Rails
angular
Rails5

SPA(single page application)のURLでシェアする

本稿は「rails5 APIモードでindex.htmlを配信する」の続きです。
SPA(single page application)のURLでシェアするために少し発展させました。

https://hoge.com/article/1をtwitterでシェアし、そのツイートのリンクをクリックして初めてサイトにアクセスする人がトップページではなくいきなり該当コンテンツを見れるようにします。

自由なシェアこそwebの本懐!SSR(server side rendering)を導入しなくてもやれておきたい。

railsとangularでやっています。OGPタグはアプリ共通のためサムネイルの表示などはできません。

アプローチ

サーバでindex.htmlを返しつつ、ブラウザに到達してからLocationを展開します。
https://hoge.com/article/1を例にしていきます。

  • railsのルーティングでSPAのURLを認知する
  • SPAのURLならパス/article/1を保持しつつindex.htmlを返す
  • フロントでindex.htmlを展開しつつ該当パス/article/1へのルーティングを行う

railsのルーティングでSPAのURLを認知する

まず、前回はget '*path', to: redirect('/')とし、API以外のその他URLを全てリダイレクトしていた箇所をcontrollerに渡すようにする。

routes.rb
get '*path', to: 'static_pages#spa_forward'

次に、リクエストオブジェクトからSPAで定義しているURLを識別します。

class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    # SPAのURLをパターンマッチングする
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      # SPAのURLの場合の処理を書く
    else
      redirect_to root_url
    end
  end

end

SPAのURLならパス/article/1を保持しつつindex.htmlを返す

3つ方針を考えました。

  1. URLをそのままにしてindex.htmlを返す
  2. SPAのURLをクエリパラメーターに退避して、ブラウザでSPAが自力で展開する
  3. index.htmlにカスタムデータ属性でURLを書き込み、ブラウザでSPAが自力で展開する

1.URLをそのままにしてindex.htmlを返す

もっともシンプルで自然な形です。URLを加工しないので、ブラウザのLocationに/article/1がセットされ、SPAのルーターがキャッチしたらルーティングを開始します。

class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      render file: 'public/index.html'
    else
      redirect_to root_url # アセットファイルなどのその他はルートに返す
    end  
  end

end

しかし、status200を返しつつもindex.htmlが返却されません。response headerがtext/plainだしEtag貼られてるしで、railsのAPIモードが何かしら原因ぽさあるがどうすればいいか調べてもわからず断念。railsでなければ、というかAPIモードで無ければこれで十分のはず...。

2.SPAのURLをクエリパラメーターに退避して、ブラウザでSPAが自力で展開する

採用した形式です。URIオブジェクトを起こし、クエリーパラメーターにto_paramでエンコーディングしたパスをセットします。その後はルート/にリダイレクトすれば、ブラウザではhttps://hoge.com/?path=%2Farticle%2F1でURLが認知されます。

class StaticPagesController < ApplicationController

  (...)

  def spa_forward
    if request.original_fullpath =~ /^\/(article|user)(|\/.*)$/
      uri = URI('/')
      # クエリパラメーターにSPAのパス`/article/1`をセットする
      uri.query = { path: request.original_fullpath}.to_param
      # ルートにリダイレクトする
      redirect_to uri.to_s # -> /?path=%2Farticle%2F1
    else
      redirect_to root_url # アセットファイルなどのその他はルートに返す
    end  
  end

end

3.index.htmlにカスタムデータ属性でURLを書き込み、ブラウザでSPAが自力で展開する

軽くindex.htmlにtag("div", :data => {:path => @path})あたりをセットするようなノリで考えたが、やはりAPIモードの壁にあたり断念。やるならstringで自力でviewを作りファイルに書き出しtextで返す感じかな。rails APIモードはネイティブアプリやマイクロサービスのためにあるんやね、webページはほんと持てないね。

フロントでindex.htmlを展開しつつ該当パス/article/1へのルーティングを行う

クエリーパラメーターで渡されたパスがLocationにセットされているので、historyAPIでLocaitonから取り出しデコードして再びLocationにセットします。

app.component.ts
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { UrlSerializer } from '@angular/router';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {

  constructor(
    private location: Location,
    private urlSerializer: UrlSerializer,
  ) {
    // path文字列が含まれているならLocationを書き換える
    if (this.location.path().match(/path/)) {
      // URLをパースしてクエリーパラメーターをデコードする
      const urlTree = this.urlSerializer.parse(this.location.path());
      // クエリパラーメーターに退避していたSPAのパスをLocationにセットする
      this.location.replaceState(urlTree.queryParams['path']);
    }
  }

}

urlTreeオブジェクトの中身はこんなんになっています。%2Farticle%2F1/article/1に戻っています。

{
  "root":{
    "segments":[],
    "children":{},
    "parent":null
  },
  "queryParams":{
    "path":"/article/1"
  },
  "fragment":null
}  

できたこと

  • ブラウザのアドレスバーにhttps://hoge.com/article/1を手入力して/article/1を開ける
  • 別サイトのaタグにhttps://hoge.com/article/1を埋め込んで/article/1を開ける
  • iPhoneアプリのtwitter/LINE/facebookのweb viewで/article/1を開ける
  • はてなブックマークして/article/1を開ける
  • mobile Safariのブックマークから/article/1を開ける

( ゚∀゚)o彡゜シェア!シェア!