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 3 years have passed since last update.

Vue.jsのstateの動きをCDN版で確認する

Posted at

概要

Vue.js の state の同期・非同期の更新をミニマムコードで確認したくなったので、サンプルコードを作ってみた。

補足
良くも悪くWebサーバなしのhtml/JavaScriptをそのまま動かすのでシンプルですが、importやcorsなどの制約があります。なので、モジュール分割や単一ファイルコンポーネントなど構成は異なります。

シンプルな例

表示例
ボタンを押すと2づつ増えます。

image.png

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 に対して変更・参照を行う構造になっています。

image.png

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 で増分する数値を -12 とで変えています。

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秒後に配列に追加します。

image.png

ボタンを押して、格納されるまでのタイムラグが分るようにタイムラインで表示しています。

image.png

10個以上配列に追加すると例外を出すようにしています。
Store上の変数を参照し、もう食べられないことを表現しています。
絵文字のビールは日本酒にしようか悩みましたがが、寿司ビール問題ということで、ビールにしました。

image.png

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>
  `
})
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?