PHP
JavaScript
laravel
React
redux

React/Redux + Laravelでインスタグラムライクな方法でWebアプリを作った

■はじめに

ReactとPHPの連携で検索するとcomponentDidMountのときにfetchでデータを取ってくる方法が多いように思いますが、これじゃない感がありました。
たまたまインスタグラムのページのソースコードでも眺めてみようと思い、見ていると、reactを使っているらしいことがわかりましたが、それより注目したのがwindow._sharedDataというJSONデータが埋め込まれていることでした。
フレームワークでページを動的に生成しているものと思われます。
そこでこのインスタグラムライクなwindow._sharedDataを埋め込む方法でアプリを作成してみました。

■1. create-react-app

まずReactでアプリを作成します。

create-react-app instagram_like
cd instagram_like
npm start

■2. インストール

reduxのインストール

cd instagram_like
npm install redux
npm install react-redux
npm install redux-devtools
npm install redux-thunk
npm install redux-logger

react-router-domのインストール

cd instagram_like
npm install react-router
npm install history
npm install react-router-dom
npm install react-router-redux

■3. ディレクトリ

instagram_like配下に次のディレクトリを作成

actions
components
constants
containers
reducers

■4. window._sharedDataの埋め込み

まずReact開発環境でアプリを作成するため、後でPHPで埋め込むデータwindow._sharedDataのテストデータをpublic/index.htmlに埋め込んでおきます。

public/index.html
    <script>
    window._sharedData= {
        "entry_data": {
            "PostPage":[ {
                "graphql": {
                    "shortcode_media": {
                        "__typename":"GraphImage",
                        "id":"1418168299602868211",
                        "shortcode":"BOuWI4BjHfz",
                        "dimensions": {
                            "height": 719, "width": 1080
                        }
                        ,
                        "media_preview":"ACob1zGkqYPzcYyeprElgMMnyD5e/t/n/wCvW0uFQDtgD9Kb5qqxzg5GccflUvVgZDQM3J6euD/OqsgYdRnH+cd6157lYjs+7GTwePzA7j8fpmocxiQCfJDcLgdSfbFTZjMQncMjtSYX1q6YcFgV2+nvyeegqMW3HVfzphsXkupN6oxymOmM9B7c46fXNVmnlLuc/KnVf9jOOPw5NRiRgNoOACcfmf8APNQRE/Oe4Rv6VduvkG1iWXcuC2flbap5wQMFdvsB/k1caYq/3wACCfz79wB6YHaqisXADchSuB6c1sGyhMTOV+ZupyeefrVPXQnYz7y4OFwQ2c4I5Hv9ar7F7gZ78mkdAblIv4BgAfhn69akkjXceO5/nWb8il3Z/9k=",
                        "display_url":"https://scontent-nrt1-1.cdninstagram.com/vp/6e66efbd25293a921cd22e47c17764e0/5B9C768B/t51.2885-15/e35/15624585_764711637019057_7120075205669552128_n.jpg",
                        "display_resources":[ {
                            "src": "https://scontent-nrt1-1.cdninstagram.com/vp/495c961e30443a6f995090a6bead1c4a/5B856AEA/t51.2885-15/s640x640/sh0.08/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 640, "config_height": 426
                        }
                        ,
                        {
                            "src": "https://scontent-nrt1-1.cdninstagram.com/vp/0fc749d0cee710512e1960efdfd30cbc/5B93CCC7/t51.2885-15/s750x750/sh0.08/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 750, "config_height": 499
                        }
                        ,
                        {
                            "src": "https://scontent-nrt1-1.cdninstagram.com/vp/6e66efbd25293a921cd22e47c17764e0/5B9C768B/t51.2885-15/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 1080, "config_height": 719
                        }
                        ],
                        "is_video":false,
                        "edge_media_to_caption": {
                            "edges":[ {
                                "node": {
                                    "text": "\u3042\u3051\u307e\u3057\u3066\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\ud83c\udf8d\ud83c\udf8d\n\u307f\u306a\u3055\u3093\u3069\u3046\u304a\u904e\u3054\u3057\u3067\u3057\u3087\u3046\u304b\uff01\uff1f\n\uff12\uff10\uff11\uff17\u5e74\u3082\u30a4\u30ed\u30e0\u30af\u3092\u4f55\u5352\u3088\u308d\u3057\u304f\u304a\u9858\u3044\u3044\u305f\u3057\u307e\u3059\ud83c\udf05\n\u5199\u771f\u306f\u884c\u304f\u305e\u30fc\u7684\u306a\u5199\u771f\uff01"
                                }
                            }
                            ]
                        }
                        ,
                        "edge_media_to_comment": {
                            "count":1,
                            "page_info": {
                                "has_next_page": false, "end_cursor": null
                            }
                            ,
                            "edges":[ {
                                "node": {
                                    "id":"17846398948162135",
                                    "text":"\u3042\u3051\u307e\u3057\u3066\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\ud83c\udf8d\uff012016\u5e74\u306f\u30a4\u30ed\u30e0\u30af\u306e\u97f3\u697d\u306b\u51fa\u4f1a\u3063\u3066\u3001\u30e9\u30a4\u30d6\u306b\u3082\u884c\u3051\u3066\u7d20\u6575\u306a\u5e74\u3067\u3057\u305f\u263a\ud83d\udc972017\u5e74\u3082\u30e9\u30a4\u30d6\u884c\u3051\u307e\u3059\u3088\u3046\u306b\uff01\u3053\u308c\u304b\u3089\u3082\u5fdc\u63f4\u3057\u3066\u307e\u3059(\u0e51\uff65\u0311\u25e1\uff65\u0311\u0e51)\uff01\uff01\uff01",
                                    "created_at":1483345890,
                                    "owner": {
                                        "id": "2977883515", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/e247d3d990af2efd5d3b2ee5c05b63c7/5B97A5D0/t51.2885-19/s150x150/23823421_518036085236629_1235652405907947520_n.jpg", "username": "krn___0111"
                                    }
                                }
                            }
                            ]
                        }
                        ,
                        "taken_at_timestamp":1483278857,
                        "edge_media_preview_like": {
                            "count":54,
                            "edges":[ {
                                "node": {
                                    "id": "1318483545", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/4448ff89af1aef43c7cdea2fd455bfa4/5B866180/t51.2885-19/s150x150/28766206_152048362133455_3871769183184224256_n.jpg", "username": "nnm813"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "282654007", "profile_pic_url": "https://scontent-frt3-1.cdninstagram.com/vp/856b9478629f7c2f4ae549c4c8cc5dd7/5B94597A/t51.2885-19/11906329_960233084022564_1448528159_a.jpg", "username": "ryusin5"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "354626907", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/694dbc7b80d476d200ca69a206ed910a/5B77E51F/t51.2885-19/s150x150/17662411_915294138573097_6100425398091251712_a.jpg", "username": "nnea26"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "3427312785", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/efdadbd7736b2ae2c04f0d4a3973cd81/5B83AFE2/t51.2885-19/s150x150/13636244_148622568894353_2026179178_a.jpg", "username": "mino_mucho"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "2680071939", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/fa7da14e359fa1e176a6a58485e62ba5/5B97062B/t51.2885-19/s150x150/30953906_165210784163203_5439584193577222144_n.jpg", "username": "b___chi03"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "2167399340", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/cc76a23f4b750da84e5da10632d72080/5B8E80E4/t51.2885-19/s150x150/30085659_182081129268861_5548356254688083968_n.jpg", "username": "rktsiii"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "1958134017", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/eb1b0e11fd332179a9d49f7b278b221b/5B7750C9/t51.2885-19/s150x150/26156903_135638420441941_4769086334918721536_n.jpg", "username": "akurahotam3201"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "1184178809", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/5ec0bfc77242987241b0947fd1156a3a/5B791B06/t51.2885-19/s150x150/29089751_1615548411892122_7820419247334490112_n.jpg", "username": "_kairi_t"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "1253575861", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/9570723853d74db6d434113223c5750c/5B771A90/t51.2885-19/s150x150/30603984_1631773820263962_1485500506171244544_n.jpg", "username": "goootrrr"
                                }
                            }
                            ,
                            {
                                "node": {
                                    "id": "1783453030", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/ed5f2ad408bd21b34d3c37840c6d8aeb/5B95ABD2/t51.2885-19/s150x150/27579913_1952399085076934_8398136771493232640_n.jpg", "username": "iro_mana_muku"
                                }
                            }
                            ]
                        }
                        ,
                        "owner": {
                            "id": "4159115721", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/dea5194bedde9964e31601f976b7f7c4/5B920B2D/t51.2885-19/s150x150/14719801_1675576302772916_3410902154188161024_a.jpg", "username": "iromuk", "blocked_by_viewer": false, "followed_by_viewer": false, "full_name": "\u30a4\u30ed\u30e0\u30af\u516c\u5f0f\u30a4\u30f3\u30b9\u30bf\u30b0\u30e9\u30e0", "has_blocked_viewer": false, "is_private": false, "is_unpublished": false, "is_verified": false, "requested_by_viewer": false
                        }
                    }
                }
            }
            ]
        }
        ,
        "hostname":"www.instagram.com"
    };
    </script>

■5. sharedDataをruduxする

reducers/sharedData.js
import {SET_SHARED_DATA_ACTION} from '../constants/ActionTypes'

const initialState = {
  sharedData: {}
}

const _sharedData = (state, action) => {
  switch (action.type) {
    case SET_SHARED_DATA_ACTION:
      return action.sharedData
    default:
      return state
  }
}

export const getSharedData = state =>
  state.sharedData

const sharedData = (state = initialState, action) => {
  return {
    sharedData: _sharedData(state.sharedData, action)
  }
}

export default sharedData;
actions/index.js
import * as types from '../constants/ActionTypes'

const setSharedDataAction = sharedData => ({
  type: types.SET_SHARED_DATA_ACTION,
  sharedData
})

export const setSharedData = sharedData => (dispatch, getState) => {
  dispatch(setSharedDataAction(sharedData))
}
constants/ActionTyps.js
export const SET_SHARED_DATA_ACTION = 'SET_SHARED_DATA_ACTION'
reducer/index.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import sharedData, * as fromSharedData from './sharedData'

export default combineReducers({
  routing: routerReducer,
  sharedData
})

export const getSharedData = state => fromSharedData.getSharedData(state.sharedData)
store.js
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import { createLogger } from 'redux-logger'
import createHistory from 'history/createBrowserHistory'
import { syncHistoryWithStore } from 'react-router-redux'
import reducer from './reducers'

const middleware = [ thunk ]
if (process.env.NODE_ENV !== 'production') {
  middleware.push(createLogger())
}

const store = createStore(
  reducer,
  applyMiddleware(...middleware)
)

const _history = createHistory()
export const history = syncHistoryWithStore(_history, store)

export default store

■6. sharedDataのセット

index.js
import React from 'react'
import ReactDOM from 'react-dom'
//import registerServiceWorker from './registerServiceWorker'
import { Provider } from 'react-redux'
import { Router, Route, Switch } from 'react-router-dom'
import store, {history} from './store'
import IndexPage from './containers/IndexPage'
import PhotoPage from './containers/PhotoPage'
import {setSharedData} from './actions'
import './index.css'

// Note: window._sharedData  はpublic/index.htmlでセットされる
let sharedData = window._sharedData
store.dispatch(setSharedData(sharedData))

const baseUrl = process.env.PUBLIC_URL

ReactDOM.render(
  <Provider store={store}>
    <Router history={history}>
      <Switch>
        <Route name="index" exact path={baseUrl + "/"}
          render={props =>
            <IndexPage photoUrl={baseUrl + "/photo"} {...props} />
          }
        />
        <Route name="photo" exact path={baseUrl + "/photo/:shortcode"} component={PhotoPage} />
      </Switch>
    </Router>
  </Provider>,
  document.getElementById('root')
);
//registerServiceWorker();

今回の方法ではregisterServiceWorkerは切っておいた方がいいです。切っておかないとServiceWorkerが静的ページを返してしまい、PHPフレームワークにリクエストが来ない現象が発生しました。

■7. ページの作成

container/App.js
import React from 'react';
import PropTypes from 'prop-types'
//import logo from './logo.svg';
import './App.css';

const App = ({children}) => (
  <div className="App">
    <header>
      Instagram Like
    </header>
    <hr/>
    {children}
    <hr/>
    <header>
      (c)2018 ryujimiya
    </header>
  </div>
)

App.propTypes = {
  children: PropTypes.node.isRequired
}

export default App;
container/IndexPage.js
import React from 'react'
import PropTypes from 'prop-types'
import App from './App'

const IndexPage = ({photoUrl}) => (
  <App>
    <a href={photoUrl + "/BOuWI4BjHfz"}>「あけましておめでとうございます🎍🎍
みなさんどうお過ごしでしょうか!?
2017年もイロムクを何卒よろしくお願いいたします🌅
写真は行くぞー的な写真!」</a>
  </App>
)

IndexPage.propTypes = {
  photoUrl: PropTypes.string.isRequired
}

export default IndexPage
container/PhotoPage.js
import React from 'react'
import App from './App'
import PhotoContainer from './PhotoContainer'

const PhotoPage = () => (
  <App>
    <PhotoContainer />
  </App>
)

export default PhotoPage
container/PhotoContainer.js
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { getSharedData } from '../reducers'

const PhotoPageContainer = ({sharedData}) => {
  const entryData = sharedData.entry_data
  const postPage = entryData.PostPage
  const graphQl = postPage[0].graphql
  const shortcodeMedia = graphQl.shortcode_media
  const shortcode = shortcodeMedia.shortcode
  const displayResources = shortcodeMedia.display_resources
  const imgSrc = displayResources[0].src
  const mediaToCaption = shortcodeMedia.edge_media_to_caption
  const caption = mediaToCaption.edges[0].node.text
  return (
    <div>
      <div><img src={imgSrc} alt={caption} /></div>
      <div>{caption}</div>
    </div>
  )
}

PhotoPageContainer.propTypes = {
  sharedData: PropTypes.object.isRequired
}

const mapStateToProps = state => ({
  sharedData: getSharedData(state)
})

export default connect(
  mapStateToProps,
  {}
)(PhotoPageContainer)

■8. ビルド

package.jsonにホスティングするURLパスを記述します。

package.json
  "homepage": "/instagram_like",

ビルドします。

npm run build

■9. デプロイ

ここからはサーバーの話になります。

生成されたbuildフォルダの中身をサーバーのLaravelアプリの
public/instagram_like
配下に格納します。

■10. .htaccessとindex.php

Laravelで処理できるように.htaccessとindex.phpをpublic/instagram_like/に格納します。

public/instagram_like/.htaccess
<IfModule mod_rewrite.c>
    <IfModule mod_negotiation.c>
        Options -MultiViews -Indexes
    </IfModule>

    RewriteEngine On

    # RewriteBaseの設定が必要
    RewriteBase /

    # Handle Authorization Header
    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    # Redirect Trailing Slashes If Not A Folder...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_URI} (.+)/$
    RewriteRule ^ %1 [L,R=301]

    # Handle Front Controller...
    #ディレクトリ除外をコメントアウト
    #RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

index.phpは一つ上の階層(/public)のLaravelのindex.phpを読み込むようにします。

public/instagram_like/index.php
<?php

include realpath(dirname(__FILE__) . '/../index.php');

これでLaravelを通してHTMLが出力されるようになります。

■11. Laravelのルーティング

routes/web.php
<?php

Route::get('/', 'InstagramLikeController@index');
Route::get('/photo', 'InstagramLikeController@photo');

■12. Laravelのコントローラーの作成

php artisan make:controller InstagramLikeController

app/Http/Controllers/InstagramLikeController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\InstagramLikeService;

class InstagramLikeController extends Controller
{
    public function index() {
        $svc = new InstagramLikeService();
        $sharedData = $svc->reqIndex();
        $sharedDataJson = json_encode($sharedData, JSON_UNESCAPED_UNICODE);
        return view('instagram_like.index', ['sharedData' => $sharedDataJson]);
    }

    public function photo($shortcode) {
        $svc = new InstagramLikeService();
        $sharedData = $svc->reqPhoto($shortcode);
        $sharedDataJson = json_encode($sharedData, JSON_UNESCAPED_UNICODE);
        return view('instagram_like.photo', ['sharedData' => $sharedDataJson]);
    }
}

■13. sharedDataの実装

sharedDataを生成するサービスクラスを用意します。
ここにロジックを書くことになります。
今回はテストデータを返すだけになっています。

app/InstagramLikeService.php
<?php

namespace App;

class InstagramLikeService
{
    public function reqIndex() {
        $sharedData = array('name' => 'sharedData');
        return $sharedData;
    }

    public function reqPhoto($shortcode) {
        $json = <<< 'EOM'
        {
            "name": "sharedData",
            "entry_data": {
                "PostPage":[ {
                    "graphql": {
                        "shortcode_media": {
                            "__typename":"GraphImage",
                            "id":"1418168299602868211",
                            "shortcode":"BOuWI4BjHfz",
                            "dimensions": {
                                "height": 719, "width": 1080
                            }
                            ,
                            "media_preview":"ACob1zGkqYPzcYyeprElgMMnyD5e/t/n/wCvW0uFQDtgD9Kb5qqxzg5GccflUvVgZDQM3J6euD/OqsgYdRnH+cd6157lYjs+7GTwePzA7j8fpmocxiQCfJDcLgdSfbFTZjMQncMjtSYX1q6YcFgV2+nvyeegqMW3HVfzphsXkupN6oxymOmM9B7c46fXNVmnlLuc/KnVf9jOOPw5NRiRgNoOACcfmf8APNQRE/Oe4Rv6VduvkG1iWXcuC2flbap5wQMFdvsB/k1caYq/3wACCfz79wB6YHaqisXADchSuB6c1sGyhMTOV+ZupyeefrVPXQnYz7y4OFwQ2c4I5Hv9ar7F7gZ78mkdAblIv4BgAfhn69akkjXceO5/nWb8il3Z/9k=",
                            "display_url":"https://scontent-nrt1-1.cdninstagram.com/vp/6e66efbd25293a921cd22e47c17764e0/5B9C768B/t51.2885-15/e35/15624585_764711637019057_7120075205669552128_n.jpg",
                            "display_resources":[ {
                                "src": "https://scontent-nrt1-1.cdninstagram.com/vp/495c961e30443a6f995090a6bead1c4a/5B856AEA/t51.2885-15/s640x640/sh0.08/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 640, "config_height": 426
                            }
                            ,
                            {
                                "src": "https://scontent-nrt1-1.cdninstagram.com/vp/0fc749d0cee710512e1960efdfd30cbc/5B93CCC7/t51.2885-15/s750x750/sh0.08/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 750, "config_height": 499
                            }
                            ,
                            {
                                "src": "https://scontent-nrt1-1.cdninstagram.com/vp/6e66efbd25293a921cd22e47c17764e0/5B9C768B/t51.2885-15/e35/15624585_764711637019057_7120075205669552128_n.jpg", "config_width": 1080, "config_height": 719
                            }
                            ],
                            "is_video":false,
                            "edge_media_to_caption": {
                                "edges":[ {
                                    "node": {
                                        "text": "\u3042\u3051\u307e\u3057\u3066\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\ud83c\udf8d\ud83c\udf8d\n\u307f\u306a\u3055\u3093\u3069\u3046\u304a\u904e\u3054\u3057\u3067\u3057\u3087\u3046\u304b\uff01\uff1f\n\uff12\uff10\uff11\uff17\u5e74\u3082\u30a4\u30ed\u30e0\u30af\u3092\u4f55\u5352\u3088\u308d\u3057\u304f\u304a\u9858\u3044\u3044\u305f\u3057\u307e\u3059\ud83c\udf05\n\u5199\u771f\u306f\u884c\u304f\u305e\u30fc\u7684\u306a\u5199\u771f\uff01"
                                    }
                                }
                                ]
                            }
                            ,
                            "edge_media_to_comment": {
                                "count":1,
                                "page_info": {
                                    "has_next_page": false, "end_cursor": null
                                }
                                ,
                                "edges":[ {
                                    "node": {
                                        "id":"17846398948162135",
                                        "text":"\u3042\u3051\u307e\u3057\u3066\u304a\u3081\u3067\u3068\u3046\u3054\u3056\u3044\u307e\u3059\ud83c\udf8d\uff012016\u5e74\u306f\u30a4\u30ed\u30e0\u30af\u306e\u97f3\u697d\u306b\u51fa\u4f1a\u3063\u3066\u3001\u30e9\u30a4\u30d6\u306b\u3082\u884c\u3051\u3066\u7d20\u6575\u306a\u5e74\u3067\u3057\u305f\u263a\ud83d\udc972017\u5e74\u3082\u30e9\u30a4\u30d6\u884c\u3051\u307e\u3059\u3088\u3046\u306b\uff01\u3053\u308c\u304b\u3089\u3082\u5fdc\u63f4\u3057\u3066\u307e\u3059(\u0e51\uff65\u0311\u25e1\uff65\u0311\u0e51)\uff01\uff01\uff01",
                                        "created_at":1483345890,
                                        "owner": {
                                            "id": "2977883515", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/e247d3d990af2efd5d3b2ee5c05b63c7/5B97A5D0/t51.2885-19/s150x150/23823421_518036085236629_1235652405907947520_n.jpg", "username": "krn___0111"
                                        }
                                    }
                                }
                                ]
                            }
                            ,
                            "taken_at_timestamp":1483278857,
                            "edge_media_preview_like": {
                                "count":54,
                                "edges":[ {
                                    "node": {
                                        "id": "1318483545", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/4448ff89af1aef43c7cdea2fd455bfa4/5B866180/t51.2885-19/s150x150/28766206_152048362133455_3871769183184224256_n.jpg", "username": "nnm813"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "282654007", "profile_pic_url": "https://scontent-frt3-1.cdninstagram.com/vp/856b9478629f7c2f4ae549c4c8cc5dd7/5B94597A/t51.2885-19/11906329_960233084022564_1448528159_a.jpg", "username": "ryusin5"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "354626907", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/694dbc7b80d476d200ca69a206ed910a/5B77E51F/t51.2885-19/s150x150/17662411_915294138573097_6100425398091251712_a.jpg", "username": "nnea26"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "3427312785", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/efdadbd7736b2ae2c04f0d4a3973cd81/5B83AFE2/t51.2885-19/s150x150/13636244_148622568894353_2026179178_a.jpg", "username": "mino_mucho"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "2680071939", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/fa7da14e359fa1e176a6a58485e62ba5/5B97062B/t51.2885-19/s150x150/30953906_165210784163203_5439584193577222144_n.jpg", "username": "b___chi03"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "2167399340", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/cc76a23f4b750da84e5da10632d72080/5B8E80E4/t51.2885-19/s150x150/30085659_182081129268861_5548356254688083968_n.jpg", "username": "rktsiii"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "1958134017", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/eb1b0e11fd332179a9d49f7b278b221b/5B7750C9/t51.2885-19/s150x150/26156903_135638420441941_4769086334918721536_n.jpg", "username": "akurahotam3201"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "1184178809", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/5ec0bfc77242987241b0947fd1156a3a/5B791B06/t51.2885-19/s150x150/29089751_1615548411892122_7820419247334490112_n.jpg", "username": "_kairi_t"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "1253575861", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/9570723853d74db6d434113223c5750c/5B771A90/t51.2885-19/s150x150/30603984_1631773820263962_1485500506171244544_n.jpg", "username": "goootrrr"
                                    }
                                }
                                ,
                                {
                                    "node": {
                                        "id": "1783453030", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/ed5f2ad408bd21b34d3c37840c6d8aeb/5B95ABD2/t51.2885-19/s150x150/27579913_1952399085076934_8398136771493232640_n.jpg", "username": "iro_mana_muku"
                                    }
                                }
                                ]
                            }
                            ,
                            "owner": {
                                "id": "4159115721", "profile_pic_url": "https://scontent-nrt1-1.cdninstagram.com/vp/dea5194bedde9964e31601f976b7f7c4/5B920B2D/t51.2885-19/s150x150/14719801_1675576302772916_3410902154188161024_a.jpg", "username": "iromuk", "blocked_by_viewer": false, "followed_by_viewer": false, "full_name": "\u30a4\u30ed\u30e0\u30af\u516c\u5f0f\u30a4\u30f3\u30b9\u30bf\u30b0\u30e9\u30e0", "has_blocked_viewer": false, "is_private": false, "is_unpublished": false, "is_verified": false, "requested_by_viewer": false
                            }
                        }
                    }
                }
                ]
            }
            ,
            "hostname":"www.instagram.com"
        }
EOM;
        $sharedData = json_decode ($json, true, 512, JSON_BIGINT_AS_STRING|JSON_OBJECT_AS_ARRAY);
        return $sharedData;
    }
}

■14. ビューの作成(sharedDataの埋め込み)

Reactでbuildしたときに生成されたindex.htmlをビューにコピーし、window._sharedDataの箇所にコントローラーから渡されたsharedDataを埋め込みます。

resources/views/layouts/instagram_like.blade.php
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <link rel="manifest" href="/instagram_like/manifest.json">
    <link rel="shortcut icon" href="/instagram_like/favicon.ico">
    <title>Instagram like</title>
    <link href="/instagram_like/static/css/main.29266132.css" rel="stylesheet">
</head>

<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <script>
        window._sharedData = {!! $sharedData !!};
    </script>

    <div class="container">
        @yield('content')
    </div>

    <script type="text/javascript" src="/instagram_like/static/js/main.83c4061d.js"></script>
</body>

</html>
resources/instagram_like/index.blade.php
@extends('layouts.instagram_like')

@section('content')
    <p>いんでっくす</p>
@endsection
resources/instagram_like/photo.blade.php
@extends('layouts.instagram_like')

@section('content')
    <p>ふぉと</p>
@endsection

■15. 完成!!!!!!!

以上で完成です。
インデックスページ/instagram_like/にアクセスするとReactで作成したページにPHPで付け加えた「いんでっくす」という文字がでてきます。

20180511_Instagram_like_indexページ.jpg

次にフォトページ/instagram_like/photo/BOuWI4BjHfzにアクセスすると、sharedDataからとってきた画像とコメントが表示されます。

20180511_instagram_like_photoページ.jpg

■ まとめ

React/ReduxとPHP Laravelをwindow._sharedDataを介して連携するというインスタグラムライクな方法でWebアプリを作りました。