Edited at

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

More than 1 year has passed since last update.


■はじめに

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アプリを作りました。