概要
Vue.js の state の同期・非同期の更新をミニマムコードで確認したくなったので、サンプルコードを作ってみた。
補足
良くも悪くWebサーバなしのhtml/JavaScriptをそのまま動かすのでシンプルですが、importやcorsなどの制約があります。なので、モジュール分割や単一ファイルコンポーネントなど構成は異なります。
シンプルな例
表示例
ボタンを押すと2づつ増えます。
index.html
Vue.js
および Vuex
のモジュールを CDN で配付されているものを取り込んで使います。このあたり import Vue...
だとかの代わりになるものです。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Counter Store Example</title>
</head>
<body>
<div id="app"></div>
<!-- see: https://github.com/vuejs/vue/releases -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<!-- Vuex -->
<script src="https://unpkg.com/vuex"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
<script src="main.js"></script>
</body>
</html>
main.js
require
だとかの読み込みは動かないのとシンプルになるのを優先して、1ファイルに処理をまとめています。(最小といいつつ、Vue.Store のmutationは引数付きですがご容赦ください)
- state からの読み取りは
counter
コンポーネントの computed から行っています - state の更新は
counter
コンポーネントの methods から Storeのmutation 呼び出します
この例だと同期更新です
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, num) {
state.count += 1
}
}
});
Vue.component('counter', {
props: ['amount'],
template: `
<div>
<button v-on:click="increment(2)">Add 2</button>
<div>{{ count }}</div>
</div>
`,
computed: {
count () {
return this.$store.state.count
}
},
methods: {
increment: function(num) {
store.commit('increment', num)
}
}
})
let app = new Vue({
el: '#app',
store,
template: `
<div>
<counter></counter>
</div>
`
})
複数コンポーネントを使った例
これをやらないと emit
だとかの違いが判ってこないので、やや複雑ですが載せます。
画面例
15 と表示しているテキストエリアと Add -1
ボタンと Add 2
は別コンポーネントです。それぞれが Store に対して変更・参照を行う構造になっています。
index.html
上のと同じ。
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Store Example</title>
</head>
<body>
<div id="app">
<product message="hello"></product>
</div>
<!-- see: https://github.com/vuejs/vue/releases -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<!-- Vuex -->
<script src="https://unpkg.com/vuex"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
<script src="main.js"></script>
</body>
</html>
main.js
大したことはしていません。ボタンを少し変えたかったので prop
で増分する数値を -1
と 2
とで変えています。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment(state, num) {
state.count += num
}
}
});
Vue.component('counter', {
props: ['amount'],
template: `
<div>
<button v-on:click="increment(Number(amount))">Add {{amount}}</button>
</div>
`,
methods: {
increment: function(num) {
store.commit('increment', num)
}
}
})
Vue.component('count-view', {
template: `
<div>
<div>{{ count }}</div>
</div>
`,
computed: {
count () {
return this.$store.state.count
}
}
})
let app = new Vue({
el: '#app',
store,
template: `
<div>
<count-view></count-view>
<div style="display: flex;">
<counter amount="-1"></counter>
<counter amount="2"></counter>
</div>
</div>
`
})
非同期の例
そこそこ複雑な例です。
画面例
2つのボタンは別コンポーネントです。
- 右が寿司を取って3秒後に配列に追加します。
- 左がビールボタンは 0.1秒後に配列に追加します。
ボタンを押して、格納されるまでのタイムラグが分るようにタイムラインで表示しています。
10個以上配列に追加すると例外を出すようにしています。
Store上の変数を参照し、もう食べられないことを表現しています。
絵文字のビールは日本酒にしようか悩みましたがが、寿司ビール問題ということで、ビールにしました。
index.html
ボタンに絵文字を入れたら文字化けするので <meta charset="utf-8"/>
を加えています。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Stomach Service</title>
</head>
<body>
<div id="app"></div>
<!-- see: https://github.com/vuejs/vue/releases -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
<!-- Vuex -->
<script src="https://unpkg.com/vuex"></script>
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
<script src="main.js"></script>
</body>
</html>
main.js
長いと読まないと思うのでポイントだけ書いておきます。
- 非同期処理のなかで寿司がおなか(配列)に入るのは 3秒のラグがありますから、ボタンを押した時点ではまた配列は溢れていません
- そのため、実際に溢れに気づくのは commit の先の mutations の addDish です。ただ、エラー通知は addTimeLine がmutations 中で呼べなかったので例外を出して action 関数に制御を戻しています。これはこれで良いかなと思います。
const store = new Vuex.Store({
state: {
dishes: [],
timeLine: [],
error: {
title: 'It\'s OK.',
isOverFlow: false,
},
messages: []
},
mutations: {
addDish(state, dish) {
if (state.dishes.length < 10) {
state.dishes.push(dish)
return
}
if (!state.error.isOverFlow) {
state.error.isOverFlow = true
state.error.title = '503 Stomach Service Temporarily Unavailable'
const errorObj = {
message: `stomach overflow by ${dish}`,
severity: 'ERROR',
color: 'red',
}
throw errorObj
} else {
const errorObj = {
message: `I refused to enter the ${dish} room. (もう入りません)`,
severity: 'ERROR',
color: 'red',
}
throw errorObj
}
},
addTimeLine(state, newItem) {
if( state.timeLine.length > 8 ) {
state.timeLine.shift()
}
state.timeLine.push({
timestamp: (new Date()).toISOString(),
message: newItem.message,
severity: newItem.severity,
color: 'black',
})
}
},
actions: {
orderDispatchAsync ({ commit }, param) {
// console.log(`orderDispatchAsync: dish=${param.dish} waitMsec=${param.waitMsec}`);
commit(
'addTimeLine',
{
message: `you picked up ${param.dish}.`,
severity: 'INFO',
color: 'black'
}
)
// asynchronous callback
setTimeout(() => {
try {
commit('addDish', param.dish)
commit(
'addTimeLine',
{
message: `${param.dish} entered the stomach.`,
severity: 'INFO'
}
)
} catch (error) {
commit('addTimeLine', error)
}
}, param.waitMsec)
}
}
});
Vue.component('hand', {
props: {
dish: { type: String, required: true, },
waitMsec: { type: Number, required: false, default: 100, },
},
template: `
<div>
<button v-on:click="orderDish(dish, waitMsec)"
:disabled="error.isOverFlow"
>
Pick {{dish}} (Wait: {{waitMsec}}[msec])
</button>
</div>
`,
methods: {
orderDish: function(dish, waitMsec) {
// console.log(`orderDish: dish=${dish} waitMsec=${waitMsec}`);
const param = {
dish, waitMsec
}
store.dispatch('orderDispatchAsync', param)
}
},
computed: {
error () {
return this.$store.state.error
}
}
})
Vue.component('stomachMonitor', {
template: `
<div>
<div>{{ error.title }}</div>
<div>{{ error.message }}</div>
<div v-if="error.isOverFlow" style="color: red;">
Fatal: Stomach digestion is not implemented. Please reload. (胃の消化は未実装です。)
</div>
<div>Current: {{dishes.length}}</div>
<span v-for="dish in dishes">{{ dish }}</span>
<h3>Timeline</h3>
<table>
<tr v-for="(item, index) in timeLine" >
<td>{{ item.timestamp }}</td>
<td>
<span v-if="item.severity === 'ERROR'" style="color: red;">{{ item.message }}</span>
<span v-else>{{ item.message }}</span>
</td>
</tr>
</table>
</div>
`,
computed: {
dishes () {
return this.$store.state.dishes
},
error () {
return this.$store.state.error
},
timeLine () {
return this.$store.state.timeLine
}
},
data: function() {
return {
styles: {
color: 'red'
}
}
}
})
let app = new Vue({
el: '#app',
store,
template: `
<div>
<div style="display: flex;">
<hand dish="🍣" :waitMsec="3000"></hand>
<hand dish="🍺" :waitMsec="100"></hand>
</div>
<stomachMonitor></stomachMonitor>
</div>
`
})