create-react-appとMAMPで開発して、レンタルサーバーで動かしてみた設定

環境構築を頑張りたくなかったので、
Reactはcreate-react-appで環境を作成、
PHPはMAMPで動かしてみました。

その際に自分が設定したことをせっかくなので投稿させていただきました。m(_ _)m

また私は、React、PHPの初心者です。間違っていたら教えていただけるとありがたいです。

---

私が最終的に作成したコードはこちらです

- https://github.com/okumurakengo/react-mamp-app

# create-react-appでプロジェクト作成

```bash
# mampルートへ移動
cd /Applications/MAMP/htdocs

# mampルートでreactのプロジェクトを作成する
npx create-react-app .

# 今回は簡単に見た目をかっこよくできる material-ui も使いました
yarn add @material-ui/core @material-ui/icons
```

# Reactのプロジェクトを起動

```bash
yarn start 
```

<img width="612" alt="Screen Shot 2019-10-06 at 17.36.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/17a7edd4-9066-d6a6-c2f8-174dfb964aaa.png">

# PHPで簡単にapiを作成

mampのルートにcreate-react-appでプロジェクトを作成しました。
そこに`api`フォルダを作成して、phpでapiを動かすことにしました。

---

`/api/`以下のアクセスは必ず、`./api/index.php`が読み込まれるように設定

```./api/.htaccess
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]
```

```bash
$ composer require vlucas/phpdotenv
```

```./api/index.php
<?php

require __DIR__ . '/../vendor/autoload.php';

$dotenv = Dotenv\Dotenv::create(dirname(__DIR__));
$dotenv->load();
$dotenv->required('DB_CONNECTION')->notEmpty();
$dotenv->required('DB_HOST')->notEmpty();
$dotenv->required('DB_DATABASE')->notEmpty();
$dotenv->required('DB_USERNAME')->notEmpty();
$dotenv->required('DB_PASSWORD')->notEmpty();

try{
    $pdo = new PDO(
        sprintf('%s:dbname=%s;host=%s;charset=utf8', getenv('DB_CONNECTION'), getenv('DB_DATABASE'), getenv('DB_HOST')),
        getenv('DB_USERNAME'), 
        getenv('DB_PASSWORD'), 
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]
    );
    $pdo->exec(
        'CREATE TABLE IF NOT EXISTS todos (
            id   INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
            text VARCHAR(255) NOT NULL
        );'
    );

    header('Content-Type: application/json; charset=utf-8');

    [, , $target] = $uri = explode('/', $_SERVER["REQUEST_URI"]);
    switch($target) {
        case 'list':
            echo json_encode($pdo->query('SELECT * FROM todos')->fetchAll());
            exit;
        case 'add':
            $stmt = $pdo->prepare('INSERT INTO todos(text) VALUES(?)');
            $stmt->execute([$_POST['text']]);
            json_encode(['res' => 'ok']);
            exit;
        case 'delete':
            $stmt = $pdo->prepare('DELETE FROM todos WHERE id = ?');
            $stmt->execute([end($uri)]);
            exit;
        default:
            http_response_code(404);
            echo '404 Not Found';
            exit;
    }
} catch (PDOException $e) {
    http_response_code(500);
}
```

```:.env
APP_URL=http://localhost:3000
REACT_APP_API_URL=$APP_URL/api

DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=testdb
DB_USERNAME=root
DB_PASSWORD=root
```


# MAMPへproxyの設定をする

- create-react-appを使うとポート3000で起動
- mampはデフォルトでポート8888でサーバーが起動

なので、package.jsonで以下のように設定すると、
`fetch(http://localhost:3000/api)` のリクエストが、`fetch(http://localhost:8888/api)`へとproxyでうまくリクエストできました。

```package.json
"proxy": "http://localhost:8888"
```

# 簡単にTodoリストを作成

Reactとphpのapiで簡単に動くものを作りました

![T3zKJSRXgb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/c7215207-8a64-c7fa-6156-26da122038d7.gif)


```jsx:src/App.js
import React, { useState, useEffect } from 'react';
import Container from '@material-ui/core/Container';
import Box from '@material-ui/core/Box';
import TodoHeader from './components/TodoHeader'
import TodoList from './components/TodoList'

function App() {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState([]);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => { 
    getTodos()
  }, []);

  async function getTodos() {
    const res = await fetch(`${process.env.REACT_APP_API_URL}/list`)
    setIsLoading(false)
    setTodos(await res.json())
  }

  async function addTodo(e) {
    e.preventDefault()
    setIsLoading(true)
    await fetch(`${process.env.REACT_APP_API_URL}/add`, { method: 'POST', body: new FormData(e.target) })
    setText('')
    getTodos()
  }

  async function deleteTodo(id) {
    if (!window.confirm('削除しますか？')) {
      return;
    }
    setIsLoading(true)
    await fetch(`${process.env.REACT_APP_API_URL}/delete/${id}`, { method: 'POST' })
    getTodos()
  }

  return (
    <Container maxWidth="sm">
      <Box m={5}>
        <TodoHeader {...{ text, setText, addTodo }} />
        {isLoading ? <>Loading...</> : <TodoList {...{ todos, deleteTodo }} />}
      </Box>
    </Container>
  );
}

export default App;
```

```jsx:src/components/TodoHeader.js
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles(theme => ({
    button: {
        margin: theme.spacing(1),
    },
}));

export default function TodoHeader({ text, setText, addTodo }) {
    const classes = useStyles();

    return (
        <form onSubmit={addTodo}>
            <TextField
                name="text"
                label="Add Todo"
                value={text}
                onChange={({ target : { value } }) => setText(value)}
            />
            <Button
                type="submit"
                variant="contained"
                color="primary"
                className={classes.button}
                disabled={!text.trim().length}
            >
                追加
            </Button>
        </form>
    )
}
```

```jsx:src/components/TodoList.js
import React,{ Fragment } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider';
import Button from '@material-ui/core/Button';
import CreateIcon from '@material-ui/icons/Create';

export default function TodoList({ todos, deleteTodo }) {
  return (
    <List>
      {todos.map(({ id, text }) => (
        <Fragment key={id}>
          <ListItem button>
            <CreateIcon />
            <ListItemText>{text}</ListItemText>
            <Button 
              variant="contained" 
              color="secondary" 
              onClick={() => deleteTodo(id)}
            >
              削除
            </Button>
          </ListItem>
          <Divider />
        </Fragment>
      ))}
    </List>
  );
}
```

# 本番環境用に設定してみた

### `.env.production`を用意

```:.env
APP_URL=http://localhost:3000
REACT_APP_API_URL=$APP_URL/api

DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=testdb
DB_USERNAME=root
DB_PASSWORD=root
```

```:.env.production
APP_URL=http://xxxxx.ne.jp
REACT_APP_API_URL=$APP_URL/api

DB_CONNECTION=mysql
DB_HOST=localhost
DB_DATABASE=testdb
DB_USERNAME=xxxxx
DB_PASSWORD=xxxxx
```

### create-react-app

create-react-appだと、`yarn start`で`.env` が読み込まれ、 `yarn build` で `.env.production` が読み込まれますので、
特に考えずにそのままの設定で行いました

※.envの命名規則で読み込まれる順番なども決まるようです。

参考：https://create-react-app.dev/docs/adding-custom-environment-variables

![Screen Shot 2019-10-06 at 17.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/8d04d772-6fa0-0ca9-dcb6-06a0d56bf84a.png)

### php

開発環境では、`.env`を読み込み、本番環境では、`.env.production`を`.env`にリネームして、それを読み込むという設定にしてみました。
設定はwebpackで行いました。

### webpackの設定を追加

`yarn build`するときに、作成される`build`フォルダにphp関連のファイルもコピーされるようにします。

以下のPHP関連のファイルをコピー。

- `.env`
    - 本番環境用に、`.env.production`を`.env`にリネームして、それを`build`フォルダにコピーされるようにする
- `api/index.php`
- `vendor/`

---


webpackでコピーするために、`copy-webpack-plugin`を使用します。
create-react-appにちょっと設定を追加するだけですので、`eject`せずに`react-app-rewired`を使って設定を追加します。

- [copy-webpack-plugin - github](https://github.com/webpack-contrib/copy-webpack-plugin)
- [react-app-rewired](https://github.com/timarney/react-app-rewired)

```
yarn add -D react-app-rewired copy-webpack-plugin
```

##### package.jsonを変更

公式の内容に従って変更します

```diff:package.json
  "scripts": {
-    "start": "react-scripts start",
-    "build": "react-scripts build",
-    "test": "react-scripts test",
-    "eject": "react-scripts eject"
+    "start": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-app-rewired test",
+    "eject": "react-app-rewired eject"
  },
```

##### config-overrides.jsを作成

```config-overrides.js
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = function override(config, env) {
    if (!config.plugins) {
        config.plugins = [];
    }

    if (process.env.NODE_ENV === 'production') {
        config.plugins.push(new CopyWebpackPlugin([
            {from: 'api', to: 'api'},
            {from: 'vendor', to: 'vendor'},
            {from: '.env.production', to: '.env', toType: 'file'},
        ]));
    }

    return config;
}
```

この状態で`yarn build`ととするとうまくいきました

---

`yarn build`実行前

```bash
.
├── api # yarn buildするときに一緒にビルドのフォルダに送るようにする
│   ├── .htaccess
│   └── index.php
├── node_modules
├── .env # 開発環境の設定
├── .env.production # 本番環境の設定、yarn buildするときに.envにリネームして、一緒にビルドのフォルダに送るようにする
├── composer.json
├── composer.lock
├── README.md
├── package.json
├── public
├── src
├── vendor # yarn buildするときに一緒にビルドのフォルダに送るようにする
└── yarn.lock
```

↓↓↓↓↓`yarn build`実行↓↓↓↓↓

```bash
.
├── api
│   ├── .htaccess
│   └── index.php
├── build # yarn buildで作成されるフォルダ、本番環境へは、このフォルダの中をそのままアップロードするだけで動くようにする
│   ├── api
│   │   ├── .htaccess
│   │   └── index.php
│   ├── vendor
│   ├── .env # .env.productinをリネームして設置
│   └── index.html
│   ... # その他自動で作られるファイル
├── node_modules
├── .env
├── .env.production
├── composer.json
├── composer.lock
├── README.md
├── package.json
├── public
├── src
├── vendor
└── yarn.lock
```

`build`フォルダ内をそのままレンタルサーバーにFTPでアップロードするだけで、動かすことができました。

<img width="612" alt="Screen Shot 2019-10-06 at 19.29.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/79047f61-c2ed-aa0e-c2c4-9520bc1f725b.png">

最後まで見ていただいてありがとうございました。m(_ _)m