EY-Office ブログ

Vueに入門してみた(React、Vueどちらを使うべきか)

わけあって連休中にVue.jsを学んでみました。私は作りながら学ぶ React入門という本を書いていますが、実務でも2015年からReactを使っています。Reactはこの5年間どんどん改良され使いやすくなって来たので、今までVue.jsを学ぶ機会は訪れませんでしたが、今回わけあって学んでみました。

React、Vueどちらを使うべきか

Vue.jsは本当に簡単なのか?

ネット上では、Vue.jsはReactに比べ簡単に習得できると言われていますが、私にはそう思えませんでした。簡単というのは感覚的な指標です、その人の思考、経験等に大きく影響を受けるとものだと思います。

今回、感じたVue.jsの難しさは

  • Vue.jsのGetting Startedを見ていくと、jQuery等と同じくHTMLのscriptタグに直接書く、従来のスタイルで書かれています。しかし今時はVue CLIを使ったモダンJavaScript開発環境で作ると思います。従来のスタイルとモダン開発環境では data の記述コードが違い、混乱しました。
  • Vue.jsではHTMLスタイルのテンプレートを使います、条件式v-if、繰り返しのv-forなどいくつかの独自属性を覚えないといけません。一方ReactのJSXはJavaScriptの式なので、条件式はJSの条件式、繰り返しはmapメソッド等を使えば良いので暗記する必要はありません。実は私は暗記が兆苦手です ^^);
  • テンプレート内でのデータ(data, computed等)の参照は値(コンテンツ)部分では <div>{{text}}</div> のように式で参照できますが、属性 <TitleCompnent v-bind:title="text" />のようにv-bindディレクティブを使う必要があります、この場合 "text" は文字列ではなくdata, computed等の名前として扱われます。このようにHTML文法に合わせたことで統一性が無くなるのは判りますが、モヤモヤします
  • 良く使うディレクティブには省略系があり、上の例は <TitleCompnent :title="text" />と書けますが、知らないうちは何だろうと悩みました。慣れれば便利なのでしょうが・・・
  • Vue.js(バージョン2)のコンポーネントはClassでdata(Reactのstate)の参照はにはthis. が必要になります。したがってmethods:内はfunction(){...}である必要があります、アロー関数は使えません! 無意識にアロー関数を書いてしまいdataが更新できず、長い時間ハマってしまいました ^^);

Vue.jsバージョン2はTypeScript対応に問題があるという記事をよく目にしていたので、今回TypeScriptは使いませんでした。また小さなコードしか書いてないのでVuexやVue Routerも使っていませんが、Vue.jsの設計思想は理解できました。

Vue.jsの良いところ

もちろん、 Vue.jsには良い点がたくさんあります。

  • 上で書いたようにHTMLのscriptタグに直接書く従来のスタイルでも使えるのは、全面的にモダンJavaScript開発環境に移行するのは敷居が高いと思える環境・人でも使える
  • コンポーネント内に、コンポーネント内ローカルのCSSを書ける。Reactではstyled-componentsなどを導入しないとできません、またJSXのstyleは属性名がCSSと同じではないのも頭痛の種です
  • computedプロパティーは便利かもしれない、ReactではuseMemoホック等を使って作らないといけません
  • State管理のVuex、ルーティングを扱うVue Routerなどの基本的なモジュールにVue公式があるのは初心者にはありがたいと思う
  • バージョン3でコンポーネントが関数になり、this.問題に悩ませられなくなる事や、TypeScript対応が良くなるようで期待大です
  • 下のように開発コードが、アニメーション名なのが楽しい
VersionTitle
2.6Sword Art Online : Alicization
2.5Level E
2.4Kill la Kill
2.3JoJo’s Bizarre Adventure
2.2Initial D
2.1Hunter X Hunter
2.0Ghost in the Shell
1.0Evangelion
0.12Dragon Ball
0.11Cowboy Bebop
0.10Blade Runner
0.9Animatrix

https://en.wikipedia.org/wiki/Vue.js#History からの部分引用

React , Vueは似ている

今回作ったジャンケン・ゲームのコードを サンプルコード に置きました。

ReactとVueでは表記は違いますが、コードはかなり似ています。
両者とも:

  • Virtual DOMの上に作られた宣言的なライブラリー
  • コンポーネント指向
  • コンポーネント状態が特定のデータ、ReactではState、Vueではdataで管理されている
  • 親コンポーネントから子コンポーネントへにはプロパティー(prop)を通して値が渡される
  • ReduxとVuex
  • React RouteとVue Route

最初に書いたように色々とハマりましたが、ReactとVueには共通点も多く 、1日程度でなんとか書けるようになりました。たぶんVue.jsのプログラマーがReactを学ぶさいにも学習コストは低いと思います。

ReactとVue.jsの両方の歴史には詳しくありませんが、お互いに影響を受け改良を続けてきたのではないかと思います。Vue CLIとCreate React appsの開発画面がそっくりなのに驚きした ^^)

iPhoneとAndoroidのように、お互いの良い機能を取り入れながら発展して行くのは、とても良い事だと思います。

React、Vue.jsどちらを使うべきかの選択基準

React、Vue.jsどちらを選ぶべきかの基準ですが、

  • ライブラリーの大きさ、特定の描画速度のベンチマークではVue.jsが有利なようです(ただし、これらが影響するアプリは僅かでしょう)
  • 現時点でネット上で公開されているコンポーネント・ライブラーの数はReactの方が多そうです(Vue.jsの開発者も増えているので将来はわかりませんが)
  • 現時点で開発者の人数は、世界レベルではReactの方が多いですが、日本では同程度でしょうか?(Vue.jsの開発者も増えているので将来はわかりませんが)
  • 現時点でネット上の情報も英語を含めるとReactの方が多いです(Vue.jsの開発者も増えているので将来はわかりませんが)

という事で、React、Vue.jsどちらかが圧倒的に優れているということはありません。大きな違いは設計思想です。

  • Reactは統一性のあるシンプルで強力な(コンピュターサイエンス的な)技術を導入する傾向があります
  • Vue.jsは利便性を重視し、従来のHTML,CSS,JSの文脈から大きくずれない傾向があります

技術選定にあたる方が、どちらを重視するかが選択の基準になると思います。

ただし、私はReactが好きです


サンプルコード

  • アプリの画面イメージ

アプリの画面イメージ

  • React
//  Jyanken.js
import React, { useState } from 'react'
import JyankenBox from './JyankenBox'
import ScoreBox from './ScoreBox'

const Jyanken = () => {
  const [scores, setScrores] = useState([])

  const pon = (human) => {
    const computer = Math.floor(Math.random() * 3)
    const judgment = (computer - human + 3) % 3
    const score = {human: human, computer: computer, judgment: judgment}
    setScrores([score, ...scores])
  }

  return (
    <>
      <h1>じゃんけん ポン!</h1>
      <JyankenBox actionPon={te => pon(te)} />
      <ScoreBox scrores={scores} />
    </>
   )
}

export default Jyanken

//  JyankenBox.js
import React from 'react'
import PropTypes from 'prop-types'

const JyankenBox= ({actionPon}) => {
  const divStyle = {margin: "0 20px"}
  const buttonStyle = {margin: "0 10px", padding: "3px 10px", fontSize: 14}
  return (
    <div style={divStyle}>
      <button onClick={() => actionPon(0)} style={buttonStyle}>グー</button>
      <button onClick={() => actionPon(1)} style={buttonStyle}>チョキ</button>
      <button onClick={() => actionPon(2)} style={buttonStyle}>パー</button>
    </div>
  )
}
JyankenBox.propTypes = {
  actionPon: PropTypes.func.isRequired
}

export default JyankenBox

//  ScoreBox.js
import React from 'react'
import PropTypes from 'prop-types'

const ScoreBox  = ({scrores}) => {
  const teString = ["グー","チョキ", "パー"]
  const judgmentString = ["引き分け","勝ち", "負け"]

  const tableStyle = {marginTop: 20, borderCollapse: "collapse"}
  const thStyle = {border: "solid 1px #888", padding: "3px 15px"}
  const tdStyle = {border: "solid 1px #888", padding: "3px 15px", textAlign: "center"}
  return (
    <table style={tableStyle}>
      <thead>
        <tr>
          <th style={thStyle}>あなた</th>
          <th style={thStyle}>コンピュター</th>
          <th style={thStyle}>勝敗</th>
        </tr>
      </thead>
      <tbody>
        {scrores.map((scrore, ix) =>
          <tr key={ix}>
            <td style={tdStyle}>{teString[scrore.human]}</td>
            <td style={tdStyle}>{teString[scrore.computer]}</td>
            <td style={tdStyle}>{judgmentString[scrore.judgment]}</td>
          </tr>
        )}
      </tbody>
    </table>
  )
}
ScoreBox.propTypes = {
  scrores: PropTypes.arrayOf(PropTypes.shape({
    human: PropTypes.number,
    computer: PropTypes.number,
    judgment: PropTypes.number
  })).isRequired
}

export default ScoreBox

//  index.js
import React from 'react'
import ReactDOM from 'react-dom'
import Jyanken from './Jyanken'

ReactDOM.render(
  <React.StrictMode>
    <Jyanken />
  </React.StrictMode>,
  document.getElementById('root')
)
  • Vue
//  Jyanken.vue
<template>
  <div>
    <h1>じゃんけん ポン!</h1>
    <JyankenBox v-on:clicked="pon($event)"/>
    <ScoreBox v-bind:scores="scores"/>
  </div>
</template>

<script>
import JyankenBox from './JyankenBox.vue'
import ScoreBox from './ScoreBox.vue'

export default {
  name: 'Jyanken',
  components: {
    JyankenBox,
    ScoreBox
  },
  data() {
    return {
      scores: []
    }
  },
  methods: {
    pon: function(human) {
      const computer = Math.floor(Math.random() * 3)
      const judgment = (computer - human + 3) % 3
      const score = {human: human, computer: computer, judgment: judgment}
      this.scores = [score, ...this.scores]
    }
  }
}
</script>

<style>
</style>

//  JyankenBox.vue
<template>
  <div>
    <button v-on:click="$emit('clicked', 0)">グー</button>
    <button v-on:click="$emit('clicked', 1)">チョキ</button>
    <button v-on:click="$emit('clicked', 2)">パー</button>
  </div>
</template>

<script>
export default {
  name: 'JyankenBox'
}
</script>

<style scoped>
div {
  margin: 0 20px;
}
button {
  margin: 0 10px;
  padding: 3px 10px;
  font-size: 14px;
}
</style>

//  ScoreBox.vue
<template>
  <table>
    <thead>
      <th>あなた</th>
      <th>コンピュター</th>
      <th>勝敗</th>
    </thead>
    <tbody>
      <tr v-for="(scrore, index) in scores" :key="index">
      <td>{{ teString[scrore.human] }}</td>
      <td>{{ teString[scrore.computer] }}</td>
      <td>{{ judgmentString[scrore.judgment] }}</td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'ScoreBox',
  props: {
    scores: Array
  },
  data() {
    return {
      teString: ["グー","チョキ", "パー"],
      judgmentString: ["引き分け","勝ち", "負け"]
    }
  }
}
</script>

<style scoped>
table {
  margin-top: 20px;
  border-collapse: collapse;
}
td, th {
  border: solid 1px #888;
  padding: 3px 15px;
  text-align: center;
}
</style>

//  main.js
import Vue from 'vue'
import Jyanken from './Jyanken.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(Jyanken),
}

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ