2022年7月11日 (月)

Expressと静的ファイル

 Expressでは、常にgetやpostでアクセスするだけでなく、単にURLで指定したファイルをクライアントに送ることもできます。

 Express generatorで作られたディレクトリ構造の中に、publicディレクトリがあり、その中にimages, javascripts, stylesheetsディレクトリがあります。ブラウザから、

http://localhost:3000/stylesheets/style.css

 にアクセスすることで、自動的に作られたstyle.cssファイルが表示されます。

 このpublicディレクトは、自動的にクライアントに公開されるわけではなく、App.jsの中の

var path = require('path');  // 3行目
app.use(express.static(path.join(__dirname, 'public')));  // 20行目

によって公開指定されます。Express generatorを使わない場合は、このapp.use()関数を調整する必要があります。

また、app.use()関数を複数設定することで、複数のディレクトリに静的ファイルを置いたり、

app.use('/static', express.static(path.join(__dirname, 'public')));

と設定することで、

http://localhost:3000/static/stylesheets/style.css

のように、パスの先頭にstaticをつけるよう指示することもできます。

» 続きを読む

Expressのルーティング

 Expressを使用したルーティングです。

 用語として、URLのパスを意味するルート(route)と、パスの大元(/)を意味するルート(root)がありますので、気をつけます。

基本的なルーティング

app.METHOD(PATH, HANDLER)
app
expressのインスタンス
METHOD
HTTPメソッド。get, post, put, post, deleteなどが代表
PATH
サーバー上のパス
HANDLER
ルートが一致したときに実行される関数

ルートの定義

ルートパスへのget処理

app.get('/', (req, res) => {
    res.send('root')
})

/userパスのpost処理

app.post('/user', (req, res) => {
    res.send('user')
})

/readme.txtのdelete処理。直接ファイルにアクセスができます。

app.delete('/readme.txt', (req, res) => {
    res.send('readme.txt')
})

そのほか、パスには正規表現が使えます。

ルートパラメーター

 ルートには、パラメーターを使うことができます。

app.delete('/users/:userId/books/:bookId', (req, res) => {
    res.send(req.params["userId"])
})

リクエストURL

http://localhost:3000/users/12/books/34

結果

req.params: { "userId": "12", "bookId": "34" }

ルートハンドラー

 ハンドラーは、一つのルートに対して複数の関数を当てることができます。

app.get('/', (req, res, next) => {
    console.log('root')
    next()
}, (req, res) => {
    res.send('root')
})

 複数の関数を、配列にまとめてルートに当てることもできます。

const func1 = (req, res, next) => { 
    console.log('func1')
    next()
}
const func2 = (req, res) => {
    res.send('func2')
}
app.get('/', [func1, func2])

レスポンドメソッド

 レスポンスオブジェクトresのメソッドには、レスポンスをクライアントに送信することで、リクエストーレスポンスサイクルを終了する役割があります。

 レスポンスオブジェクトの主なメソッド

res.download()
ファイルのダウンロードプロンプトを表示
res.end()
レスポンスプロセスを終了
res.json()
JSONレスポンスを送信
res.redirect()
リクエストをリダイレクト
res.render()
ビューテンプレートをレンダリング
res.send
レスポンスを送信

app.route()

 一つのルートに対して、複数のMETHODを指定することができます。

app.route('/')
    .get((req, res) => {
        res.send('Get')
    })
    .post((req, res) => {
        res.send('Post')
})

express.Router

 ルーティングをほかの.jsファイルに任せたり、間に処理を挟んだりできます。

App.js

var express = require('express');
var usersRouter = require('users');
var app = express();
app.use('/users', usersRouter);
module.exports = app;

users.js

var express = require('express');
var router = express.Router();
router.use((req, res, next) => {
    console.log('Time: ', Date.now())
    next()
})
router.get('/about' (req, res) => {
    res.send('user/about')
})
module.export = router

 App.jsから始まりますが、パスがusersで始まるとusers.jsに処理を移します。さらに、現在時刻をコンソールに出した後、users/aboutが見つかると処理を返します。

 今回はここまで。

» 続きを読む

Expressジェネレーター

 Expressの公式サイトを見ていたら、Expressのアプリケーション生成プログラムというものがありました。どうやら、Expressのひな形を作ることができるプログラムのようです。

 早速試してみました。

% npm install express-generator -g
% express --view=pug express-rest
% cd express-rest
% npm install
% DEBUG=express-rest:* npm start

 これでexpressサーバーを起動したら、localhost:3000にアクセスしてみます。

 ブラウザに「Express Welcome to Express」と表示されたら動作確認完了です。また、localhost:3000/users/にアクセスすると「respond with a resource」と表示され、それ以外のURLにアクセスすると「Not Found」エラーが出ます。

 express-restディレクトリの中には、下記のディレクトリ/ファイルができあがりました。

.
├── app.js // localhost:3000にアクセスすると、このファイルが最初に読み込まれる
├── bin
│   └── www.js
├── package.json
├── public
│   ├── images // 空フォルダ
│   ├── javascripts // 空フォルダ
│   └── stylesheets
│       └── style.css
├── routes
│   ├── index.js // app.jsの中で、'/'へのアクセスだと判断されると、このファイルが読み込まれる
│   └── users.js // app.jsの中で、'/users'へのアクセスだと判断されると、このファイルが読み込まれる
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

 新しく、PUGというファイル形式が出てきましたが、Reactで出てきたJSXと同様HTMLを表現するためのファイル形式のようです。

 さて、一番肝心なApp.jsを見ておきます。

./App.js

// (1)
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

// (2)
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

// (3)
var app = express();

// (4) view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');

// (5)
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// (6)
app.use('/', indexRouter);
app.use('/users', usersRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

 (1)でexpressの設定をしているようです。(2)でルーティングする子要素を読み込んでいます。

 (3)でexpressを起動し、(4)で表示環境の設定しています。

 (5)でexpressが使用する機能を設定しています。

 (6)がルーティング本体で、ルート(route)に応じてルーターを選択しています。ルートが存在しない場合はエラーを表示します。

 細かいことはわかりませんが、今回はここまで。

» 続きを読む

2022年7月 8日 (金)

RESTとExpress

 RESTではURLとHTTPリクエストメソッドが重要ということでしたが、Expressの公式サイトを見ていると、Expressを使うことで簡単にRESTを再現できそうでした。

 以前、react-node-mysqlの記事で使ったバックエンド用のファイルは、

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: '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}`);
})

 ですが、このうち重要なのが

app.post("/api", (req, res) => {
      res.json({message: results[0]});
})

 です。

 appの後についているpostが、HTTPリクエストメソッドを表していて、RESTでデータを取得するならget、データを追加するならpost、...と書き換えていくようです。

 そして、postの第一引数がRESTのURLになります。ここでどの情報にアクセスするかが決まります。第2引数は、クライアントからのリクエストreqと、レスポンスを返すresを引数に持つ関数になります。Expressでは、この関数をミドルウェア関数と呼びます。

 res.jsonで、クライアントにJSONデータを返します。json以外にも各種メソッドが用意されています。

 これで比較的簡単にRESTを再現できそうです。

» 続きを読む

RESTを知る

 過去の記事で、React+node.js+MySQLに挑戦しましたが、その中のExpressサーバーを構築する際、「/api」というURLを使っていました。  どうやら、サーバーにアクセスする一般的な方法があり、その一つがWeb APIであるということ、さらにその中に REST API というものがある、ということを知りました。

 REST APIの原則は、

状態管理を行わない
サーバーとの接続は1回ごとに切れます。そのため、サーバーの負荷が軽減されるそうです。しかし、ログイン状態をどうするのか(毎回ログイン情報を送るのか)などが気になります。
対象はURLで指定する
ユーザーデータベースにアクセスするとか、商品データベースにアクセスするとかをURLで指定します。原則名詞のURLにするそうです。
動作はHTTPのメソッドで指定する
HTTLにはGET, POST, PUT, DELETEなどのリクエストメソッドがあるので、同じURLに対して異なるメソッドでアクセスすることでデータの取得、作成などを行うようです。
情報はリンクできる
得られた情報からさらに情報を得ることができるようです。

 どこかの団体が厳密に定義している訳ではないので、多少の揺らぎはあるようです。

 次は、どのように実装するかですね。

» 続きを読む

Reactのチュートリアルに挑戦 (5)

React公式チュートリアルのまとめに書かれている課題の続きです。

着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。

 トグルボタンということですが、普通のボタンで対応してみます。

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                col: null,
                row: null,
            }],
            stepNumber: 0,
            xIsNext: true,
            isAscend: true, // (1)
        };
    }
(中略)
    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);

        const histories = this.state.isAscend ? history.slice() : history.slice().reverse(); // (2)

        const moves = histories.map((step, index) => { // (3)
            const move = this.state.isAscend ? index : histories.length - index - 1 // (4)
            const desc = move ? 'Go to move #' + move : 'Go to game start';
            const pos = move ? '(' + step.col + ',' + step.row + ')': '';
            const name = move == this.state.stepNumber ? 'current' : null
            return (
                <li key={move} className={name}>
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                    <span>{pos}</span>
                </li>
            );
        });

        let status;
        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
        return (
            <div className="game">
                <div className="game-board">
                    <Board squares={current.squares} onClick={(i) => this.handleClick(i)} />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <button onClick={ () => this.setState({ isAscend: !this.state.isAscend })}>{this.state.isAscend ? 'Ascend' : 'deascend'}</button> // (5)
                    <ol>{moves}</ol>
                </div>
            </div>
        );
    }

(1) コンストラクターの中で、昇順/降順を表す変数isAscendを定義します。

(2) レンダーの着手履歴を作成する直前で、履歴を修正します。元々の履歴は触りたくないので、historiesという変数を用意し、昇順であればhistoryをコピー、降順であればhistoryをコピーした上で逆順にします。昇順の場合はわざわざコピーをとる必要はないかもしれません。

(3) 仮引数moveをindexに修正しました。map関数内の無名関数の第2引数は、単純に配列の順番になります。手順表示の場合、昇順の場合はそれでよいのですが、降順の場合は逆にしたいので、ここは別の変数で受け取ります。

(4) 手順moveを、昇順の場合はindexのとおり、降順の場合は履歴数からindexを引くことで算出します。

(5) 昇順/降順を切り替えるトグルボタンです。押す度に表示が切り替わり、変数の値も逆転させます。

 これで、履歴の順番を切り替えることができるようになりました。ただ、ボタンの前に表示されている数字はCSSで書かれているのでそのままです。CSSを使わずに直接記述すれば良さそうです。

 躓きポイントは、履歴を逆順にするところでしょうか。最初、histories = history.reverse()と書いたのですが、動作がおかしくなりました。reverse()を使うと、historyそのものの順番が変わってしまうのですね。

 ひとまずここまで。

» 続きを読む

2022年7月 7日 (木)

Reactのチュートリアルに挑戦 (4)

 React公式チュートリアルのまとめに、次の課題が掲載されていました。

  1. 履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。
  2. 着手履歴のリスト中で現在選択されているアイテムを太字にする。
  3. Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。
  4. 着手履歴のリストを昇順・降順いずれでも並べかえられるよう、トグルボタンを追加する。
  5. どちらかが勝利した際に、勝利につながった 3 つのマス目をハイライトする。
  6. どちらも勝利しなかった場合、結果が引き分けになったというメッセージを表示する。

 ひとまず挑戦してみます。

履歴内のそれぞれの着手の位置を (col, row) というフォーマットで表示する。

 履歴の中にcol, rowを保存する必要がので、history変数の中に、colとrowを追加します。

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
                col: null, // 追加
                row: null, // 追加
            }],
            stepNumber: 0,
            xIsNext: true,
        };
    }
 (以下略)

 一手打つごとにcol, rowを保存します。

    handleClick(i) {
 (中略)
        this.setState({
            history: history.concat([{
                squares: squares,
                col: i % 3, // 追加
                row: Math.floor(i / 3), // 追加
            }]),
            stepNumber: history.length,
            xIsNext: !this.state.xIsNext,
        });
    }

 col, rowを表示します。

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber];
        const winner = calculateWinner(current.squares);

        const moves = history.map((step, move) => {
            const desc = move ? 'Go to move #' + move : 'Go to game start';
            const pos = move ? '(' + step.col + ',' + step.row + ')': ''; // 追加
            return (
                <li key={move}>
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                    <span>{pos}</span> // 追加
                </li>
            );
        });

 これで、一手打つごとにボタンの横に座標が表示されるようになりました。

着手履歴のリスト中で現在選択されているアイテムを太字にする。

 履歴の番号と手順が一致するものを太字にすれば良さそうです。

        const moves = history.map((step, move) => {
            const desc = move ? 'Go to move #' + move : 'Go to game start';
            const pos = move ? '(' + step.col + ',' + step.row + ')': '';
            const name = move == this.state.stepNumber ? 'current' : null // 追加
            return (
                <li key={move} className={name}> // className={name}を追加
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                    <span>{pos}</span>
                </li>
            );
        });

index.cssの末尾に、CSSを追加します。

.current {
    font-weight: bold;
}

 これで、履歴のどこを表示しているかがわかりやすくなります。

Board でマス目を並べる部分を、ハードコーディングではなく 2 つのループを使用するように書き換える。

 for文で書けばよいのかと思ったのですが、JSXの中には文は書けないとのことで、map関数を使って書き直しました。

    render() {
        return (
            <div>
            {[0, 1, 2].map((row) => {
                return (
                    <div className="board-row">
                    {[0, 1, 2].map((col) =>  this.renderSquare(row * 3 + col))}
                    </div>
                )              
            })}
            </div>
        );
    }

 おそらくこれでハードコーディングと同じになるはずです。

 4.以降はまた後日。

» 続きを読む

Reactのチュートリアルに挑戦 (3)

 三目並べは完成しましたが、Reactチュートリアルはまだ終わりません。

 三目並べに履歴機能をつけます。1手打つごとに盤の情報を保存しておき、どの段階にでも戻ることができるという機能です。

Stateのリフトアップ

 ゲームの状態はBoardが保持していましたが、Boardの親であるGameがゲーム状態の履歴を保持し、Boardは最新の状態を親から受け取って表示する、という形に変えます。

 まずは、Gameを書き換えます。

class Game extends React.Component {
// ここから追加
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            xIsNext: true,
        };
    }
// ここまで追加
    render() {
 (以下略)

 Gameにコンストラクターを追加し、その中で盤の状態squaresを保存するhistory配列を作成します。xIsNextもGameで管理します。

 Boardは、自身で状態を管理しなくなったのでコンストラクターは不要になります。また、盤の状態は親から受け取り、マスをクリックされたときの処理は親に任せることにします(handleClick()関数も不要)。

class Board extends React.Component {

// constructor(){}は削除

    renderSquare(i) {
        return <Square value={ this.props.squares[i] } onClick={() => this.props.onClick(i)}/>; // this.props.onClickを呼び出す
    }

// handleClick(){}は削除

    render() {
        const winner = calculateWinner(this.props.squares); // stateがなくなったのでpropsを使用
        let status;
        if (winner) {
          status = 'Winner: ' + winner;
        } else {
          status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }

        return (
            <div>
            <div className="status">{props}</div>
            <div className="board-row">
                {this.renderSquare(0)}
                {this.renderSquare(1)}
                {this.renderSquare(2)}
            </div>
            <div className="board-row">
                {this.renderSquare(3)}
                {this.renderSquare(4)}
                {this.renderSquare(5)}
            </div>
            <div className="board-row">
                {this.renderSquare(6)}
                {this.renderSquare(7)}
                {this.renderSquare(8)}
            </div>
            </div>
        );
    }   
}

 Gameのrender()関数を書き換えます。履歴から最新の状態を取り出し、勝敗の判定を行います。その後、Boardに表示を任せます。さらに、画面横に現在の状態を表示します。

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            xIsNext: true,
        };
    }
    render() {
        const history = this.state.history;
        const current = history[history.length - 1];
        const winner = calculateWinner(current.squares);
        let status;
        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
        return (
            <div className="game">
                <div className="game-board">
                    <Board squares={current.squares} onClick={(i) => this.handleClick(i)} />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

 勝敗の判定などもGameに任せたので、Boardから該当部分を削ります。

class Board extends React.Component {
    renderSquare(i) {
        return <Square value={ this.props.squares[i] } onClick={() => this.props.handleClick(i)}/>;
    }
    render() {
        return (
            <div>
            <div className="status">{this.props.winner} </div> // ここも書き換えます

 (以下略)

 handleClick()関数を、Gameに移動させます。

class Game extends React.Component {
 (中略)
    handleClick(i) {
        const history = this.state.history;   //履歴を保存(stateは操作できないため)
        const current = history[history.length - 1]; // 最新の履歴を保存
        const squares = current.squares.slice(); // 最新の履歴の盤の状態をコピー
        if (calculateWinner(squares) || squares[i]) { // 勝敗がついているか、そのマスにOXがある場合は何もせず戻る
            return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O'; // マスにOXを入力する
        this.setState({
            history: history.concat([{ // 保存しておいた履歴に新しい盤の状態を追加し、親要素のhistoryを置き換える
                squares: squares,
            }]),
            xIsNext: !this.state.xIsNext, // 親要素のxIsNextの状態を反転させる
        });
    }
 (以下略)

履歴の表示

 盤面の状態の履歴は保存できたので、履歴を表示します。

 Gameのrender関数内に履歴を表示する機能を実装します。

class Game extends React.Component {
  (中略)
    render() {
        const history = this.state.history;
        const current = history[history.length - 1];
        const winner = calculateWinner(current.squares);
        // ここから追加
        const moves = history.map((step, move) => {
            const desc = move ?
                'Go to move #' + move :
                'Go to game start';
            return (
                <li>
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                </li>
            );
        });
        // ここまで追加
        let status;
        if (winner) {
            status = 'Winner: ' + winner;
        } else {
            status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
        }
        return (
            <div className="game">
                <div className="game-board">
                    <Board squares={current.squares} onClick={(i) => this.handleClick(i)} />
                </div>
                <div className="game-info">
                    <div>{status}</div>
                    <ol>{moves}</ol> // ここ変更
                </div>
            </div>
        );
    }
}

 ブラウザで確認すると、盤の右にボタンが表示されます。OXを打つごとにボタンが増えていきます。

 ここで、コンソールを確認するとワーニングが出ています。リストの中の子要素にはユニークKeyが必要だというワーニングです。これは、<li>要素などのリストの子要素にはユニークキーを付けろという警告です。ユニークキーをつけておけば、リストの子要素に変化があったとき、実際に変化のあった子要素とそうでない要素の区別をつけることができるので、再描画の最適化がしやすいという利点があります。

        const moves = history.map((step, move) => {
            const desc = move ?
                'Go to move #' + move :
                'Go to game start';
            return (
                <li key={move}> // ここにkey={move}を追加
                    <button onClick={() => this.jumpTo(move)}>{desc}</button>
                </li>
            );
        });

 右のボタンで何手目かに戻れるようにしますが、そのためには現在が何手目かを記録ひつようがあるので、その変数を作ります。

class Game extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            history: [{
                squares: Array(9).fill(null),
            }],
            stepNumber: 0, // この変数を追加
            xIsNext: true,
        };
    }
 (以下略)

さらに、handleClick(i)とrender()の間に、jumpTo()関数を追加します。この関数で、戻る手数と、そのときの手番を決定します。

    jumpTo(step) {
        this.setState({
            stepNumber: step,
            xIsNext: (step % 2) === 0,
        });
    }

 手番が戻ったときに盤面を戻す処理を加えます。

    render() {
        const history = this.state.history;
        const current = history[this.state.stepNumber]; // 履歴から手番の盤面を取り出す
        const winner = calculateWinner(current.squares);
 (以下略)

 最後に、新しい手を打ったときにそれ以降の履歴を削除し、新しい履歴を加えるようにします。逆に言うと、新しい手を打つまでは履歴を残しておくので、どの手数でも見ることができます。

    handleClick(i) {
        const history = this.state.history.slice(0, this.state.stepNumber + 1);  // 指定した手番までの履歴を保存
        const current = history[history.length - 1];
        const squares = current.squares.slice();
        if (calculateWinner(squares) || squares[i]) {
            return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            history: history.concat([{
                squares: squares,
            }]),
            stepNumber: history.length, // 履歴の長さを次の手番に設定
            xIsNext: !this.state.xIsNext,
        });
    }

 これで、三目並べの履歴機能が実装できました。

 JavaScriptの配列が連想配列のためか冗長に感じられたり、必ずsetStateを使う必要がある、そのときにはstate直下の変数を書き換える必要があるなど、覚えることが多そうです。

» 続きを読む

Reactのチュートリアルに挑戦 (2)

stateのリフトアップ

 先ほどの記事では、Squareにマスの状態(空白、X)を持たせましたが、ゲームとして成り立つためには、状態を一元管理した方がよいです。そのためには、子要素Squareのstateを親要素Boardに持たせるように書き換えます。このように、子要素のstateを親要素に移すことをリフトアップと呼ぶようです。

class Board extends React.Component {
    // ここから追加
    constructor(props) {
        super(props);
        this.state = {
            squares: Array(9).fill(null),
        };
    }
 // ここまで追加
    renderSquare(i) {
        return <Square value={ this.state.squares[i] } />;
    }
 (以下略)

 Boardにコンストラクターを追加し、その中で要素数9の配列(中身はnull)を定義しています。そして、renderSquare(i)の中でSquareに渡す値を、this.state.squares[i]に変更しています。

 さて、次の問題は、Squareでボタンが押されたときに、Boardから渡された値を書き換える方法です。Reactでは、親要素から渡された変数propsの中身を書き換えることができません。そこで、親要素の変数を書き換える関数を子要素に渡します。

class Square extends React.Component {
     // constructor(){}は削除
    render() {
        return (
            <button className="square" onClick={() => this.props.onClick()} > // onClickの処理先を this.props.onClickに変更
                {this.props.value}  // 表示する内容は、直接propsの内容を利用
            </button>
        );
    }
}

 まず、Squareの中で変数を保持しなくなったので、コンストラクターは削除します。そして、onClick()は this.props.onClick()に変更し、button要素の中身も this.props.value に戻します。

 これで、ボタンを押したら親要素Boarderの関数を呼び出すようにできました。Squareには、this.props.valueを表示する機能と、ボタンを押されたらthis.props.onClick()を呼び出す機能しか持っていません。

class Board extends React.Component {
 (中略)
    // ここから追加
    handleClick(i) {
        const squares = this.state.squares.slice(); // squarsを保存
        squares[i] = 'X'; // squaresの中を変更
        this.setState({squares: squares}); // this.state.squaresをsquaresに置き換え
    }
 // ここまで追加
    renderSquare(i) {
        return <Square value={ this.state.squares[i] } onClick={() => this.handleClick(i)}/>; // onClick〜を追加
    }
 (以下略)

 Boaderのコンストラクターの後に、handleClick(i){}関数を追加します。そして、SquareにはvalueのほかにonClickも渡すようにします。

 ここで、handleClick(i){}関数の中で、this.state.squaresの値をそのまま書き換えることはなく、一度squaresにコピーしてから書き換え、改めてthis.setState({squares: squares})に書き戻しています。これは、変数が書き換えられたことを他のコンポーネントに伝えることが、state直下の値が書き換えられたときのみしか起こらない、と理解しました。

関数コンポーネント

 現在Reactはクラスではなく関数コンポーネントで書くことが主流になってきています。とりあえず、状態を持たないSquareを関数コンポーネントに書き換えてみます。

function Square(props) {
    return (
        <button className="square" onClick={props.onClick}>
            {props.value}
        </button>
    );
}

 ほんのちょっと、プログラムが短くなりました。

対戦相手を作る

 今のところ、マスをクリックするとXが表示されましたが、OとXを交互に表示されるようにします。まずは、手番が交互になるように変更します。

class Board extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            squares: Array(9).fill(null),
            xIsNext: true, // 手番を表す変数を追加
        };
    }
    handleClick(i) {
        const squares = this.state.squares.slice();
        squares[i] = this.state.xIsNext ? 'X' : 'O';
        this.setState({
            squares: squares,
            xIsNext: !this.state.xIsNext, // 一手ごとに手番を交代
        });
    }
 (以下略)

 コンストラクターの中に、xIsNextというBool型の変数を追加しました。そして、handleClick(i)の中で、squares[i]に入れる値をxIsNextの値に応じてXかOかを選択します。さらに、毎回xIsNextのBool値を逆転させます。

 ブラウザで確認すると、マスをクリックするたびにOとXが交互に表示されるようになりました。さらに、render()関数の中も書き換えて、次の手番がOかXかを表示させます。

 (前略)
    render() {
        const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // 手番表す変数にOXを代入
 (以下略)

勝利判定

 次に、3マス並んだら勝利という判定を実装します。index.jsファイルの末尾にcalculateWinner()関数を追加します。

(前略)
// 行末に追加
function calculateWinner(squares) {
    const lines = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8],
        [0, 4, 8],
        [2, 4, 6],
    ];
    for (let i = 0; i < lines.length; i++) {
        const [a, b, c] = lines[i];
        if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
            return squares[a];
        }
    }
    return null;
}

 あらかじめ「この3マスが同じ手番だったら3マス並んだことになる」という3個の値の集合を用意し、squaresの中身のうちその3個が同じならその手番(O, X)を返すという関数です。

 この関数を、盤を描画するときに呼び出し、その結果に応じて表示を変えます。

class Board extends React.Component {
 (中略)
    render() {
        const winner = calculateWinner(this.state.squares); // どちらが勝者か判定
        let status;
        if (winner) { 
          status = 'Winner: ' + winner; // 勝者がいれば、勝者を代入
        } else { 
          status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O'); // 勝者がいなければ、次の手番を代入
        }

        return (
 (以下略)

 ブラウザで確認すると、同じ手番が3つ並んだところで「Winner: ?」と表示されます。しかし、その後も試合は続きますし、すでにOXが置かれているマスをクリックするとそのマスが置き換わってしまいます。そこを修正します。

    handleClick(i) {
        const squares = this.state.squares.slice();
        if (calculateWinner(squares) || squares[i]) { // 勝者がいるか、すでに置かれているならreturn
            return;
        }
        squares[i] = this.state.xIsNext ? 'X' : 'O';
 (以下略)

 これで、三目並べ完成です。

» 続きを読む

Reactのチュートリアルに挑戦 (1)

 React公式サイトのチュートリアル・Reactの導入に挑戦します。

 このチュートリアルでは、三目並べを作成します。

Reactの準備

まずは、ローカル環境を設定します。

npx create-react-app my-app
cd my-app
cd src
rm -f *
cd ..

 create-react-appで基本的なReact環境を整えたあと、不要なファイルを消してしまいます。そして、以下のファイルを作成します。

my-app/src/index.css

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
}

.square というCSSが、三目並べの盤に関係しそうですね。

my-app/src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

class Square extends React.Component {
    render() {
        return (
            <button className="square">
            {/* TODO */}
            </button>
        );
    }
}

class Board extends React.Component {
    renderSquare(i) {
        return <Square />;
    }

    render() {
        const status = 'Next player: X';

        return (
            <div>
            <div className="status">{status}</div>
            <div className="board-row">
                {this.renderSquare(0)}
                {this.renderSquare(1)}
                {this.renderSquare(2)}
            </div>
            <div className="board-row">
                {this.renderSquare(3)}
                {this.renderSquare(4)}
                {this.renderSquare(5)}
            </div>
            <div className="board-row">
                {this.renderSquare(6)}
                {this.renderSquare(7)}
                {this.renderSquare(8)}
            </div>
            </div>
        );
    }
}

class Game extends React.Component {
    render() {
        return (
            <div className="game">
                <div className="game-board">
                    <Board />
                </div>
                <div className="game-info">
                    <div>{/* status */}</div>
                    <ol>{/* TODO */}</ol>
                </div>
            </div>
        );
    }
}

// ========================================

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Game />);

 2つのファイルを作成したら、Reactを起動します。

% npm start

 ブラウザ上には、「Next player: X」の下に3×3の盤が表示されます。

index.jsの構成

 index.jsにある3つのクラス(Square, Board, Game)は、いずれも React.Component を継承しています。これらが React コンポーネント型になります。Reactではこのようなコンポーネントを組み合わせてUIを作っていきます。

 コンポーネントの中に書かれている HTML のようなものは、JSX とよばれる React で定義されている構文です。PHP になれていると、変数などが展開された後、そのままHTMLになるのかと思ってしまいますが、Reactの場合はJavaScriptの関数に変換されるそうです。

 index.jsの3つのクラス、Squareはマス目でbuttonを描く、Boardは盤で「Next Player: X」とSquareを3行3列描く、GameはBoardを描くという役割を持っています。

データをPropで渡す

 Squareはマス目として最終的に○×を表示しますが、その情報を上位であるBoardから受ける必要があります。そこで、BoardにはSquareを呼び出すときに引数を渡す機能を、Squareには引数を受け取る機能をつけます。

class Board extends React.Component {
    renderSquare(i) {
        return <Square value={i} />;
    }
 (以下略)

 なぜSquareを呼び出すために専用の関数 renderSquare(i){} を定義しているのか謎ですが、その中で Square を呼び出しています。という構文によって、Squareに対し {value: i}という連想配列を渡しているようです。  

class Square extends React.Component {
    render() {
        return (
            <button className="square">
                {this.props.value}
            </button>
        );
    }
}

 受け取る側は、this.props で渡された引数を受け取ります。関数のように仮引数で受け取るわけではなさそうです。また、this.propsは連想配列なので、this.props.value でも this.props["value"] でも値を取得することができます。

 ここでブラウザを見てみると、3×3のマスの中に0...9の数字が表示されています。

ボタンで状態を変える

 Squareは内部でボタンを表示していますが、ボタンを押したときに変化をつけたいです。

class Square extends React.Component {
    render() {
        return (
            <button className="square" onClick={() => console.log('click') }>
                {this.props.value}
            </button>
        );
    }
}

 buttonにonClick属性をつけました。ブラウザ上でマスをクリックすると、コンソールに「click」と表示されます。

 コンソールに表示するだけでは意味がないので、マスに表示する必要があります。そのためには、自分自身が何を表示しているのかを記憶する必要があります。そのために、状態を記憶する変数 state を利用します。

class Square extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            value: null,
        };
    }
    render() {
        return (
            <button className="square" onClick={() => this.setState({ value: 'X'}) }>
            {this.state.value}
            </button>
        );
    }
}

 Squareクラスにコンストラクターを記載します。コンストラクターは、引数にpropsをとり、super(props)で親クラスを呼ぶことが基本となっているそうです。そして、this.state に連想配列 { value: null }を代入します。これにより、マス目に表示する文字は最初nullになります。

 buttonの中身は、this.state.value ですが、Squareが表示された直後は空白、ボタンが押された後は'X'が表示されます。

 そして、buttonのonClick属性は、this.setState({value: 'X'}) とします。これは、クリックされたらvalueの値を'X'にするというものです。Reactでは、直接 this.state.value = 'X' とするのではなく、setState()を使う必要があります。this.state.value = 'X' とすると、ボタンをクリックしてもマスの中身が'X'になりません。

 以上で、Squareに'X'を表示するまでを実装しました。

» 続きを読む

«JavaScriptの関数