わけあって連休中にVue.jsを学んでみました。私は作りながら学ぶ React入門という本を書いていますが、実務でも2015年からReactを使っています。Reactはこの5年間どんどん改良され使いやすくなって来たので、今までVue.jsを学ぶ機会は訪れませんでしたが、今回わけあって学んでみました。
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対応が良くなるようで期待大です - 下のように開発コードが、アニメーション名なのが楽しい
Version | Title |
---|---|
2.6 | Sword Art Online : Alicization |
2.5 | Level E |
2.4 | Kill la Kill |
2.3 | JoJo’s Bizarre Adventure |
2.2 | Initial D |
2.1 | Hunter X Hunter |
2.0 | Ghost in the Shell |
1.0 | Evangelion |
0.12 | Dragon Ball |
0.11 | Cowboy Bebop |
0.10 | Blade Runner |
0.9 | Animatrix |
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),
}