MSBOOKS

プログラミングとか深層学習とか日常の出来事とか

【勉強メモ】Reactのチュートリアルを試してみた その3

はじめに

react公式のチュートリアルの続き、タイムトラベル機能の追加からやります。以前の着手まで時間を巻き戻すことができるようにします。
ja.reactjs.org

着手の履歴の保存

文章がすごくカタカナだらけで読みにくかったので要約してみます。
squaresの配列のデータを直接変更していたら、この実装は難しいです。しかし私たちは着手があるたびにsquaresのコピーを作る不変性(イミュータビリティ)を利用しました。なので、過去のバージョンをすべて保存しておいて過去の手番にさかのぼることができるようになります。なので過去のsquaresの配列をhistoryという名前の別の配列に保存します。
直接書き換えをすると、オブジェクトが変わらないので、変更の検出がしづらくなるって話ですね。

State のリフトアップ、再び

トップレベル(最も上の親の部分)のGameコンポーネント内で過去の着手の履歴を表示したいので、history stateをGameコンポーネントに置き、そのためにBoardにあるsquaresなどのstateをリフトアップ(親にもっていく)する必要があります。なのでGameコンポーネントのコンストラクタでそのstateを定義します。
・Gameコンポーネント

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      xIsNext: true,
    };
  }

  render() {
    return (
      <div className="game">
        <div className="game-board">
          <Board />
        </div>
        <div className="game-info">
          <div>{/* status */}</div>
          <ol>{/* TODO */}</ol>
        </div>
      </div>
    );
  }
}

BoardコンオーネントにあったsquaresとonClickはGameコンポーネントからpropsで受け取るように変更します。こうなるとsquareコンポーネントからonClickを受け取り、それをBoardコンポーネントが受けとり、それをさらに位置の情報iを加えてGameコンポーネントが受け取るっていうとても深い構造になりましたね。

・Boardコンポーネント

class Board extends React.Component {
  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

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

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    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>
    );
  }
}

これに伴ってGameコンポーネントのrenderを更新します。
・Gameコンポーネント

  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>
    );
  }

currentのhistory.length - 1で長さ5の配列であれば4となり、0から始まるその配列の最後の要素を取り出せる仕組みですね。その取り出した盤面で決着がついているか計算した後、それに応じた表示をしています。historyの中身を取り出すときにcurrent.squaresのようにsquaresを明示的に書かないと取れないのにも注目です。以下のような配列になっているからですね。

history = [
  // Before first move
  {
    squares: [
      null, null, null,
      null, null, null,
      null, null, null,
    ]
  },
  // After first move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, null,
    ]
  },
  // After second move
  {
    squares: [
      null, null, null,
      null, 'X', null,
      null, null, 'O',
    ]
  },
  // ...
]

HTMLの this.handleClick(i)でBoardのthis.props.onClick(i)のiが(i)にはいってきて、それがthis.handleClick(i)のiに入っていくんですね。
statusがGameコンポーネントで書くことになったので、Boardコンポーネントからは削除します。
・Boardコンポーネント

  render() {
    return (
      <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>
    );
  }

またhandleClickメソッドを Board コンポーネントから Game コンポーネントに移動します。Game コンポーネントの state はhistroyの中にsquaresがあるのでそれに合わせて修正します。
・Gameコンポーネント

  handleClick(i) {
    const history = this.state.history;
    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,
      }]),
      xIsNext: !this.state.xIsNext,
    });
  }

concat()は配列の結合で使い、push()と違って書き換えが起きないメリットがあるそうです。

過去の着手の表示

"React で複数の要素を描画するには、React 要素の配列を使うことができます。"とあり、おそらくReactの特徴でしょう。
配列には map() メソッドを用いることでデータを別のデータにマップするのによく利用されます。

const numbers = [1, 2, 3];
const doubled = numbers.map(x => x * 2); // [2, 4, 6]

このmapメソッドを使うことで、着手履歴の配列をマップ(新たに生成)して画面上のボタンを表現する React 要素を作ることができます。そしてこれによって、過去の手番に「ジャンプ」するためのボタンの一覧を表示できます。つまり、mapによってhistoryから新たなReact要素(過去にジャンプするためのボタン)を作り出すことができるということですね。
・Gameコンポーネント

  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>
    );
  }

jumpToはまだ実装していませんね。<ol>{moves}</ol>の部分にconst movesで定義したbuttonが返ってくるんですね、HTMLがこんなにバラバラに書かれているのは慣れていなかったので、最初は追うのが大変そう…。

keyを選ぶ

リストの項目に変更が生じたかどうかを確かめるためにkeyプロパティを追加します。keyの有無によって、Reactはコンポーネットを作成したり破棄したりするそうです。
なにも指定しなければkeyは、デフォルトで配列のインデックスを使用されますが、keyは動的なリストを構築する場合は割り当てることが推奨されています。またkeyはグローバルで一意にする必要はなく、コンポーネントの関係がある間で一意であれば大丈夫だそうです。

タイムトラベルの実装

三目並べゲームで履歴を保存するには、着手順によって一意なIDを設定可能です。今回の場合<li>の1つに対してkeyを加えることで、設定可能です。
・Gameコンポーネント

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

ちょっと気持ち悪いですが、mapはhistoryから一個ずつ要素を取ってきて新たな配列を作るものですが、第一引数のstepはhistory[0]などのオブジェクトが入っていて、第二引数にはインデックス番号0などが入っています。なので、moveは0,1,2というように着手順になるため、これをkeyにしています。つまりmovesに入っているものは、liのリストのHTMLがたくさん入っているイメージで、moveとmovesはまったく違うものが入っているイメージですね。
次にjumpToが未定義なので、実装しますがどこへジャンプするのかを定義するために、gameコンポーネントのstateにstepNumberを加えます。初期値は0です。

・Gameコンポーネント

class Game extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      history: [{
        squares: Array(9).fill(null),
      }],
      stepNumber: 0,
      xIsNext: true,
    };
  }

jumpToを定義し、先ほど定義したstepNumberが更新されるように設定します。xIsNextはstepNumberの値が偶数だった場合はtrueにします。書き方も面白いですね、ifをそのままxIsNextのところに入れ、返ってきた結果をそのまま入れています。

・Gameコンポーネント

  handleClick(i) {
    // this method has not changed
  }

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

  render() {
    // this method has not changed
  }

マス目をクリックしたときhandleClickが呼ばれますが、ここにstepNumberを入れる必要がありますね。次の登録するstepNumberは、history.length(historyの最後)に登録すればよいので、そのままstepNumberを入れています。また、タイムトラベルをした後はhistoryを部分的に切り出しているので、historyの0~stepNumber + 1に書き換えれば良いです。sliceの終了位置は指定した位置を含まないので、+1をしてその位置も含めるようにしているんですね。
・Gameコンポーネント

  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,
    });
  }

最後にrenderを最後の着手後の状態をレンダーするのではなく、stepNumberによって現在選択されている状態をレンダーするようにします。

・Gameコンポーネント

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

    // the rest has not changed

実行結果は↓
f:id:msteacher:20210812151602p:plain
確かに戻ることができるようになりました。
f:id:msteacher:20210812151654p:plain

おわりに

Reactのチュートリアルを通して、HTMLを分けて書くことや、props、stateの使い方がなんとなくわかった気がします。ただかなりいろいろなところにHTMLが分散されているので、慣れるまでは読むのにかなり時間を費やしそうな気がしますね。実務で色々経験していってまた何か気づきがあれば書いてみようかなって思ってます。それではまた。

【勉強メモ】Reactのチュートリアルを試してみた その2

はじめに

react公式のチュートリアルの続き、ゲームを完成させるからやります。三目並べゲームを盤面に交互に置けるようにすることと勝利判定をやっていきます。
ja.reactjs.org

Stateのリフトアップ

前回まではstateとしてそれぞれのSquareコンポーネントがprivate的な変数に状態を保持していたものを、親のBoardコンポーネントで保持できるよう変更します。バラバラのままでは今squareに何の値が入っているか何度も呼びにいかないといけなくなりますからね。これによって可読性やリファクタリングのしやすさが向上するそうです。
具体的にはBoard コンポーネントが、各Squareコンポーネントにprops を渡すことで、何を表示すべきかを伝えられます。以前やったpublicな変数に値を入れるってイメージのやつですね。

数の子要素からデータを集めたい、または 2 つの子コンポーネントに互いにやりとりさせたいと思った場合は、代わりに親コンポーネント内で共有の state を宣言する必要があります。親コンポーネントは props を使うことで子に情報を返すことができます。つまり、親コンポーネントで共有変数を用意しておいて、子へ渡すときは子のpropsに直接書き込めば良いってことですね。これを親コンポーネントにリフトアップするというそうです。子の変数を親へリフトするってイメージですかね。
・Boardコンポーネント

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]} />;
  }

renderSquareが呼ばれたら、Squareの中の情報が、Boardのstateに入っている情報に更新されるようになりました。

次に、マス目がクリックされた時の挙動を変更しましょう。現在、どのマス目に何が入っているのかを管理しているのは Board です。Square が Board の state を更新できるようにする必要があります。state はそれを定義しているコンポーネント内でプライベートなものですので、Square から Board の state を直接書き換えることはできません。代わりに、Board から Square に関数を渡すことにして、マス目がクリックされた時に Square にその関数を呼んでもらうようにしましょう。
・Boardコンポーネント

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

Squareに渡すpropsとしてvalueと、今回onClickを追加しています。onClickはマス目がクリックされた時に Square が呼び出すためのものだそうで、この後定義します。

・Squareコンポーネント

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

親のBoardで基本的には情報を管理するようにしたので、表示するものをthis.props.valueとすることで、受け取った値をそのまま表示しています。でもこのままだとSquareでクリックされたとわかっても親コンポーネントの値を変えられないので、さっきpropsに追加したonClickを使います。SquareのonClickをthis.props.onClick()とすることで、propsで渡されてきたonClick、つまり親のthis.handleClick(i)が呼ばれるんですね。

・Boardコンポーネント

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
    };
  }

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = 'X';
    this.setState({squares: squares});
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  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>
    );
  }
}

handleClickではsliceを使って(この重要性は後述)squaresをコピーしてきて、iで指定した箇所にXを代入し、新たにできたsquaresをsetStateで更新しています。Board の state が変更になると、squaresとして全体を更新しているので、個々の Square コンポーネントも自動的に再レンダーされていますね。
Square コンポーネントは、Board コンポーネントから値を受け取って、クリックされた時はそのことを Board コンポーネントに伝えるだけになりました。React 用語でいうと、Square コンポーネント制御されたコンポーネント (controlled component) になったということです。Board が Square コンポーネントを全面的に制御しています。

イミュータビリティは何故重要なのか

さっきのhandleClickでsquaresをsliceでコピーしている理由についてですね。この操作をイミュータビリティ(immutability; 不変性)というそうです。反対にデータの値を直接いじってデータを書き換えることを、ミューテート(mutate; 書き換え)するというそうです。メリットとしては以下の3つがあげられていました。

  1. 複雑な機能が簡単に実現できる:ゲームの以前のヒストリをそのまま保って後で再利用することができるようになる。
  2. 変更の検出:イミュータブルなオブジェクトでの変更の検出は簡単で、参照しているイミュータブルなオブジェクトが前と別のものであれば、変更があったと判断できる。
  3. React の再レンダータイミングの決定:イミュータブルなデータは変更があったかどうか簡単に分かるため、コンポーネントをいつ再レンダーすべきなのか決定しやすくなる。

つまり、変更の検出が容易であるため、stateの値を遡って利用したり、再レンダーするタイミングが容易になるっていうことでしょうか。

関数コンポーネント

React における関数コンポーネントとは、render メソッドだけを有して自分の state を持たないコンポーネントを、よりシンプルに書くための方法です。React.Component を継承するクラスを定義する代わりに、props を入力として受け取り表示すべき内容を返す関数を定義します。関数コンポーネントはクラスよりも書くのが楽であり、多くのコンポーネントはこれで書くことができます。確かに先ほどのSquareコンポーネントはやることが少ない割に書くことが多かったですからね。thisが省略できるのも良いね。
・Squareコンポーネント→Square関数コンポーネント

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

手番の処理

XとOのOがまだ出てきていないのを修正します。デフォルトでは、先手を仮に“X”にします。
・Boardコンポーネント

class Board extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      squares: Array(9).fill(null),
      xIsNext: true,
    };
  }

プレーヤが着手するたびに、どちらのプレーヤの手番なのかを決める xIsNext(真偽値)が反転され、ゲームの状態が保存されます。
・Boardコンポーネント

  handleClick(i) {
    const squares = this.state.squares.slice();
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

this.state.xIsNext ? 'X' : 'O'という三項演算子が急に出てきてびっくりしましたが、xIsNextがtrueだったらX、falseだったらOを入れるという感じですね。先ほど手番で決めたようにtrueはXの番だったので、それを入れているようです。最後に!で真偽値を反転していますね。
Board の render 内にある “status” テキストも変更して、どちらのプレーヤの手番なのかを表示するようにしましょう。
・Boardコンポーネント

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    return (
      // the rest has not changed

・Boardコンポーネント全体

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,
    });
  }

  renderSquare(i) {
    return (
      <Square
        value={this.state.squares[i]}
        onClick={() => this.handleClick(i)}
      />
    );
  }

  render() {
    const status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');

    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>
    );
  }
}

おおよその枠組みができました。ちなみにこのままだとOとXが同じSquareを奪い合える状態ですね、これは後で直すんでしょう。

ゲーム勝者の判定

次にやることはゲームが決着して次の手番がなくなった時にそれを表示することです。新たにcalculateWinner関数を追加するようです。
・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;
}

9 つの square の配列が与えられると、この関数は勝者がいるか確認し、'X' か 'O'、あるいは null を返します。揃っているかの確認はconst linesで地道に全部定義してありますね。これさらに大きい盤面になったら大変そう…、forとかで定義できるならそっちの方がよさそうですね。

Board の render 関数内で calculateWinner(squares) を呼び出して、いずれかのプレーヤが勝利したかどうか判定し、決着がついた場合は “Winner: X” あるいは “Winner: O” のようなテキストを表示するように変更します。
・Boardコンポーネント

  render() {
    const winner = calculateWinner(this.state.squares);
    let status;
    if (winner) {
      status = 'Winner: ' + winner;
    } else {
      status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
    }

    return (
      // the rest has not changed

レンダーするところで定義してますね。if(winner)でwinnerがいる場合はこちらに入り、nullだった場合はelseの方に入っています。

・Boardコンポーネント

  handleClick(i) {
    const squares = this.state.squares.slice();
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    squares[i] = this.state.xIsNext ? 'X' : 'O';
    this.setState({
      squares: squares,
      xIsNext: !this.state.xIsNext,
    });
  }

先ほど言ったクリックしたマス目が埋まっている場合の処理ですね。決着がついている場合も即returnしてクリックしても次のターンに進まないようになっています。実行結果↓
f:id:msteacher:20210811145650p:plain

おわりに

今回はゲームを完成させる部分までやりました。Squareは表示とクリックされたかの判定をするだけに役割を分けることで、基本の操作はBoardに集約し、管理しやすくなりました。stateとpropsの使い方もだんだんわかってきた気がします。次回はsquaresのイミュータビリティの重要性がわかるタイムトラベル機能の追加の部分をやっていきたいと思います。それではまた。

【勉強メモ】Reactのチュートリアルを試してみた その1

はじめに

前回の.net coreに続いて仕事でReact勉強しといて!っていわれたのでreact公式のチュートリアルをやってみます。java scriptとhtmlの知識は当然のように必要だそう…。今回は概要編をやってみます。コードはブラウザでできるものを利用しました→
https://codepen.io/gaearon/pen/oWWQNa?editors=0010

ja.reactjs.org

React とは?

React はユーザインターフェイスを構築するための、宣言型で効率的で柔軟な JavaScript ライブラリだそうで、基本的な文法はjava scriptっぽいです。複雑な UI を、「コンポーネント」と呼ばれる小さく独立した部品から組み立てることができるのが特徴だそう。javaのクラスのUI版ってイメージでしょうか。そのコンポーネントの中のReact.componetから勉強が始まります。

class ShoppingList extends React.Component {
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
          <li>Oculus</li>
        </ul>
      </div>
    );
  }
}

// Example usage: <ShoppingList name="Mark" />

コンポーネントは、React に”何を描画したいか”を伝えます。データが変更されると、React はコンポーネントを効率よく更新して再レンダーします。と書いており、つまりデータが更新されたときの再表示が効率よく行えるということでしょうか。

ここで ShoppingList は React コンポーネントクラスです。コンポーネントprops(“properties” の略)と呼ばれるパラメータを受け取りrender メソッドを通じて、表示するビューの階層構造を返します。
render メソッドが返すのはあなたが画面上に表示したいものの説明書きです。React はその説明書きを受け取って画面に描画します。具体的には、render は、描画すべきものの軽量な記述形式である React 要素というものを返します。たいていの React 開発者は、これらの構造を簡単に記述できる “JSX” と呼ばれる構文を使用しています。htmlがreact要素に変換されるイメージですかね。

JSX では JavaScript のすべての能力を使うことができます。どのような JavaScript の式も JSX 内で中括弧に囲んで記入することができます。各 React 要素は、変数に格納したりプログラム内で受け渡ししたりできる、JavaScript のオブジェクトです。
それぞれの React のコンポーネントカプセル化されており独立して動作します。これにより単純なコンポーネントから複雑な UI を作成することができます。

まあいろいろ書いてあってわかりにくいですが、おそらく

スターターコードの中身を確認する

コードを見てみると、3 つの React コンポーネントがあります。HTMLをカプセル化したものが3つあるイメージですかね。

  • Square(正方形のマス目)
  • Board(盤面)
  • Game
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>
    );
  }
}

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

ReactDOM.render(
  <Game />,
  document.getElementById('root')
);

見たところHTMLの要素がコンポーネントに分かれていて、オブジェクト指向HTMLのような見た目をしているよう。
Square(マス目)コンポーネントは 1 つの <button> をレンダーし、Board(盤面)が 9 個のマス目をレンダーしています。Boardクラスのという書き方がそれを示しているそうですね。関数を呼び出すイメージでしょうか。Game コンポーネントは盤面と、後ほど埋めるものらしい。

データを Props 経由で渡す

まずは以下のように変更するそうです。
・Boardコンポーネント

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

・Squareコンポーネント

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

propsは先ほど引数みたいなイメージって言っていたやつですね。<Square value={i} />でSquareコンポーネントにpropsとしてvalueを渡しています。それをSquareコンポーネントがthis.props.valueでその中身を取り出していますね。これだとまるでSquareクラスにある見えないpublicなフィールドに外部から値を入れているようですね。
実行結果↓
f:id:msteacher:20210810155854p:plain

インタラクティブコンポーネントを作る

次にSquareコンポーネットがクリックされた場合にXが表示されるような変更をするそうです。
・Squareコンポーネント

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

これはよくあるクリックされたときにonClickが呼ばれてアラートが表示されるやつですね。ちなみにfunctionを省略した↓のような形がよく用いられるそうです。

<button className="square" onClick={() => alert('click')}>

コンポーネントが何かを「覚える」ためには、state というものを使います。javaで言うprivateのフィールドのイメージですね。
・Squareコンポーネント

class Square extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: null,
    };
  }

  render() {
    return (
      <button className="square" onClick={() => alert('click')}>
        {this.props.value}
      </button>
    );
  }
}

コンストラクターを使うところ本当にjavaっぽい。propsを受け取っていますが、propsがさっきの例の見えないpublicフィールドだとすれば、stateは見えるprivateフィールドで、オブジェクト指向の考え方であればこのstateを使うのがよさそう。this.stateにはフィールドの値を初期化しています。

・Squareコンポーネント

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>
    );
  }
}

この変更では、onclickで呼ばれているものがSquareのフィールドにもつstateのvalueに'X'を代入していますね。先ほどはpropsからvalueを読みだしていましたが、button内の表示はstateのvalueに変更されています。これによってこのボタンを押したらstateの値がnullからXになるので、クリックされたところだけXが表示されるようになるんですね。
setState をコンポーネント内で呼び出すと、React はその内部の子コンポーネントも自動的に更新するそうで、setStateが再レンダー(表示の更新)の合図になっているんですね。実行結果は↓
f:id:msteacher:20210810162400p:plain

おわりに

Reactはhtml,javascirptをオブジェクト指向的な書き方ができるってイメージまでつかむことができるようになりました。複雑なwebページになればなるほど重宝しそうな書き方なので、さらに学んでいきたいと思いました。次回はゲームを完成させる部分をやってみたいと思います。それではまた。

【勉強メモ】JavaScriptを使用してASP.NET Core Web APIを呼び出すチュートリアルを試してみた

はじめに

前回の最後に行っていた、JavaScript を使用して ASP.NET Core Web API を呼び出すチュートリアルも勉強してみました。CSS、HTML、JavaScript に関する知識が必要らしいけど、ちょっと触ったことある程度だけど大丈夫かな?
docs.microsoft.com

JavaScriptでWeb APIを呼び出す

To Do アイテムを作成および管理するためのフォームが含まれる HTML ページを追加することが主にやることだそう。結構楽しそう。この前やったGETとかはイベント ハンドラによって、送信されるみたい。Fetch APIのfetch関数により、各HTTP要求を記述するそうだ。postmanで書いていたやつをプログラム的に書く方法だね。
”fetch 関数からは Promise オブジェクトが返され、このオブジェクトには Response オブジェクトとして表された HTTP 応答が含まれます。一般的なパターンでは、Response オブジェクトに対して json 関数を呼び出して、JSON 応答の本文を抽出します。”と書いてあって、まあつまり返り値でもらったものをJSONで解釈すれば、前回のような応答文が読み取れるって感じなのかな?

"1.静的ファイルを提供し、既定のファイルマッピングを有効にするように、アプリを構成します。 Startup.cs の Configure メソッドでは、次の強調表示されたコードが必要です。"って書いてあるけど、多分自分で作ったファイルを開いてもらうようにするための変更かな?
・Startup.cs

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
               opt.UseInMemoryDatabase("TodoList"));
            services.AddControllers();

        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseDefaultFiles(); //追加
            app.UseStaticFiles(); //追加

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

2~6は言われたとおりに作成した。
・index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>To-do CRUD</title>
    <link rel="stylesheet" href="css/site.css" />
</head>
<body>
    <h1>To-do CRUD</h1>
    <h3>Add</h3>
    <form action="javascript:void(0);" method="POST" onsubmit="addItem()">
        <input type="text" id="add-name" placeholder="New to-do">
        <input type="submit" value="Add">
    </form>

    <div id="editForm">
        <h3>Edit</h3>
        <form action="javascript:void(0);" onsubmit="updateItem()">
            <input type="hidden" id="edit-id">
            <input type="checkbox" id="edit-isComplete">
            <input type="text" id="edit-name">
            <input type="submit" value="Save">
            <a onclick="closeInput()" aria-label="Close">&#10006;</a>
        </form>
    </div>

    <p id="counter"></p>

    <table>
        <tr>
            <th>Is Complete?</th>
            <th>Name</th>
            <th></th>
            <th></th>
        </tr>
        <tbody id="todos"></tbody>
    </table>

    <script src="js/site.js" asp-append-version="true"></script>
    <script type="text/javascript">
        getItems();
    </script>
</body>
</html>

・site.css

input[type='submit'], button, [aria-label] {
    cursor: pointer;
}

#editForm {
    display: none;
}

table {
    font-family: Arial, sans-serif;
    border: 1px solid;
    border-collapse: collapse;
}

th {
    background-color: #f8f8f8;
    padding: 5px;
}

td {
    border: 1px solid;
    padding: 5px;
}

この7のsite.jsが今回の主題であるfetch 関数のところだね。GETとかの命令がjavascriptで記述されているっぽい。
・site.js

const uri = 'api/TodoItems';
let todos = [];

function getItems() {
  fetch(uri)
    .then(response => response.json())
    .then(data => _displayItems(data))
    .catch(error => console.error('Unable to get items.', error));
}

function addItem() {
  const addNameTextbox = document.getElementById('add-name');

  const item = {
    isComplete: false,
    name: addNameTextbox.value.trim()
  };

  fetch(uri, {
    method: 'POST',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
    .then(response => response.json())
    .then(() => {
      getItems();
      addNameTextbox.value = '';
    })
    .catch(error => console.error('Unable to add item.', error));
}

function deleteItem(id) {
  fetch(`${uri}/${id}`, {
    method: 'DELETE'
  })
  .then(() => getItems())
  .catch(error => console.error('Unable to delete item.', error));
}

function displayEditForm(id) {
  const item = todos.find(item => item.id === id);
  
  document.getElementById('edit-name').value = item.name;
  document.getElementById('edit-id').value = item.id;
  document.getElementById('edit-isComplete').checked = item.isComplete;
  document.getElementById('editForm').style.display = 'block';
}

function updateItem() {
  const itemId = document.getElementById('edit-id').value;
  const item = {
    id: parseInt(itemId, 10),
    isComplete: document.getElementById('edit-isComplete').checked,
    name: document.getElementById('edit-name').value.trim()
  };

  fetch(`${uri}/${itemId}`, {
    method: 'PUT',
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(item)
  })
  .then(() => getItems())
  .catch(error => console.error('Unable to update item.', error));

  closeInput();

  return false;
}

function closeInput() {
  document.getElementById('editForm').style.display = 'none';
}

function _displayCount(itemCount) {
  const name = (itemCount === 1) ? 'to-do' : 'to-dos';

  document.getElementById('counter').innerText = `${itemCount} ${name}`;
}

function _displayItems(data) {
  const tBody = document.getElementById('todos');
  tBody.innerHTML = '';

  _displayCount(data.length);

  const button = document.createElement('button');

  data.forEach(item => {
    let isCompleteCheckbox = document.createElement('input');
    isCompleteCheckbox.type = 'checkbox';
    isCompleteCheckbox.disabled = true;
    isCompleteCheckbox.checked = item.isComplete;

    let editButton = button.cloneNode(false);
    editButton.innerText = 'Edit';
    editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

    let deleteButton = button.cloneNode(false);
    deleteButton.innerText = 'Delete';
    deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

    let tr = tBody.insertRow();
    
    let td1 = tr.insertCell(0);
    td1.appendChild(isCompleteCheckbox);

    let td2 = tr.insertCell(1);
    let textNode = document.createTextNode(item.name);
    td2.appendChild(textNode);

    let td3 = tr.insertCell(2);
    td3.appendChild(editButton);

    let td4 = tr.insertCell(3);
    td4.appendChild(deleteButton);
  });

  todos = data;
}

実行してみたところ以下のような画面が見れた。
f:id:msteacher:20210806201136p:plain

To Do アイテムのリストの取得

HTTP GET 要求の話で、前にやった↓のやつですね。

  • GET /api/TodoItems
function getItems() {
    fetch(uri)
        .then(response => response.json())
        .then(data => _displayItems(data))
        .catch(error => console.error('Unable to get items.', error));
}

Web API から正常に返ってくると、_displayItems 関数が呼び出されるそうです。
_displayItems関数は最後の方にあって、↓のやつですね。

function _displayItems(data) {
    const tBody = document.getElementById('todos');
    tBody.innerHTML = '';

    _displayCount(data.length);

    const button = document.createElement('button');

    data.forEach(item => {
        let isCompleteCheckbox = document.createElement('input');
        isCompleteCheckbox.type = 'checkbox';
        isCompleteCheckbox.disabled = true;
        isCompleteCheckbox.checked = item.isComplete;

        let editButton = button.cloneNode(false);
        editButton.innerText = 'Edit';
        editButton.setAttribute('onclick', `displayEditForm(${item.id})`);

        let deleteButton = button.cloneNode(false);
        deleteButton.innerText = 'Delete';
        deleteButton.setAttribute('onclick', `deleteItem(${item.id})`);

        let tr = tBody.insertRow();

        let td1 = tr.insertCell(0);
        td1.appendChild(isCompleteCheckbox);

        let td2 = tr.insertCell(1);
        let textNode = document.createTextNode(item.name);
        td2.appendChild(textNode);

        let td3 = tr.insertCell(2);
        td3.appendChild(editButton);

        let td4 = tr.insertCell(3);
        td4.appendChild(deleteButton);
    });

    todos = data;
}

forEachとアロー関数が出てきてもう何がなんだかわからなくなってきました。forEachは配列の各要素に関して、個別に操作を行うことができる関数で、アロー関数(item=>はfunction(item){を短縮したものらしいです。data配列の中身を一個ずつアクセスしていて、それをitemとして取り出し、その中にはこの前やったid,name,isCompleteが入っているので、item.idのようにすることでアクセスできるようです。checkboxとedit、deleteを作って、1つの行を作ってindex.htmlのに追加しているっぽいです。最後にグローバル変数のtodosに追加しているって感じですね。Web API 要求が失敗した場合は、ブラウザのコンソールにエラーが表示されるっぽいですね。

To Do アイテムの追加

表示方法はわかったけど、まずはデータを入れないとイメージできないので、追加方法を見てみます。

  • POST /api/TodoItems

の部分ですね。
実行結果は↓
f:id:msteacher:20210806222040p:plain

function addItem() {
    const addNameTextbox = document.getElementById('add-name');

    const item = {
        isComplete: false,
        name: addNameTextbox.value.trim()
    };

    fetch(uri, {
        method: 'POST',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(response => response.json())
        .then(() => {
            getItems();
            addNameTextbox.value = '';
        })
        .catch(error => console.error('Unable to add item.', error));
}

”item 変数は、To Do アイテムのオブジェクト リテラル表現を構築するために宣言されています。”…?リテラルとは?調べたところ、"リテラルとは、プログラムに直接記述するデータ値のことです。コードに書いた値が、そのまま実行時にもその値のまま意味をもちます。"だそうで、おそらく今回jsonにして送るときに送りやすい形にしておくってイメージでしょうか。
fetchの中には

  • method— POSTを指定(postmanでは最初に選択してたやつ)
  • headers—Accept および Content-Type の HTTP 要求ヘッダーがapplication/json に設定され、それぞれ、受信および送信されるものとしてjsonを使っているってことですね。確かにpostmanでもjsonを指定した気がする。
  • body— 要求本文の JSON 表現が指定されていて、item をJSON.stringify関数(JavaScript のオブジェクトをJSON文字列に変換する関数)に渡すことによってJSON形式になって送信されるっぽいですね。

これによってHTTP POST 要求が api/TodoItems ルートに送信され、返ってきた値に対して、さっき見たgetItems()で画面表示がされているみたいです。

To Doアイテムの更新

この話ですね。更新できるってやつです。

  • PUT /api/TodoItems/{id}

{id}に、更新するアイテムのidが指定されます。api/TodoItems/1 のようになるそうです。

function updateItem() {
    const itemId = document.getElementById('edit-id').value;
    const item = {
        id: parseInt(itemId, 10),
        isComplete: document.getElementById('edit-isComplete').checked,
        name: document.getElementById('edit-name').value.trim()
    };

    fetch(`${uri}/${itemId}`, {
        method: 'PUT',
        headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(item)
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to update item.', error));

    closeInput();

    return false;
}

さっきのGETとほぼおんなじですがmethodは、PUTになっていますね。また、fetchのところみると、`${uri}/${itemId}`になっていてちゃんとidが送られています。itemIdはどっからきてるのかなーって見てみたら、あるtodoのEditボタンが押されたときに、displayEditForm関数でそのtodoのidがindex.htmlののところへ代入されているんですね。
editボタンを押すと、displayEditForm関数で新しい修正用フォームが以下のように表示されます。
f:id:msteacher:20210806223445p:plain
試しにnameをNakasu Kasumiに変更してみました。
f:id:msteacher:20210806223510p:plain

To Do アイテムの削除

DELETEのやつですね。指定したのを消せるやつ。

  • DELETE /api/TodoItems/{id}

さっきのPUTとほぼ同じで、method オプションを DELETE に設定して、最後にidをつけるだけですね。

function deleteItem(id) {
    fetch(`${uri}/${id}`, {
        method: 'DELETE'
    })
        .then(() => getItems())
        .catch(error => console.error('Unable to delete item.', error));
}

Deleteのときは特にjsonの送信がないので、簡単ですね。idはdeleteボタンのdeleteButton.setAttribute('onclick', `deleteItem(${item.id})`)で指定していました。削除したら、再度GETして表示データを更新しています。
実行結果↓
f:id:msteacher:20210806224133p:plain

IsCompleteは動いてる?

これで基本の関数の説明は終わりましたが、あれIsCompleteってやってないけど、ただの飾りなんじゃね?って思ってしまいました。そしたらeditでこのチェックボックスが入れられる事に気づき、ちゃんと保存されているかを確かめるために↓のように新たにtodoを追加してIsCompleteにチェックを入れてみました。
f:id:msteacher:20210806224734p:plain

データベースに保存されていれば、この画面を開き直してもIsCompleteがチェックされた状態ででてくるはずです。ブラウザを開き直したところ
f:id:msteacher:20210806224856p:plain

ちゃんと保存されていました。疑ってごめんなさい。

おわりに

.net Coreをjavascriptでweb上で操作できるようになりました。より実際のアプリに近いものを作ることができたので勉強になりました。
ただちょっとチュートリアルが日本語が微妙に直訳感があって読みにくかったです…。でもソースコードは世界共通ってこともあって、コードを読めば何をしているかなんとなくわかるのは改めてすごいと思いましたね。
また何か勉強したら共有したいと思います。それではまた。

【勉強メモ】ASP.NET CoreでWeb APIを作成するチュートリアルを試してみた その2

はじめに

.net core勉強のその2です。前回の続きのGET メソッドの確認から勉強していきます。
docs.microsoft.com

GET メソッドの確認

このAPIの動作を確認する。

  • GET /api/TodoItems
  • GET /api/TodoItems/{id}

postmanの実際の記述は以下のようになるっぽい。

これによってGetTodoItemsが呼び出される。
{id}は取り出す番号っぽい、idは入れた順番に登録されているようだ。
試しにwalk catをpostしてみたらidが2で登録されていた。
f:id:msteacher:20210805214924p:plain

逆に{id}を指定しない場合は全部取得できるようだ。
f:id:msteacher:20210805215039p:plain

なんとなくのHTTPメソッドのGETの挙動を理解できた気がする。

ルーティングとURLパス

・TodoItemsController.cs

[Route("api/[controller]")]
[ApiController]
public class TodoItemsController : ControllerBase
{

[controller] をコントローラーの名前に置き換えられるそうで、 今回はTodoItems Controllerなので、コントローラー名は"TodoItems"(Controllerが除かれる)らしい。
つまりapi/TodoItemsになって、さっきのGETの文と同じになるんですね。
さっきのGetTodoItemが呼び出されると、そのidにURLの"{id}"の値が指定されるっぽい。結構便利。

戻り値

戻り値の型は、ActionResult型だそうで、JSON形式で返ってくる。↓みたいな感じなやつですね。うまく作ればJavascirptでいい感じに処理できそう(語彙力)。

    {
        "id": 1,
        "name": "walk dog",
        "isComplete": true
    }

"この戻り値の型の応答コードは 200 で、ハンドルされない例外がないものと想定します。ハンドルされない例外は 5xx エラーに変換されます。"だそうで、たしかにpostmanの結果を見ると、"200 OK"と書いてあって、存在しないidを指定すると確かに"404 Not Found"になった。
f:id:msteacher:20210805220350p:plain

PutTodoItemメソッド

  • PUT /api/TodoItems/{id}

の話で、

のようにHTTP PUTを指定するっぽい。
"応答は 204 (No Content) となります。 HTTP 仕様に従って、PUT 要求では、変更だけでなく、更新されたエンティティ全体を送信するようクライアントに求めます"だそうで、JSONのid1つの全体を送る必要があるらしい。

"Id = 1 の To Do アイテムを更新し、その名前を "feed fish" に設定します。"といわれたのでそのまま実行してみた。
f:id:msteacher:20210805220839p:plain

となって、GETを呼ぶと、
f:id:msteacher:20210805220939p:plain

となり、ちゃんと内容が変更されていることがわかった。

DeleteTodoItem メソッド

ここまでくると次がなんなのかなんとなくわかってきた。

  • DELETE /api/TodoItems/{id}

idで指定したものを削除するんでしょう。
DeleteTodoItemメソッドを使用する場合はメソッドをDELETEにしてURIを以下のように指定する。

今更だがURLとURIって何が違うんやって思い調べたら、"「URI」のほうが広い概念で、「URL」はURIの部分集合です。"だそうです。URIは名前または場所を識別する書き方のルールの総称で、URLは場所を示す書き方のルールで、ページや画像などを取得したりするための主要な場所とアクセス方法を指定するものだそうだ。
DELETEで1を指定して削除してGETしてみたところ、以下のような結果となった。
f:id:msteacher:20210805222206p:plain

2番目にいれたwalk catはidは2のままになっていて、とくに前につめられたりはしないようだ。でもデータベースの管理としては当たり前か…。

過剰な投稿を防止する

セキュリティのために入力されるデータや返されるデータを制限するのが通常環境のアプリだそう。"モデルのサブセットは、通常、データ転送オブジェクト (DTO)、入力モデル、またはビュー モデルと呼ばれます。 この記事では DTO を使用しています。"だそうで、DTOを利用すると、過剰な投稿を防止したり、クライアントに表示させたくないものを非表示にしたりできるみたい。そのためにまずDTOを実現するためにシークレットフィールドを作るそう。
・TodoItem.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
        public string Secret { get; set; }
    }
}

管理アプリの場合は公開することを選択でき、シークレット フィールドを投稿および取得できるようになっている。たしかにこのままだと何もついてないからアクセスできそう。
次にDTOモデルを作成。

・TodoItemDTO.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TodoApi.Models
{
    public class TodoItemDTO
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

なるほど、データ転送時はDTOを使うことで見えなくするってことか。
TodoItemDTO を使用するように TodoItemsController.csを更新して出来上がり。

・ TodoItemsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
        {
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return ItemToDTO(todoItem);
        }

        [HttpPut("{id}")]
        public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)
        {
            if (id != todoItemDTO.Id)
            {
                return BadRequest();
            }

            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            todoItem.Name = todoItemDTO.Name;
            todoItem.IsComplete = todoItemDTO.IsComplete;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))
            {
                return NotFound();
            }

            return NoContent();
        }

        [HttpPost]
        public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)
        {
            var todoItem = new TodoItem
            {
                IsComplete = todoItemDTO.IsComplete,
                Name = todoItemDTO.Name
            };

            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            return CreatedAtAction(
                nameof(GetTodoItem),
                new { id = todoItem.Id },
                ItemToDTO(todoItem));
        }

        [HttpDelete("{id}")]
        public async Task<IActionResult> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return NoContent();
        }

        private bool TodoItemExists(long id) =>
             _context.TodoItems.Any(e => e.Id == id);

        private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>
            new TodoItemDTO
            {
                Id = todoItem.Id,
                Name = todoItem.Name,
                IsComplete = todoItem.IsComplete
            };
    }
}

ちょっと急に長くなって差分がよくわかんなかったのでGETだけ比較してみた。
・変更前

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            return await _context.TodoItems.ToListAsync();
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return todoItem;
        }

・変更後

        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()
        {
            return await _context.TodoItems
                .Select(x => ItemToDTO(x))
                .ToListAsync();
        }

        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return ItemToDTO(todoItem);
        }

これを見ると.Select(x => ItemToDTO(x))でシークレットが指定され、引数で受け取る部分と帰り値がTodoItemDTOに変更されていて、シークレットにアクセスできなくなっていることがわかりました。
POSTで追加してみた。

f:id:msteacher:20210805224737p:plain

なんか追加できているようにも見えるけど、JSONだから多分Secretのところは読み飛ばされて、入っていないのでしょう。
GETして取得してみた。

f:id:msteacher:20210805224833p:plain

Secretが表示されていませんでした。ユーザには見せたくない内部的な処理を書くときには良さそうですね。

おわりに

.net coreについてなんとなーく雰囲気がつかめた気がします。いろいろやってみてさらに理解が深めていけたらと思います。
今回はPostmanを使いましたが、JavaScript を使用した Web API の呼び出しもチュートリアルがあるそうなので、こっちだとわりとwebアプリっぽそうなので、次はそれも記事にしてみたいと思います。
それではまた。
docs.microsoft.com

【勉強メモ】ASP.NET CoreでWeb APIを作成するチュートリアルを試してみた その1

はじめに

仕事で.net coreを使った開発するから勉強しといて!って言われ、python、C、Javadeep learningぐらいしかできない僕がはじめてweb apiを勉強したのでメモ書き程度に残しておきます。今回はwindows10上でVisual studio Community 2019を使いました。参考にしたサイトは↓こちら。ちょっと日本語が怪しい気が…。
docs.microsoft.com

このチュートリアルで実現できること

API:説明

  • GET /api/TodoItems:すべての To Do アイテムを取得します。
  • GET /api/TodoItems/{id}:ID でアイテムを取得します。
  • POST /api/TodoItems:新しいアイテムを追加します。
  • PUT /api/TodoItems/{id}:既存のアイテムを更新します。
  • DELETE /api/TodoItems/{id}:アイテムを削除します


アプリのデザインは↓のような感じらしい。MVC(モデル・ビュー・コントローラー)モデルについては多少の知識があったので、少しは理解できた。データベースから取り出したモデルをコントローラーがクライアント(ビュー)に投げたり、逆にクライアントからrequestを受け取ったりという感じでしょうか。
f:id:msteacher:20210804214119p:plain

まあ多分超簡単に言えば、データベースにtodoリストを登録したり更新したりするって感じでしょうか。

APIのテスト

プロジェクトの作成を言われるがまま作成して、apiテストをやるとちゃんとjson形式で結果が返ってきた。これがなんの意味なのかは不明…。

[{"date":"2021-08-05T21:48:33.466844+09:00","temperatureC":18,"temperatureF":64,"summary":"Freezing"},{"date":"2021-08-06T21:48:33.4679942+09:00","temperatureC":34,"temperatureF":93,"summary":"Mild"},{"date":"2021-08-07T21:48:33.4679962+09:00","temperatureC":37,"temperatureF":98,"summary":"Bracing"},{"date":"2021-08-08T21:48:33.4679965+09:00","temperatureC":28,"temperatureF":82,"summary":"Bracing"},{"date":"2021-08-09T21:48:33.4679966+09:00","temperatureC":-15,"temperatureF":6,"summary":"Chilly"}]

モデルクラスの追加

C#のコードを指示通りコピペすると、コントローラーのスキャフォールディングのところで躓くので、以下のようにnamespaceを定義して書いた。C#は全く触ったことなかったので書き方がわからない…、調べたらnamespaceは名前空間と呼ぶらしい。namespaceを区別することで、同じ名前のクラスを使い分けることを可能になるらしく、同じクラス名でもここを分ければ共存(?)できるっぽい?
・TodoItem.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace TodoApi.Models
{
    public class TodoItem
    {
        public long Id { get; set; }
        public string Name { get; set; }
        public bool IsComplete { get; set; }
    }
}

Idはリレーショナル データベース内の一意のキーとして機能するらしく主キー的な存在なのかな?ついでにnameはtodoのやることの名前で、IsCompleteは完成したかどうかを判定するbool変数っぽいです。

データベース コンテキストの追加

コンテキストってなんぞや…って思い調べたら、"コンテキストとは、「一連の処理中で引き継いでいく情報の集合体」のイメージです。異なるスレッドやタスクを跨ぐこともできます。"だそうで、ただのデータの集まりのイメージで、その集まりがビューとかに流れていくものなのかな?
・TodoContext.cs

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models
{
    public class TodoContext : DbContext
    {
        public TodoContext(DbContextOptions<TodoContext> options)
            : base(options)
        {
        }

        public DbSet<TodoItem> TodoItems { get; set; }
    }
}

なんですかここの書き方は…。見慣れない書き方に動揺してしまいマス。少しずつc#にもなれていかねば…。get; set;はゲッターとセッターなのかな?超簡単にかけるやん…。

データベース コンテキストの登録

なんか修正点だけ強調表示されていますが、バージョンの違いかわかりませんが、他の部分も違ってなんか動かなかったので、全削除してまるまるコピー。
・Startup.cs

// Unused usings removed
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<TodoContext>(opt =>
               opt.UseInMemoryDatabase("TodoList"));
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}

ConfigureServicesメソッドのservices.AddDbContextはなんとなくDBにこのコンテキストを入れるってイメージできますが、Configureメソッドの方はなにやってるんだかちんぷんかんぷん(泣)
"データベース コンテキストがメモリ内データベースを使用することを指定します。"って意味も正直よくわからないけど、コンテキストがメモリ内のデータベースを通るよってことでいいのかな。

コントローラーのスキャフォールディング

またわけわかんない単語が…。"スキャフォールディングとは、データモデルとなる型を元に、いわゆるCRUD(Create/Read/Upadate/Delete)と呼ばれる追加、読込、変更、削除を行う画面とそのコードを自動で生成する機能のことです。"だそうで、つまりデータベースの追加とかを自動生成してくれるってことっぽいね。これでコンテキスト(TodoContext)をコントローラーに挿入できたっぽい。

PostTodoItem 作成メソッドの確認

"nameof 演算子を使用するために、PostTodoItem で return ステートメントを置き換えます。"とは…。nameof演算子がわからないので調べたら、"nameof演算子を使うと、名前空間/型/メソッド/プロパティ/変数などの単純な名前(=名前空間やクラス名などで修飾されていない名前)の文字列が取得できる。"だそうです。なんで使うかは後に少し書いてありました。
CreatedAtActionメソッドは帰り値として成功するとHTTP201状態コードが返ってくるそう。応答にLocationヘッダーが追加されて返ってくるそうで、Locationヘッダーには、新しく作成されたTo DoアイテムのURIが入っているらしい。
"GetTodoItemアクションを参照して Location ヘッダーの URI を作成します。 C# の nameof キーワードを使って、CreatedAtAction 呼び出しでアクション名をハードコーディングすることを回避しています。"…とは?ちょっとこれは本当に意味がわからなかったので、そういうものとしておきます…。

・TodoItemsController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using TodoApi.Models;

namespace TodoApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class TodoItemsController : ControllerBase
    {
        private readonly TodoContext _context;

        public TodoItemsController(TodoContext context)
        {
            _context = context;
        }

        // GET: api/TodoItems
        [HttpGet]
        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()
        {
            return await _context.TodoItems.ToListAsync();
        }

        // GET: api/TodoItems/5
        [HttpGet("{id}")]
        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)
            {
                return NotFound();
            }

            return todoItem;
        }

        // PUT: api/TodoItems/5
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutTodoItem(long id, TodoItem todoItem)
        {
            if (id != todoItem.Id)
            {
                return BadRequest();
            }

            _context.Entry(todoItem).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!TodoItemExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/TodoItems
        // To protect from overposting attacks, enable the specific properties you want to bind to, for
        // more details, see https://go.microsoft.com/fwlink/?linkid=2123754.
        [HttpPost]
        public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)
        {
            _context.TodoItems.Add(todoItem);
            await _context.SaveChangesAsync();

            //return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);
            return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);
        }

        // DELETE: api/TodoItems/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<TodoItem>> DeleteTodoItem(long id)
        {
            var todoItem = await _context.TodoItems.FindAsync(id);
            if (todoItem == null)
            {
                return NotFound();
            }

            _context.TodoItems.Remove(todoItem);
            await _context.SaveChangesAsync();

            return todoItem;
        }

        private bool TodoItemExists(long id)
        {
            return _context.TodoItems.Any(e => e.Id == id);
        }
    }
}

Postman を使用した PostTodoItem のテスト

www.postman.com
Postmanって使ったことなかったんですが、調べたら"API 開発をする際にアクセス、モック作成、テストなどを個人やプロジェクトで使うことができる API クライアント/サーバーです。"だそうで、web開発未経験の僕からするとちょっと何言ってるかわからないですって感じですが、APIのテストが簡単にできるってイメージですかね。
手順としてはvisual studioの実行部分のIIS ExpressをTodoApiに変更して押してサーバーを立ち上げた状態にします。たぶんDBが立ち上がるのでここで入れたり消したりできるようになってるんでしょう。
f:id:msteacher:20210804223839p:plain

postmanの+ボタンを押してリクエストを送るところを表示します。あとは手順通りにPOSTを実行するとちゃんと登録できたっぽいです。
f:id:msteacher:20210804225539p:plain

Postman で Location ヘッダーの URI をテストする

先程の結果をみるとHeaderが5件返ってきており、Locationのところに"https://localhost:5001/api/TodoItems/1"がありました。
HTTPメソッドをGETにしてURIをこれにすると
f:id:msteacher:20210804230015p:plain
ちゃんとさっき登録したデータが返ってきました。よかった。

おわりに

ここまでで半分くらいですが、知らないことばかりで調べながらだと本当に苦労しました…。いつかこれがスイスイ書けるようになる日がくるのでしょうか…。
ちなみにGET メソッドの確認やその先の内容についてはまた次回勉強しながら書きます。それではまたよろしくおねがいします。

【ドイツ・ベルリン】シャルロッテンブルク宮殿に行ってみた

はじめに

ドイツ、ベルリンに行った話の、シャルロッテンブルク宮殿に行ってみたお話です。
今までとは違いここに行ったときは十分時間を取れたので、中まで散策し、ゆっくり観光しました!

シャルロッテンブルク宮殿(Schloss Charlottenburg)とは

ベルリンの中心部に建てられた華麗な宮殿で、プロイセン王・フリードリヒ1世が18世紀に妃ゾフィーシャルロッテのために、夏の離宮として建てられた宮殿です。宮殿だけでなく、ベルベデーレ、新パビリオン、霊廟なども建ち並んでおり、広大な庭園は一度は見ておきたいドイツ、ベルリンを代表する観光スポットです。

シャルロッテンブルク宮殿へ

f:id:msteacher:20200824173355j:plain
Uバーン(地下鉄)のSophie-Charlotte-Platz駅から↓のような道を歩いて約15分歩いたところにあります。ゾフィーシャルロッテのプラッツ(広場)っていう駅なので、降りるときもわかりやすいですね。
f:id:msteacher:20200824173021j:plain

f:id:msteacher:20200824173125j:plain
バスで宮殿の目の前までいく路線もありますが、駅から宮殿までのSchloßstraße(シュロス)通りは、ヨーロッパの美しい街並みを見ながらいけるので、宮殿まで歩いていくのもオススメです♪

goo.gl

宮殿入口

近くまでくると、クリーム色の大きい宮殿が見えてきます。
f:id:msteacher:20200824174310j:plain

ゾフィーシャルロッテは女性であることもあり、塗装はやわらかいクリーム色がベースで、全体的に曲線の美しいシルエットが特徴の宮殿です。
f:id:msteacher:20200824174458j:plain

園内は非常に広く、大きな庭園があることも見どころのひとつですね。
f:id:msteacher:20200824174624j:plain

チケットは全館周れるCharlottenburg+を買いました。価格は17€(学生は13€)です。学割は日本語の学生証でも問題なかったです。館内で写真撮影を行うには、さらに写真撮影料(カメラチケット)(3€)支払って、以下の写真の一番下にある赤いリストバンドを買います。これを買わないで写真撮影すると、おこられている人をみかけたので、写真撮影するなら絶対に買いましょう。
f:id:msteacher:20200825163840p:plain

入口付近でオーディオガイドも無料で借りれます。なんと日本語に対応していました!ありがたい…。

宮殿本棟

館内入れると、そこは西洋の宮殿らしく、豪華絢爛な内装に圧倒されました。日本語解説を聞きながら周れるので、より理解を深めることが出来ます。
f:id:msteacher:20200825164729j:plain
f:id:msteacher:20200825164754j:plain
f:id:msteacher:20200825165245j:plain
f:id:msteacher:20200825170523j:plain

シャルロッテンブルク宮殿で特に有名な陶磁器の間(Porzellankabinett)では、中国や日本の陶磁器が壁面にぎっしりと飾られていて、圧巻でした。
f:id:msteacher:20200825164949j:plain
f:id:msteacher:20200825165011j:plain

f:id:msteacher:20200825170014j:plain
数学でお世話になった"ライプニッツの公式"で有名なライプニッツさんがこんなところに…。ゾフィーシャルロッテさんに勉強を教えていたそうで、ベルリンで科学アカデミーの創設などにも参加した人らしく、公式だけの人だと思っていた印象がガラリと変わりましたね笑

f:id:msteacher:20200825165104j:plain
f:id:msteacher:20200825165149j:plain
宮殿の丁度真ん中あたりからは、庭園が見渡せます。庭園の綺麗な模様も一望できます。

新翼

宮殿東部からは新翼という部分に入れます。

f:id:msteacher:20200825170724j:plain
f:id:msteacher:20200825170759j:plain
本棟の隣に建つ新翼はロココ調で、本棟よりもずっとシンプルなすっきりとした印象を受けます。

f:id:msteacher:20200825170546j:plain
ここで最も有名なところが、ロココの可憐な装飾が印象的な「ゴールデンギャラリー」です。フリードリヒ大王の時代に造られた最も重要なロココのインテリアのひとつです。

庭園

宮殿をぐるりと回って、入口と反対方向までくると大きく広がる庭園が見えてきます。
f:id:msteacher:20200825200853j:plain

宮殿の上から見える綺麗な模様の近くのすぐそばを歩いてみると、綺麗な花々を楽しむことが出来ます。
f:id:msteacher:20200825200945j:plain

f:id:msteacher:20200825201113j:plain
庭園から見た宮殿

f:id:msteacher:20200825201131j:plain
噴水と宮殿

f:id:msteacher:20200825201203j:plain
庭園自体は広大で、宮殿の周辺にはこういった林が続いており、一周するだけでもけっこうな時間がかかります。この宮殿をすべて周るつもりなら、半日以上は必要ですね。私も午前中に入ったのですが、全部周り終わったのは2時を過ぎていた気がします。

おわりに

9回にわたって、ドイツのベルリンに行った話を書いてきました。初めてのヨーロッパで感動しっぱなしだったので、その面白さを伝えきれたか不安ではありますが、ベルリンに行く予定がある際は、ぜひ参考にしてもらえれば嬉しいです。