LoginSignup
37
46

More than 3 years have passed since last update.

REST APIを利用し、WordPressのフロントエンドをReactで構築する

Last updated at Posted at 2020-06-12

まえがき 2020.6.11投稿

WordPressの管理画面で
記事を投稿したりコンテンツを編集する機能はクライアント向けに残しておきたい。
しかしフロントエンドはモダンjsフレームワークであるReactを利用しSPAとしたい。
これが本記事の目的です。

この記事でできるようになること

・WordPressのフロントエンドを、Reactで組めるようになる
・WP REST APIをReactで使えるようになる
・カスタムフィールドの内容をReactでどう取得して出力するかわかる
・Reactでタブコンテンツを実装できるようになる
・Reactでハンバーガーメニューを実装できる
・json形式のデータをmapで出力できる

ハンバーガーメニューはReactでclass名をtoggleしています。
cssは適宜用意してください。
タブコンテンツも同様です。
※参考ハンバーガーボタン
※参考タブ

まずWordPress

URLを最終的に
https://example.com/
でReact公開する場合
https://example.com/wp/
などにWordPressを設置してください。
設置方法は特に説明しません。
Advanced custom fieldsでカスタムフィールドを設定しておく。

Reactプロジェクトを作成

$ create-react-app your-project-name

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

$ npm install react-router-dom

各ファイルの編集

まず./src/index.js

index.js

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch, Link } from 'react-router-dom';

import './style.css';
import * as serviceWorker from './serviceWorker';



const site_url = 'https://example.com/wp/';//ここにWordPress本体のURLを記述
const posts_url = site_url + 'wp-json/wp/v2/posts/?_embed';//ただの投稿一覧を取得する場合のREST API記述
const pages_url = site_url + 'wp-json/wp/v2/pages/3?_embed';//固定ページのID3を取得する場合
const pages_url2 = site_url + 'wp-json/wp/v2/posts/146?_embed';//投稿ID146を取得する場合
const listurl = site_url + 'wp-json/wp/v2/posts?categories=2&_embed';//categoryID2の投稿一覧を取得する場合
//_embedのパラメータを付与するのは、記事に設定されているサムネイル画像を取得するため

//ディスクリプションを取得
let description = document.getElementsByName('description')[0].content;
console.log(description);//取得したディスクリプションをconsoleで確認

//ヘッダコンポーネントを定義
class Header extends React.Component {
  render(){
    return (
      <header>
        <div>
          <h1><Link to='/'><img src="/img/logo.png" alt="ロゴ" /></Link></h1>
          <ul>
            <li><Link to='/'>HOME</Link></li>
            <li><Link to='/about/'>About</Link></li>
            <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
          </ul>
          <div id="menu">
            <App/>
          </div>
        </div>
      </header>
    );
  }
}

//パンくず
class Bread extends React.Component {
  render(){
    return (
      <div>
        <ul className="bread">
          <li><Link to='/'>HOME</Link></li>
          <li>{document.title}</li>
        </ul>
      </div>
    );
  }
}

//サイドバーコンポーネント
class Sidecat extends React.Component {
  render(){
    return (
      <div id="sidecat">
        <h2>カテゴリ別</h2>
        <ul>
          <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
        </ul>
      </div>
    );
  }
}

//スマホ表示時のハンバーガーメニュー用コンポーネント
class App extends React.Component {
  //this.state.isShowを宣言
  constructor(props) {
    super(props);
    this.state = {
      isShow : false
    }
    //toggleをbindしておかないと動かない(たぶん)
    this.toggle = this.toggle.bind(this)
  }
  //toggleメソッドを定義
  toggle(e) {
    this.setState({
      isShow: !this.state.isShow 
    });
  }
  render() {
    return (
      <div>
        <a className={'menu-trigger' + (this.state.isShow ? ' active' : '')}  onClick={this.toggle}>
          <span></span>
          <span></span>
          <span></span>
        </a>
        <ul className = {'menu' + (this.state.isShow ? ' active' : '')}>
          <li><Link to='/'>HOME</Link></li>
          <li><Link to='/about/'>About</Link></li>
          <li><Link to='/list/'>カテゴリID2の一覧</Link></li>
        </ul>
      </div>
    );
  }
}

//複数行テキストの改行を保持するためのコンポーネント [出典](http://qs.nndo.jp/display-multiline-text-by-react)
//カスタムフィールドで改行含むコンテンツを呼び出すとき、<MultiLineText></MultiLineText>で囲む
class MultiLineText extends React.Component {
  render() {
    const renderTexts = () => {
      if (typeof(this.props.children) === "string") {
        return this.props.children.split("\n").map((m,i) => <span key={i}>{m}<br/></span>)
      } else {
        return "";
      }
    }
    return (
      <div className={this.props.className}>
        {renderTexts()}
      </div>
    );
  }
}

//indexページのコンポーネント
class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      title: 'トップページ',
    };
  }
  componentDidMount(){
    document.title = 'トップページ';//titleタグ書き換え
    document.getElementsByName('description')[0].content = 'トップページのディスクリプション';//descriptionタグ書き換え
  }
  render() {
    return (
      <div>
        <Header/>
        <div id="main">
          <!--indexページの内容-->
        </div>
      </div>
    );
  }
}

//固定ページAboutのコンポーネント
class About extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
    };
  }

  componentDidMount(){
    document.getElementsByName('description')[0].content = 'Aboutのディスクリプション';
    document.title = 'Aboutのタイトル';
     fetch(pages_url)//pages_urlで定義した固定ページからpromiseでデータを取得
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });

     })
     .catch((error) =>{
       console.error(error);
     });
  }

  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);
    //data.acf.body1で、カスタムフィールドbody1の内容を取得し出力できる
    if(this.state.loading){
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="post">
                <h2>{data.title.rendered}</h2>
                <p>{data.acf.body1}</p>
                <MultiLineText>{data.acf.body2}</MultiLineText>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}

//個別記事ページ用コンポーネント(タブ機能つき)
class Post extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false,
      isShow : false,
      active1: 'active',
      active2: '',
      active3: '',
    }
    //toggle用にbind
    this.toggle1 = this.toggle1.bind(this);
    this.toggle2 = this.toggle2.bind(this);
    this.toggle3 = this.toggle3.bind(this);
  }
  //toggle1でtab1をアクティブに
  toggle1(e) {
    this.setState({active1:'active'});
    this.setState({active2:''});
    this.setState({active3:''});
  }
  //toggle2でtab2をアクティブに
  toggle2(e) {
    this.setState({active1:''});
    this.setState({active2:'active'});
    this.setState({active3:''});
  }
  //toggle3でtab3をアクティブに
  toggle3(e) {
    this.setState({active1:''});
    this.setState({active2:''});
    this.setState({active3:'active'});
  }
  componentDidMount(){
     //posts_urlに、URLパラメータで送られてきた投稿IDをくっつけてpromiseで投稿記事のデータを取得
     fetch(post_url+this.props.match.params.id+'?_embed')
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });

     })
     .catch((error) =>{
       console.error(error);
     });
  }
  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);
    //data.acf.rubyでカスタムフィールドrubyの値を取得 例外として、件名、本文など一部フィールドはdata.title.renderedで取得
    if(this.state.loading){
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="post">
                <h2>{data.title.rendered}<span>{data.acf.ruby}</span></h2>
                  <nav>
                    <a href='javascript:void(0);' data-tab='one' onClick={this.toggle1} className={(this.state.active1 )}>TAB1</a>
                    <a href='javascript:void(0);' data-tab='two' onClick={this.toggle2} className={(this.state.active2 )}>TAB2</a>
                    <a href='javascript:void(0);' data-tab='three' onClick={this.toggle3} className={(this.state.active3 )}>TAB3</a>
                    <div className='clear'></div>
                  </nav>            
                  <div className="tabwrap">
                    <div className='tabContainer'>
                      <div id='one' className={('Tabcondent ' + this.state.active1 )}>
                        <!--tab1のコンテンツ-->
                      </div>
                      <div id='two' className={('Tabcondent ' + this.state.active2)}>
                        <!--tab2のコンテンツ-->
                      </div>
                      <div id='three' className={('Tabcondent ' + this.state.active3)}>
                        <!--tab3のコンテンツ-->
                      </div>
                    </div>
                  </div>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}

//投稿記事の一覧コンポーネント
class List extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      loading: false
    };
  }

  componentDidMount(){
    document.getElementsByName('description')[0].content = 'このコンポーネント用ディスクリプション';
    document.title = 'このコンポーネント用タイトルタグ';
     fetch(list_url)
     .then((response) => response.json())
     .then((responseJson) => {
       this.setState({
         loading: true,
         data: responseJson,
       });

     })
     .catch((error) =>{
       console.error(error);
     });
  }
  render() {
    const data = this.state.data;
    const json = JSON.stringify(data);

    //一覧に含まれる記事の数だけmapでまわす
    if(this.state.loading){
      const side = data.map((value,i) => (
        <li key={i}><Link to={'/post/' + value.id}>{value.title.rendered}</Link></li>
      ))
      const list = data.map((value,i) => (
        <div key={i}>
          <img className='list_image' src={value.acf.list_image}/>
          <Link to={'/post/' + value.id}>詳細を見る</Link>
        </div>
      ))
      return(
        <div>
          <Header/>
            <div id="main">
              <Bread />
              <div id="main_content">{list}</div>
              <div id="side">
                <Sidecat />
                <ul id="sidemenu">{side}</ul>
              </div>
            </div>
        </div>
      );
    }else{
      return(
        <div>
          <Header/>
          <div id="main">
            <p>Loading...</p>
          </div>
        </div>
      );
    }
  }
}




ReactDOM.render((
  <BrowserRouter>
    <Switch>
      <Route path='/' exact component={Index}/>
      <Route path='/about/' component={About}/>
      <Route path='/list/' component={List}/>
      <Route path='/post/:id' component={Post}/>
      </Switch>
  </BrowserRouter>
  ), document.getElementById('root')
);


index.htmlの内容

./public/index.html

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="デフォルトのディスクリプション">
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <script src="https://kit.fontawesome.com/409eaacc50.js" crossorigin="anonymous"></script>
    <title>デフォルトのタイトル</title>
    <base href="/">
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="content"></div>
    <footer>
    Copyright © 
    </footer>

  </body>
</html>

style.cssは好きなように

./src/style.css

最後に

以上で実装終わりです。
ウェブサーバに公開する場合は
yarn build または npm run buildして
buildの中身をアップロードすればいけると思います。

自分はこれをやるためにあれこれ検索してまわり、
「できる」ということはわかっていたのですが
具体的に細かく自分のレベルで理解できるほどの記事を見つけることができず、
いろいろな記事や情報を調べつつ、
メンターの方を探して質問させていただいたりしつつ、やっと完成したので
作り方を残したいと思います。

37
46
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
46