0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ReactチュートリアルをVue.jsで実装

Last updated at Posted at 2020-09-20

Vue.jsとReactの勉強のため,Reactのチュートリアルの「Tic Tac Toe」をVue.jsで実装してみる。

vue-cliを使ってプロジェクトを作成(省略)。
バージョンはVue2。

components/フォルダ以下に次のファイルを作成。

  • Square.vue
    • ゲームの1マスを管理
  • Board.vue
    • ゲームボード(9マス)を管理
  • Game.vue
    • ゲーム全体の状態(手番,履歴)を管理

ちなみにcssもチュートリアルと同じになるように移植した。

Square.vue

Square.vue
<template>
  <button class="square" @click="onClick">{{value}}</button>
</template>

<style scoped>
.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;
  background: #ddd;
}
</style>

<script>
export default {
  name: 'Square',
  props: {
    value: String,
    onClick: Function,
  },
}
</script>

Squareコンポーネントはゲームの1マスにあたるボタンを表示する。
同時にボタンクリックのイベントを発火してゲームが進行するのだが,ゲームの状態は上位のGameコンポーネントが管理している。
通常はemitを使うところなのだろうが,ここではbutton@clickに上位コンポーネントからプロパティとして引き渡されたonClick(名前は何でもよい)関数を当ててみたら上手く動いた。(この方法が正しいのかどうか,どなたかご教示ください。)

Board.vue

Board.vue
<template>
  <div>
    <div class="board-row" v-for="r in [0,1,2]" :key="r">
      <square v-for="c in [0,1,2]" :key="c"
        :value="squares[r*3+c]"
        :onClick="() => onClick(r*3+c)"
      />
    </div>
  </div>
</template>

<style scoped>
.board-row:after {
  clear: both;
  content: "";
  display: table;
}
.status {
  margin-bottom: 10px;
}
</style>

<script>
import Square from '@/components/Square'

export default {
  name: 'Board',
  components: {
    Square,
  },
  props: {
    squares: Array,
    onClick: Function,
  },
}
</script>

Boardコンポーネントはゲームの盤面である9つのマスを管理する。
Reactのチュートリアルでは,Square 1つのレンダリングを関数にして,それを9回呼び出す方式をとっている。
Reactではレンダリングのテンプレート自体がJavaScript(の拡張)なのでそういったことができるが,Vue.jsで相当する方法が判らなかったので,ここではv-forによる二重ループで実装した。
onClickは「上から渡されたハンドラ関数をマス番号の引数付きで呼び出す」アロー関数に設定する。

Game.vue

Game.vue
<template>
  <div class="game">
    <div class="game-board">
      <board
        :squares="current.squares"
        :onClick="handleClick"
      />
    </div>
    <div class="game-info">
      <div>{{status}}</div>
      <ol>
        <li v-for="(step, move) in history" :key="move">
          <button @click="() => jumpTo(move)">{{move ? 'Go to move #'+move : 'Go to game start'}}</button>
        </li>
      </ol>
    </div>
  </div>
</template>

<style scoped>
.game {
  display: flex;
  flex-direction: row;
}
.game-info {
  margin-left: 20px;
}
ol, ul {
  padding-left: 30px;
}
</style>

<script>
import Board from '@/components/Board'

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
}

export default {
  name: 'Game',
  components: {
    Board,
  },
  data() {
    return {
      history: [{
        squares: Array(9).fill(null)
      }],
      stepNumber: 0,
      xIsNext: true,
    }
  },
  computed: {
    current() {
      return this.history[this.stepNumber]
    },
    winner() {
      return calculateWinner(this.current.squares)
    },
    status() {
      return this.winner ? 'Winner: ' + this.winner : 'Next player: ' + (this.xIsNext ? 'X' : 'O')
    },
  },
  methods: {
    handleClick(i) {
      const history = this.history.slice(0, this.stepNumber + 1)
      const current = history[history.length - 1]
      const squares = current.squares.slice()
      if (calculateWinner(squares) || squares[i]) {
        return
      }
      squares[i] = this.xIsNext ? 'X' : 'O'
      this.history = history.concat([{ squares: squares }])
      this.stepNumber = history.length
      this.xIsNext = !this.xIsNext
    },
    jumpTo(step) {
      this.stepNumber = step
      this.xIsNext = (step % 2) === 0
    },
  }
}
</script>

Gameコンポーネントはゲーム全体の状態を管理し,履歴を遡るための機能を提供している。
やはりレンダリングテンプレートを関数にできないことから,<template>の中がReactに比べて無理をしがち。履歴制御ボタンの@clickやテキストにそのあたりが表れている。
Vue.jsの流儀であれば,ここもコンポーネントにするべきなのだろうが,今回はReactチュートリアルとの比較しやすさをとった。

App.vue

App.vue
<template>
  <div id="app">
    <game />
  </div>
</template>

<style lang="scss">
# app {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}
</style>

<script>
import Game from '@/components/Game'

export default {
  name: 'App',
  components: {
    Game,
  },
}
</script>

最後にGameコンポーネントを呼び出すようにApp.vueを修正。

動作画面はこんな感じ。
tic-tac-toe.png

今回の気付き

  • ReactではレンダリングテンプレートがJavaScriptであることにより,関数化などの柔軟な実装が可能。
  • 下位コンポーネントで発生したイベントを,上位コンポーネントのハンドラで処理するためにプロパティが使える(要調査)。
  • v-forで生成される要素に固有のハンドラを割り当てるために,@clickにアロー関数を設定する方法が使える。
0
0
0

Register as a new user and use Qiita more conveniently

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?