はじめに
社内発表用として作成中の資料です。
(4/14発表しました。)
Vue.jsとは
[1.x]
・ MVVMという設計思想を用いている
・ 同じMVVMのKnockout.jsより、分かりやすくてシンプルなAPIを提供しているらしい
・ Vue.jsのいくつかのAPIはAngularJSから影響を受けているらしい(ただし設計思想は全く異なる)
・ AngularJSの経験者はVue.jsのディレクティブを見ると、どうゆうものかすぐ分かると思います。
[2.x]
・ 2016年10月に正式版リリースされた新しいライブラリ
・ コンポーネント指向を取り入れている
・ 仮想DOMを採用
・ React.jsとAngularの良い所を採用して作られたようなライブラリ
コンポーネント指向
Vue.jsはReactなどで採用されているコンポーネント指向を取り入れている。
MVVMはModel, View, ViewModelに分けて、モジュールを開発していたのに対して、
コンポーネント指向ではDOM(HTML, JS, CSSを一纏めにした)単位でモジュールを開発する。
例えば、以下のようにVueファイルを実装する。
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'hello',
data () {
return {
msg: 'message!!'
}
}
}
</script>
<style>
h1 {
font-weight: normal;
}
</style>
コンポーネント間のデータの受け渡しは、親コンポーネントから子コンポーネントへはprops経由で渡し、
子から親へはイベントを発火させることで連携する。
この辺を次ページで詳細に書きます。
親子コンポーネント間のデータバインディングについて
・ 単方向のデータバインディング
・ 親コンポーネントの状態を更新した時に、子コンポーネントへ伝えて反映させる。
・ 子から親への状態更新は不可(Warningが出る)。
以下のように書くことで、親の変更を子へ反映させることができる。
<child v-bind:msg="parentMsg"></child>
export default {
data: function () {
return {
parentMsg: "parent message!!!"
}
}
}
↓ v-bindは省略可。
<child :msg="parentMsg"></child>
子コンポーネントでは、propsを介して値を受け取る。
型制限や必須チェックなども記載できて、これに反したデータを受け取るとWarningを出す。
export default {
props: {
msg: {
type: String,
required: true,
default: "default message",
validator: function (value) {
return value.length > 1
}
}
}
}
双方向データバインディング
データを更新したらUIにも反映 + UIを変更したらデータにも反映。
<input type="text" v-model="parentMsg">
new Vue({
data: {
parentMsg: 'message!!'
}
})
↑data.parentMsgの「message!!」をテキストボックスに出力し、
その後UI上のテキストボックスが入力されたらdata.parentMsgが更新される。
また、先程の親子関係があるコンポーネント間では使ってはいけません。
以下のように親から受け取ったmsgをv-modelで書いてしまうとWarningがでます。
<input type="text" v-model="msg">
export default {
props: {
msg: {
type: String,
}
}
}
それでも変更したい時は、子コンポーネント内で別のプロパティとして定義する。
<input type="text" v-model="childMsg">
export default {
props: {
msg: {
type: String,
}
},
data: function () {
return {
childMsg: this.msg
}
}
}
単一のコンポーネントでは、双方向のデータバインディングは許されるが、
複数のコンポーネント間(親子関係の)では、単方向のデータバインディングで実装する。
子コンポーネントで起きたイベントを親へ伝える
子で出力している値(テキストボックスの値とかチェックボックスのチェック状態とか)を親で管理している場合、
子で起きたイベントを親へ伝えるような実装にする必要がある。
以下のようにchildMsgとonChangeMsgを子へpropsで渡す。
onChangeMsgはコールバック関数として渡せるので、イベント発生時に親へ通知できる。
<child :childMsg="childMsg" :onChangeMsg="onChangeMsg"></child>
export default {
data: function () {
return {
childMsg: "child message!!!"
}
},
methods: {
onChangeMsg: function (msg) {
this.childMsg = msg
}
}
}
子ではprops経由で受け取ったonChangeMsgをv-onを使って書くことで、
イベント発生時に親へ伝えることができる。
<input type="text" v-on="change:onChangeMsg">{{ childMsg }}</input>
export default {
props: {
childMsg: {
type: String,
},
onChangeMsg: {
type: Function,
}
}
}
↓ v-onは省略可。
<input type="text" @change="onChangeMsg">{{ childMsg }}</input>
親子関係まとめ
先程のことを意識して実装すると、再利用性が高いコンポーネントを作りやすくなる。
また大規模アプリケーションを検討する際に、Fluxのようなアーキテクチャの導入・移行がかなり楽になる。
Fluxの導入について
Viewから状態管理に関わる処理を分離する為に導入する。
中・大規模なアプリケーションになってくると、親コンポーネントが肥大化してくるので、
状態管理に関わる処理の部分は、Flux系のフレームワークに任せて、
親コンポーネントは状態変更させるアクションと変更後の状態を受け取り、
子コンポーネントに渡す役割をすることで、Viewの実装をシンプルにしてくれる。
Fluxについて
※画像出典:[UNIDIRECTIONAL USER INTERFACE ARCHITECTURES](http://staltz.com/unidirectional-user-interface-architectures.html)Actions:Viewの入力内容を基にデータを作成
Dispatcher:Actionの命令に合わせて、Storeにデータを送る
Store:データを更新して保管
View:データを表示
Flux導入のメリット
データを更新した際の流れが単方向であり、MVVMフレームワークのような双方向な流れが無いため、分かりやすい
デメリット
Viewでデータを更新する際はActionから実装するため、冗長構成になりやすい
Redux
※画像出典:[UNIDIRECTIONAL USER INTERFACE ARCHITECTURES](http://staltz.com/unidirectional-user-interface-architectures.html)Fluxに以下の制約を加えたもの
・ Single source of truth (真実の単一の源)
⇒システム全体のStateを1つのStoreで管理する。
・ State in read-only (読み込み専用の状態)
⇒StateをViewなどで更新してはいけない。Stateを更新する場合はActionを通じてのみにする。
・ Changes are made with pure functions (変更は純粋な関数で行われます)
⇒ReducerでActionと現在のStateを受け取り、新しいStateを作って返す。その際現在のStateは変更せず、新しいStateを作り出して返す。
Vuex
※画像出典:[Vuex Introduction](https://vuex.vuejs.org/ja/intro.html)Vue.jsの場合は、こちらを使うことが多いかもしれない。
Reduxと考え方が似ており、Reducerの代わりにMutationがその役割を担う。
ReduxはReducerでActionTypeを指定するので、Actionに対して単体もしくは複数のReducerが紐づき、処理する。
VuexはActionでMutationTypeを指定するので、Actionから単体もしくは複数のMutationを呼出し、処理する。
図には無いが、StateからViewの間にGettersというのがVuex内にあり、StateをViewで出力する用に加工したい時などに使える。
ViewでStateを直接受け取って加工すれば、特にGettersは使わなくても良いが、加工したStateを複数コンポーネントで使う場合などは、Gettersで処理しておけば、冗長的に書かずに済む。
CSSModulesの導入について
ある程度システム規模が大きくなると、CSSのグローバルスコープでは影響範囲の把握が辛いものがあるので、
CSSModulesを導入して、コンポーネントごとにローカルスコープ化した。
以下の設定をすることで、実現できる。
<template>
<div :class="$style.sample" />
</template>
<style module>
.sample {
margin-top: 10px;
}
</style>
<style module>
と書くことで、内部的にWebpackのcss-loader
で処理されて変換される。
また、命名規則を変えたいなどの追加オプションは、Webpackのvue-loader
のオプションで指定できる。
module.exports = {
module: {
rules: [
{
test: /\.vue$/,
use: [{
loader: 'vue-loader',
options: {
cssModules: {
localIdentName: '[name]__[local]___[hash:base64:5]',
camelCase: true
}
}
}],
exclude: /node_modules/
}
]
}
}
CSSModulesでは、命名を意識せずにローカルスコープ化できるので、その点おすすめ
BEMなどのCSS設計を導入・浸透させるのに苦戦していたら、CSSModulesを試してみるのも良いかと思う。
ESLintの導入
コードの統一やレビュアーのストレス軽減の為、ESLint(コードの静的検証)を導入している。
module.exports = {
module: {
rules: [
{
test: /\.vue$|\.js$/,
use: 'eslint-loader',
exclude: /node_modules/
},
]
}
}
Vue.jsのLintはeslint-plugin-vue
,eslint-config-vue
を使用した。
"root": true,
"parser": "babel-eslint",
"parserOptions": {
"sourceType": "module"
},
"extends": "vue",
"plugins": [
"html"
]
ESLintではなく、以下のようなコードフォーマッタ的なのもある。
https://github.com/prettier/prettier
コードフォーマッタとLintを両方使うことも可能だが、たぶん競合するので、
prettier-eslint
等を使って、回避する必要がある。
この辺はそのうち検証しようと思ってます。
UnitTestについて
コンポーネントとVuexのAction, Getter, MutationでUnitTestを書いている。
Karma, Mocha, Chaiで構成してて、vue-cli
で使われてるライブラリと一緒です。
(いくつか他のライブラリも検討しましたが、結局これにしました。)
const webpack = require('webpack')
const webpackConfig = require('../../gulp/webpack.test.config')
module.exports = (config) => {
config.set({
frameworks: ['mocha', 'sinon-chai'],
files: [
'../../node_modules/babel-polyfill/dist/polyfill.js',
'../../src/**/*.vue',
'../../src/**/*.js',
{
pattern: 'specs/**/*.js',
watched: false
}
],
preprocessors: {
'../../src/**/*.vue': ['webpack'],
'../../src/**/*.js': ['webpack'],
'specs/**/*.js': ['webpack', 'sourcemap']
},
proxies: {
'/api': 'http://example.aaa.jp:8888/api'
},
reporters: ['spec', 'coverage'],
browsers: ['PhantomJS'],
webpack: webpackConfig,
webpackMiddleware: {
quiet: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'text-summary' },
{ type: 'html' }
]
}
})
}
カバレッジの出力も行っていて、babel-plugin-istanbul
を使うとBabel変換時にカバレッジを計測してくれる。
上記のKarmaの設定に加えて、以下をBabelに設定しておく。
{
"env": {
"test": {
"plugins": ["istanbul"]
}
}
}
番外編1 methodsとcomputedはどちらを使うか
以下はgetValue1もgetValue2も計算結果の2を画面に表示する。
export default {
methods: {
getValue1: function () {
return 1 + 1
}
},
computed: {
getValue2: function () {
return 1 + 1
}
}
}
<div>
{{ getValue1() }}
{{ getValue2 }}
</div>
これらの使い分けとして、
computedはキャッシュが使えるのに対して、methodsはキャッシュせずに毎回計算する。
computedは依存関係にあるプロパティが更新された時のみ、再評価してくれる。
methodsはキャッシュを使いたくない時に使用すれば良い。
また、computedはgetter/setterが書ける。
export default {
data () {
return {
sampleName: 'sample'
}
},
computed: {
getName: {
get: function () {
return this.sampleName
},
set: function (str) {
this.sampleName = str
}
}
}
}
番外編2 テンプレートの書き方について
テンプレート(html)の実装方法は以下の2通りがある。
Vue1系からやっている人は、①の書き方に慣れてる方が多いかもしれません。
Vue.component('my-component', {
template: '<div>A custom component!</div>'
})
<template>
<div>
A custom component!
</div>
</template>
これらの違いは、コンパイルされるタイミングで
①はブラウザで実行前にコンパイルされる(JustInTimeコンパイル)
②はWebpack等で事前にコンパイルされる
①はコンパイラが必要になるので、完全ビルド
②はコンパイラが不要なので、ランタイム限定ビルドを利用できる。
Vueはデフォルトでランタイム限定ビルドとなるので、①のようなtemplateオプションは読み込まれない。
かつ、ランタイム限定ビルドの方が軽いので、特に理由が無ければ②の書き方にしましょう。
やむを得ず①の書き方にしたい場合は、バンドラーでエイリアスを設定する。
↓Webpackでの例
module.exports = {
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
}
番外編3 IEについて
IE以外のブラウザはJSの部分は問題無く動くと思いますが、IEは別途対応が必要。
IE8以下
サポート対象外
IE9以上
babel-polyfillが無いと動かなかった。
Webpackでbabel-polyfillを読み込んであげると動くようになる。
module.exports = {
entry: {
bundle: [
`${__dirname}/../node_modules/babel-polyfill/dist/polyfill.js`,
`${__dirname}/../src/index.js`
]
}
}