本投稿は TECOTEC Advent Calendar 2021 の23日目の記事です。
こんにちは、次世代デジタル基盤開発事業部の渡邊です。
今回の記事では、自己学習を目的として「React+Node.js+MySQL」の組み合わせで「マンガ管理アプリ」を作ってみた時に取り組んだことを備忘録として残していきます。
なお、今回の取り組みでは「予めMySQLに登録されたマンガ情報をReactで構築した検索画面から検索して表示させる」ことを目標としています。
イメージとしては以下のようなものを作ります。(かなりシンプルです・・・)
今回の記事は長い記事となりますので、先に構成をご紹介します。
目次
環境
今回の取り組みで使っている環境は以下の通りです。
- WSL2
- Ubuntu 20.04.1 LTS
- Node.js 16.13.0
- MySQL 8.0.27
- npm 8.1.0
また、以下のパッケージも使用していきます。
- mysql2
- @material-ui/core
- axios
環境構築
この章では、環境構築を行っていきます。 ただし、MySQLに関してはインストールだけでなく初期設定や一部作成/データ投入も行っていきます。
それでは、流れに沿って環境を構築していきます。
1.WSL/Ubuntu/Node.js/npmのインストール
いきなり説明がシンプルになってしまいますが、Microsoftが提供している下記ドキュメントを参考にインストールを実施します。
https://docs.microsoft.com/ja-jp/windows/dev-environment/javascript/nodejs-on-wsl
2.MySQLのインストール
こちらもMicrosoftが提供しているドキュメントを参考に、Ubuntu上にMySQLをインストールします。
https://docs.microsoft.com/ja-jp/windows/wsl/tutorials/wsl-database
インストールが完了したら、冒頭でお伝えした通り初期設定などの追加作業も行っていきます。
[初期設定]
まずはMySQLの初期設定を行っていきます。インストールしただけでは設定として不十分ということなので、下記記事を参考に設定を行っていきます。
https://www.yokoweb.net/2020/08/16/ubuntu-20_04-server-mysql/
具体的には、記事で紹介されている下記コマンドを実行して初期設定を行います。
sudo mysql_secure_installation
[DB作成/テーブル作成/ユーザー作成/ユーザー権限付与]
MySQLの初期設定は完了していますので、MySQLで必要となるものを作成していきます。
今回作成する情報は以下を想定して進めていきます。
DB名 :comic
テーブル名:list
ユーザー名:testuser001
各作業ごとに実行していくクエリは以下になります。
やること | 実行クエリ |
---|---|
DB作成 | create database comic; |
テーブル作成 | create table list (title varchar(255),category varchar(255),volume int,story int); |
ユーザー作成 | create user 'testuser001'@'localhost' IDENTIFIED BY 'パスワード'; |
ユーザーへのDB操作権限付与(※1) | grant all privileges on comic.* to 'testuser001'@'localhost'; |
※1:以下のようなクエリだと権限付与とユーザー作成を同時に行います。
grant all privileges on DB名.* to 'ユーザー名'@'ホスト名' IDENTIFIED BY 'パスワード';
[MySQLの日本語化対応]
MySQLで日本語を含むデータの登録ができるように設定を行っていきます。
そのためには、MySQLの設定ファイルに下記定義を追加する必要があります。
[client] default-character-set=utf8 [mysql] default-character-set=utf8 [mysqldump] default-character-set=utf8 [mysqld] character-set-server=utf8
因みに、設定ファイルは構築した環境によって配置場所が異なるみたいなので注意してください。ネットで調べるとmy.confというファイルに設定すると書かれていましたが当方の場合はmysql.confというファイル名になっていました。
また、編集するにはファイルの権限を変える必要があるのでchmodコマンドなどを使って権限を変更してから実施する必要があります。
ただし、それを終えた後は必ず権限を戻さないとMySQLへ接続する際に下記のような警告メッセージが出てくると思います。
mysqld: [Warning] World-writable config file '/etc/mysql/my.cnf' is ignored.
[データ投入]
先ほどの設定を行ったことで、日本語を含むデータを投入できるようになっているはずです。
作成したユーザーを使ってMySQLに接続して、作成したDB内で作ったテーブルへデータを投入していきます。
今回投入するデータは以下になります。
タイトル(title) | ジャンル(category) | 読んだ巻数(volume) | 読んだ話数(story) |
---|---|---|---|
タイトル001 | アクション | 20 | 0 |
タイトル002 | スポーツ | 0 | 160 |
タイトル003 | ファンタジー | 10 | 120 |
それでは、insert文を使ってデータを投入します。
insert into list values ('タイトル001','アクション',20,0); insert into list values ('タイトル002','スポーツ',0,160); insert into list values ('タイトル003','ファンタジー',10,120);
select文でlistの中身を確認すると、以下のようになっていると思います。
mysql> select * from list; +-----------------+--------------------+--------+-------+ | title | category | volume | story | +-----------------+--------------------+--------+-------+ | タイトル001 | アクション | 20 | 0 | | タイトル002 | スポーツ | 0 | 160 | | タイトル003 | ファンタジー | 10 | 120 | +-----------------+--------------------+--------+-------+ 3 rows in set (0.00 sec)
確認が取れたら、最後は忘れずにコミットします。
commit;
長くなりましたが、以上でMySQLの事前準備は完了となります。
あと、今回のアプリ開発に直接の影響はありませんが開発しやすいようにWindowsターミナルを入れるのもおススメです。
https://docs.microsoft.com/ja-jp/windows/terminal/install
これで、環境構築は以上になります。
次からは、実際に作った環境にプロジェクトを構築して各環境ごとの接続確認を行っていきます。
接続確認
この章では、プロジェクトを作成しながらMySQLも含めた接続確認を行っていきます。 React+Node.js(express)+MySQLと複数の要素が絡んでくるので、今回は接続確認を段階的に分けて行っていきたいと思います。以下のような流れで進めていきます。
1.React+Node.js
2.Node.js+MySQL
3.React+Node.js+MySQL
1.React+Node.js
まず初めに、React+Node.jsの接続確認を行います。ここでは、Node.js内で用意した値をブラウザ表示できるか確認していきます。
ただ、こちらに関しては有益な記事がございますので下記に沿って進めていきます。
https://reffect.co.jp/react/front-react-back-node
これによって、クライアント側はcreate-react-appプロジェクト、バックエンドはexpressプロジェクトを使用した構成が出来上がります。
接続確認するために2つのターミナルを立ち上げた後、expressプロジェクトとcreate-react-appプロジェクトを分けて操作できるようにします。 そうしたら、各プロジェクト内で以下のコマンドを実行してください。
npm start
実行するとサービスが動き出して、ブラウザが立ち上がった後で以下のように表示されているのが確認できるかと思います。
これで、React+Node.jsの接続確認は以上となります。
2.Node.js+MySQL
次に、Node.js+MySQLの接続確認を行います。 ここでは、Node.js内でコネクションを定義してクエリを実行し、MySQLに登録されているデータを取得してブラウザ上に表示されることを確認していきます。
それにあたって、接続するために必要な実装とパッケージについてご紹介します。
[mysql2パッケージのインストール]
Node.jsからMySQLのデータを操作するためには、MySQLへ問い合わせを行えるパッケージが必要になります。ここでは、expressからMySQLへ接続できるようにmysql2パッケージをインストールします。
ネットで調べるとmysqlパッケージのインストールを紹介している記事が多いですが、色々調べたところmysqlではなくmysql2パッケージを使用したほうが良さそうです。
詳しくは下記の記事などが参考になりますので、気になる方は一読してみてください。
https://stackoverflow.com/questions/50093144/mysql-8-0-client-does-not-support-authentication-protocol-requested-by-server https://stackoverflow.com/questions/44946270/er-not-supported-auth-mode-mysql-server
それでは、expressプロジェクト内で以下のコマンドを実行してmysql2をインストールします。
npm install mysql2
mysql2のインストールが完了したら、MySQLへ接続できるようにbackend/index.jsの中身を実装していきます。
backend/index.js
const express = require('express') const mysql = require('mysql2') const app = express() const port = process.env.PORT || 3001 const connection = mysql.createConnection({ host: 'localhost', user: 'testuser001', password: 'パスワード', database: 'comic' }); app.get("/api", (req, res) => { connection.query( 'SELECT * FROM `list`', function(err, results, fields) { if(err) { console.log("接続終了(異常)"); throw err; } res.json({message: results[0].title}); } ); console.log("接続終了(正常)"); }); app.listen(port, () => { console.log(`listening on *:${port}`); })
これで接続できるようになっていますので、ブラウザを起動して下記URLへリクエストしてみてください。
そうすると、うまくいっていればブラウザ上に取得結果が表示されるはずです。
以上で、Node.js+MySQLの接続確認は終了になります。
3.React+Node.js+MySQL
最後は、上記1,2を掛け合わせた接続確認を行います。ただ、これに関してはそんなに難しくありません。[1.React+Node.js]と同様の方法で下記コマンドを実行してください。
npm start
サービスが起動して、ブラウザ上で以下のように表示されたら成功です!
これ以降の作業では、create-react-appプロジェクトで[npm start]を実行する際は別ターミナルでexpressプロジェクトも同様のことを実施している前提で進めますのでご認識おきください。
長くなりましたが、以上で接続確認は終了となります。 では、次からは本格的に実装を行っていきます。
UIの実装
さて、この章ではUIの実装についてご紹介していきます。すでに記事が長くなってしまっているので、改めて実装したいアプリを確認します。 今回作ろうとしているものは、以下のような検索機能を持ったアプリになります。
そして、ここでは3つのコンポーネントを用意して実装していきます。
1.Headerコンポーネント
2.Formコンポーネント
3.Listコンポーネント
各コンポーネントを実装していく中で、今回はUI周りをReactのライブラリであるMaterial UIを使っていきます。create-react-appプロジェクト内で下記コマンドを実行します。
npm install @material-ui/core
それでは、各コンポーネントの実装を行っていきます。
1.Headerコンポーネントの実装
Material UIの公式ドキュメントを活用します。
https://v4.mui.com/components/app-bar/#simple-app-bar
ただし、今回はメニューバーやログイン機能の実装はしないので、不要な部分は取り除きます。取り除いた結果が以下になります。
Header.jsx
import React from 'react' import AppBar from '@material-ui/core/AppBar'; import Box from '@material-ui/core/Box'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; export default function Header() { return ( <> <Box sx={{ flexGrow: 1 }}> <AppBar position="static"> <Toolbar> <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> Comic </Typography> </Toolbar> </AppBar> </Box> </> ); }
2.Formコンポーネントの実装
このコンポーネントでは、検索キーワードを入力する欄と検索ボタンだけ実装します。実装した結果が以下になります。
Form.jsx
import React from 'react' import TextField from "@material-ui/core/TextField"; import Grid from '@material-ui/core/Grid'; import Button from '@material-ui/core/Button' export default function Form() { return ( <> <Grid container rowSpacing={8}> <form> <Grid item> <TextField label="タイトル" /> </Grid> <Grid item> <Button color="inherit" onClick={funTest}>検索</Button> </Grid> </form> </Grid> </> ) }
3.Listコンポーネントの実装
Material UIの公式ドキュメントを活用します。 https://v4.mui.com/components/tables/#basic-table
この時点ではデータの取得とかはできないので、一旦は仮のデータをセットして表示を確認します。実装した結果が以下になります。
List.jsx
import React from 'react' import { makeStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; export default function List() { const useStyles = makeStyles({ table: { minWidth: 650, }, }); const classes = useStyles(); return ( <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>タイトル</TableCell> <TableCell align="right">ジャンル</TableCell> <TableCell align="right">巻数</TableCell> <TableCell align="right">話数</TableCell> </TableRow> </TableHead> <TableBody> <TableRow key="タイトルA"> <TableCell component="th" scope="row"> タイトルA </TableCell> <TableCell align="right">アクション</TableCell> <TableCell align="right">20</TableCell> <TableCell align="right">100</TableCell> </TableRow> </TableBody> </Table> </TableContainer> ); }
3つのコンポーネント実装が完了したので、それらを使えるようにApp.jsxを修正します。 とはいっても、やることはそんなに難しくなくて作成したコンポーネントをインポートして呼び出せるように各コンポーネントを定義するだけです。
その結果が以下になります。
App.jsx
import React from 'react' import Header from './modules/Header' import Form from './modules/Form' import List from './modules/List'; export default function App() { return ( <> {/* ヘッダー */} <Header /> {/* フォーム */} <Form /> {/* テーブル(検索結果の表示) */} <List /> </> ); }
この状態でcreat-react-appプロジェクトで「npm start」を実行すると、サービスが起動してから以下のようにブラウザで表示されます。
これで、UIの実装は以上となります。
useStateの利用
この章では、ReactのuseStateを使って検索キーワードを変数に保持させる方法についてご紹介していきます。ここで変数に保持させた検索キーワードは後ほど実装するPOST通信時のパラメーターとして使います。
それでは、App.jsxを修正していきます。 まず初めに、useStateが使えるようにすでに実装されているReactのインポートを修正します。
[修正前]
import React from 'react'
[修正後]
import React, { useState } from 'react'
そのあとに、useStateの初期値を設定します。
[useStateの初期化]
const [display, setDisplay] = useState({ title: "", category: "", volume: 0, story: 0 });
次に、useStateの設定をFormコンポーネントに渡せるよう以下のように修正します。
[修正前]
<Form />
[修正後]
<Form display={display} setDisplay={setDisplay} />
そして最後に、Formコンポーネントから取得したデータをListコンポーネントで表示できるよう以下のように修正します。
[修正前]
<List />
[修正後]
<List searchResult={display} />
他にもFormやListコンポーネント内で処理を追加する必要があります。 ただ、これに関してはこの後の「POST通信の実装」でご紹介します。
POST通信の実装
この章では、検索フォームで入力したキーワードを使ってMySQLからデータ取得できるようにPOST通信の実装を行っていきます。 ReactとNode.jsのそれぞれで修正が必要なので、分けて修正方法をご紹介していきます。
1.React側の修正
React側では、POST通信を実現するためにaxiosというライブラリを活用していきます。 以下のコマンドをcreate-react-appプロジェクト内で実行してaxiosをインストールします。
npm install axios
それでは、axiosを使ったPOST通信の実装方法をご紹介します。ここでは、FormとListコンポーネントで修正が必要になります。
[Formコンポーネントの修正]
Formコンポーネント内では、サーバ側(Node.js)へのPOST通信ができるようにaxiosの実装を行います。なお、ここでは先に説明したuseStateに関連する実装も併せて行います。その結果が以下になります。
Form.jsx
import React, { useState } from 'react' import TextField from "@material-ui/core/TextField"; import Grid from '@material-ui/core/Grid'; import Button from '@material-ui/core/Button' import axios from 'axios'; export default function Form(props) { const [title, setTitle] = useState(''); const funSetTitle = (e) => { setTitle(() => e.target.value); } const funPost = () => { const params = new URLSearchParams(); params.append('title', title); axios.post('/api', params) .then(function (res) { props.setDisplay({ ...props.display, title: res.data.message.title, category: res.data.message.category, volume: res.data.message.volume, story: res.data.message.story }); }) .catch(function (error) { console.log("error", error); }); } return ( <> <Grid container rowSpacing={8}> <form> <Grid item> <TextField label="タイトル" value={title} onChange={funSetTitle} /> </Grid> <Grid item> <Button color="inherit" onClick={funPost}>検索</Button> </Grid> </form> </Grid> </> ) }
[Listコンポーネントの修正]
Listコンポーネント内では、POST通信を使って取得したデータが格納されているuseStateの変数から、props経由で取得して表示できるよう修正します。
こちらでも先に説明したuseStateに関連する修正も併せて行います。その結果が以下になります。
List.jsx
import React from 'react' import { makeStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; export default function List(props) { const useStyles = makeStyles({ table: { minWidth: 650, }, }); const classes = useStyles(); return ( <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>タイトル</TableCell> <TableCell align="right">ジャンル</TableCell> <TableCell align="right">読んだ巻数</TableCell> <TableCell align="right">読んだ話数</TableCell> </TableRow> </TableHead> <TableBody> <TableRow key={props.searchResult.title}> <TableCell component="th" scope="row"> {props.searchResult.title} </TableCell> <TableCell align="right">{props.searchResult.category}</TableCell> <TableCell align="right">{props.searchResult.volume}</TableCell> <TableCell align="right">{props.searchResult.story}</TableCell> </TableRow> </TableBody> </Table> </TableContainer> ); }
2.Node.js側の修正
Node.js側では、POST通信ができるように「backend/index.js」内を修正していきます。その結果が以下になります。
backend/index.js
const express = require('express') const mysql = require('mysql2') const app = express() const port = process.env.PORT || 3001 app.use(express.urlencoded({extended: true})); const connection = mysql.createConnection({ host: 'localhost', user: 'testuser001', password: 'パスワード', database: 'comic' }); app.post("/api", (req, res) => { const sql = "SELECT * FROM list where title = ?"; connection.query( sql, req.body.title, function(err, results, fields) { if(err) { console.log("接続終了(異常)"); throw err; } res.json({message: results[0]}); } ); }); app.listen(port, () => { console.log(`listening on *:${port}`); })
以上で、POST通信の実装と一部useStateの実装が完了となります。
コンポーネントの表示/非表示切り替え
この章では、Listコンポーネントのレコードを初期表示した際に非表示になるよう修正する方法をご紹介していきます。
これまでの流れに沿って実装すると、最初にブラウザを立ち上げた際におそらくuseStateで設定された初期値も表示されてしまっているはずです。
そこで、初期表示時は非表示になるよう修正していきます。つまり、
初期表示時 :ヘッダーのみ表示
検索ボタン押下後:ヘッダー+レコードを表示
というのを実現するための修正を行っていきます。
主に「App/Form/List」の3つのコンポーネントで修正を行います。
1.Appコンポーネントの修正
まず、フラグによって表示/非表示を切り替えられるように下記のuseStateを定義します。
const [flag, setFlag] = useState(false);
次に、データを取得できた時点でフラグがfalse→trueに変わるようFormコンポーネントへ渡すプロパティを修正します。
[修正前]
<Form display={display} setDisplay={setDisplay} />
[修正後]
<Form display={display} setDisplay={setDisplay} setFlag={setFlag} />
最後に、フラグの値によってテーブルの表示が切り替わるようにListコンポーネントへ渡すプロパティを修正します。
[修正前]
<List json={display} />
[修正後]
<List json={display} displayFlag={flag} />
2.Formコンポーネントの修正
データを取得できた場合にフラグをfalse→trueに変更したいので、axios.post().then()メソッドで実装しているsetDisplayの後に下記処理を追加します。
props.setFlag(true);
3.Listコンポーネントの修正
List.jsxでは、TableContainerに含まれていたTableBodyを切り出します。これにより、フラグの値によって表示/非表示を切り替えられるようにします。
import React from 'react' import { makeStyles } from '@material-ui/core/styles'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableContainer from '@material-ui/core/TableContainer'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; import Paper from '@material-ui/core/Paper'; export default function List(props) { const useStyles = makeStyles({ table: { minWidth: 650, }, }); const classes = useStyles(); const tableBody = props.displayFlag ? ( <TableBody> <TableRow key={props.searchResult.title}> <TableCell component="th" scope="row"> {props.searchResult.title} </TableCell> <TableCell align="right">{props.searchResult.category}</TableCell> <TableCell align="right">{props.searchResult.volume}</TableCell> <TableCell align="right">{props.searchResult.story}</TableCell> </TableRow> </TableBody> ) : null; return ( <TableContainer component={Paper}> <Table className={classes.table} aria-label="simple table"> <TableHead> <TableRow> <TableCell>タイトル</TableCell> <TableCell align="right">ジャンル</TableCell> <TableCell align="right">巻数</TableCell> <TableCell align="right">話数</TableCell> </TableRow> </TableHead> {tableBody} </Table> </TableContainer> ); }
ここまで出来たら、create-react-appプロジェクト内で「npm start」を実行してサービス起動後のブラウザ表示を確認してみます。すると、初期表示は以下のように変わっているはずです。
この状態でタイトル入力欄に予めMySQLに登録したデータが持つタイトルを入力して検索すると、以下のように検索結果が表示されるようになります。
以上で、今回の記事で作ろうとしていたアプリの作成は完了となります。
まとめ
いかがだったでしょうか。 長い記事になってしまいましたが、今回はReact+Node.js+MySQLの組み合わせで簡単なアプリの実装をご紹介させていただきました。
機能や実装面で不十分なところが多いので、今後の継続的な学習手段として機能拡張も進めていけたらと思っています。
今回の記事でご紹介した内容や他サイトの記事が、読んでくださった皆様にとって少しでも参考になれば幸いです。 最後まで読んでいただきありがとうございました!