4
3

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

Vue.js童貞がネコ本読んで得たもの③

Last updated at Posted at 2018-11-11

はじめに

本稿はフロントエンド領域から逃げてきたバックエンドエンジニアが追い詰められて0から勉強した軌跡である。詳細は前回をご参照ください^^

ネコ本とは?

みんな大好きMio様の名著「基礎から学ぶ Vue.js」です。
https://www.amazon.co.jp/dp/B07D9BYHMZ

童貞卒業までの道程

  • (済)基本
  • (済)データバインディング、条件分岐、繰り返し
  • (済)ハンドラ、双方向バインディング
  • (済)算出プロパティ
  • ウォッチャ、フィルタ
  • コンポーネント ←今日はここまで
  • VueCLI、単一ファイルコンポーネント
  • Vuex
  • Vue Router

ウォッチャ

ウォッチャとは、何かしらデータ、ないし算出プロパティの変更を監視、変更をフックに処理を実行するための仕組みを指す。

双方向バインディングしているリアクティブなデータが入力フォームで変更されたりとかですね。

watchディレクティブに以下のように記載します。

<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      topics : <select v-model="current">
        <option v-for="topic in topics" v-bind:value="topic.value">
          {{topic.name}}
        </option>
      </select>
      <div v-for="item in list">{{item.full_name}}</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>

new Vue({
  el: "#app",
  data: {
    list: [],
    current: "",
    topics: [
      {value: 'vue', name: 'Vue.js'},
      {value: 'jQuery', name: 'jQuery'}
    ],
  },
  watch: {
    current: function(val) {
      axios.get('https://api.github.com/search/repositories',{
          params: {
            q: 'topic:'+this.val,
            sort: this.sort
          }
        }).then(function(response) {
          this.list = response.data.items
        }.bind(this))
    }
  }
})

上記はプルダウンの値を変更すると、githubのAPIに指定したトピックのリポジトリを取得し、一覧で表示します。

はい。ソートする軸を指定したいですね。
トピックを指定するだけだと、githubはデフォルトで関連度の高い順に表示してくれますが、検索リテラシーが低い人は、関連度というよりもstarやforkが多いものを検索した方が良さそうです。

そのためにはトピックの選択だけでなく、そーとの軸も指定できなければなりません。
更にいうと、複数の入力フォームの変化を監視して処理を実行しなければなりません。

そんな時は、算出プロパティを使って複数の値を監視しましょう。

<!<!DOCTYPE html>
<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      topics : <select v-model="current">
        <option v-for="topic in topics" v-bind:value="topic.value">
          {{topic.name}}
        </option>
      </select>
      sort : <select v-model="sort">
        <option v-for="sort_item in sort_items" v-bind:value="sort_item.value">
          {{sort_item.name}}
        </option>
      </select>
      <div v-for="item in list">{{item.full_name}}</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="main.js"></script>
  </body>
</html>
new Vue({
  el: "#app",
  data: {
    list: [],
    current: "",
    sort: "",
    topics: [
      {value: 'vue', name: 'Vue.js'},
      {value: 'jQuery', name: 'jQuery'}
    ],
    sort_items: [
      {value: 'stars', name: 'stars'},
      {value: 'forks', name: 'forks'},
      {value: 'updated', name: 'updated'}
    ]
  },
  computed: {
    watchTarget: function(){
      return [this.current, this.sort]
    }
  },
  watch: {
    watchTarget: function(val) {
      axios.get('https://api.github.com/search/repositories',{
          params: {
            q: 'topic:'+this.val,
            sort: this.sort
          }
        }).then(function(response) {
          this.list = response.data.items
        }.bind(this))
    }
  }
})

これで、更新日、Star、forkでソートすることができますね。検索リテラシーの低い人も一安心です^^

フィルタ

フィルタは文字列や数字を加工するテキストベースの処理を行う仕組みです。
末尾に「円」をつけたり、数字を桁区切りにしたり、、といった処理をまとめて置くことができます。

<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      <p>{{price}}</p>
      <p>{{price | separator}}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="main.js"></script>
  </body>
</html>
new Vue({
  el: "#app",
  data: {
    price: 19800
  },
  filters: {
    separator: function(val) {
      return val.toLocaleString()
    }
  }
})

これで、桁区切りをseparatorというフィルタにまとめることができます。

ただ、接頭辞に「¥」とつけられていないですね。場合によっては「$」となる可能性もあるので、separatorのフィルタとは別に独立して定義・利用したいです。
フィルタは引数を渡すことも、連続して繋げることもできます。

<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      <p>{{price | separator | prefix('ja')}}</p>
      <p>{{price | separator | prefix('us')}}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="main.js"></script>
  </body>
</html>
new Vue({
  el: "#app",
  data: {
    price: 19800
  },
  filters: {
    separator: function(val) {
      return val.toLocaleString()
    },
    prefix: function(val,locale){
      switch(locale) {
        case 'ja':
          return "¥"+val
        case 'us':
          return '$'+val
        default:
          return val
      }
    }
  }
})

これで一安心ですね^^

コンポーネント

コンポーネントとは、振る舞いやデータだけでなく、描画するUIやデザインも含めて1まとまりに切り出すことができる仕組みですね。
コンポーネントごとに、UIと振る舞いとデータを独立して書くことができるので、再利用したり、保守時の影響範囲を限定したりと、とても素敵な機能です。やっとフレームワークっぽくなってきましたね^^

コンポーネントは以下のように定義、利用します。

<!DOCTYPE html>
<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      <my-component></my-component>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="main.js"></script>
  </body>
</html>
Vue.component('my-component', {
  template: '<p>Hello World!</p>'
})

new Vue({
  el: '#app'
})

Vue.componentでグローバルにコンポーネント「my-component」を登録することで、<my-component></my-component>と書くだけでコンポーネントが呼び出されました。

グローバルで定義すると、大規模になってくると影響範囲が怖いので、ローカルに登録したいですね。
そんな時は以下のように記載します。

var myComponent = {
  template: '<p>Hello World!!</p>'
}

new Vue({
  el: '#app',
  components:{
    'my-component': myComponent
  }
})

また、テンプレートは単一のルート要素が持つように書かなければなりません。

// これはダメで
<p>Hello World!!</p><p>Hello Vue.js!!</p>
// これは良い
<div><p>Hello World!!</p><p>Hello Vue.js!!</p></div>

グローバルで定義した場合、Vue.componentの中のdataオプションの書き方はオブジェクトを返す関数でなければならない。

Vue.component('my-component', {
  template: '<p>{{message}}</p>',
  data: function(){
    return {
      message: 'Hello Vue.js!'
    }
  }
})

new Vue({
  el: '#app'
})

上記のdataの書き方が今までと少し違いますね。

コンポーネント間の通信(親→子:props down)

コンポーネントの中でコンポーネントを利用する状況が発生しますね。
そんな時、コンポーネント同士は親-子の関係となりますが、親で定義したリアクティブなデータは親コンポーネントのスコープなので、子のコンポーネントでそのまま利用することができません。

困りました。そんな時はprops downして、リアクティブなデータを子コンポーネントに渡してあげましょう。

<!DOCTYPE html>
<html lang="ja">
  <head>
  </head>
  <body>
    <div id="app">
      <parent-component></parent-component>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script>
    <script src="main.js"></script>
  </body>
</html>
Vue.component('child-component', {
  template: '<p>{{val}}</p>',
  props: ['val']
})

Vue.component('parent-component', {
  template: '<child-component v-bind:val="message"></child-component>',
  data: function(){
    return {
      message: 'Hello Vue.js!'
    }
  }
})

new Vue({
  el: '#app'
})

上記は親コンポーネントparent-componentで定義したリアクティブなデータを子コンポーネントchild-componentのテンプレートで描画していますね。

親はテンプレートの中でv-bind:val=と、属性を使ってデータを子コンポーネントに渡しています。
子コンポーネントはprops: ['val']props属性で親から渡されたデータを使える状態にしていますね。

注意点としては、渡されたデータはリアクティブなデータなので、親側の変更を子側に伝えることはそのままできますが、子側でデータを勝手に書き換えてはいけません。
後述する子→親へのデータの受け渡しをするか、算出プロパティを使いましょう。

また、子は親の渡すデータを何も確認せずに受けとって良いのでしょうか?そんな綺麗な親子関係が成り立っていれば世界はもっと幸せです。

子コンポーネントは渡されたデータのチェックを行うことができます。

Vue.component('child-component', {
  template: '<p>{{val}}</p>',
  props: {
    val: {
      type: [String,Number],
      required: true
    }
  }
})

上記は、データ型と必須チェックをしています。これで一安心ですね^^

コンポーネント間の通信(子→親:event up)

子で発生したイベントを元に親でアクションを実行して欲しいですよね。
また、親から渡されるデータを直接変更することができないことは前述していますが、親に変更を依頼することはできます。

そんな時は、カスタムイベントと、インスタンスメソッド$emitを使いましょう。
カスタムイベントはv-on:clickのように、v-on:〇〇と親側では記述します。簡単ですね。
$emitは直訳では「発する」「放射する」という意味で、イベントを明示的に発火させるためのメソッドのようです。よくわからないので実際に書いてみましょう。

Vue.component('child-component', {
  template: `<li>{{name}} HP. {{hp}}
    <button v-on:click="doAttack">攻撃する</button></li>
  `,
  props: {
    id: Number,
    name: String,
    hp: Number
  },
  methods: {
    doAttack: function(){
      this.$emit('attack', this.id)
    }
  }
})

Vue.component('parent-component', {
  template: '<ul><child-component v-for="item in list" v-bind:key="item.id" v-bind="item" v-on:attack="handleAttack"></child-component></ul>',
  data: function(){
    return {
      list: [
        {id: 1, name: '田中', hp: 1000},
        {id: 2, name: '佐藤', hp: 2800},
        {id: 3, name: '山田', hp: 3500}
      ]
    }
  },
  methods: {
    handleAttack: function(id){
      var item = this.list.find(function(el){
        return el.id === id
      })
      if (item !== undefined && item.hp > 0) item.hp -= 100
    }
  }
})

new Vue({
  el: '#app'
})

山田に攻撃したくなりますね。

子コンポーネントから親コンポーネントへの受け渡しは、
子コンポーネントで実行されたイベントが実行するmethods$emitメソッドを実行し、$emitメソッドは親のイベントを発火させ、発火したイベントは親コンポーネントの中で記載されたメソッドを実行し、そのメソッド内で値を変更する。

とった流れです。わかりにくいですね。

ただ、親の中で定義したデータをみだりに子で操作させないためにもイベントを経由するという方法は確かに安心安全な気がします。

終わりに

コンポーネント間のデータ通信は理解するのに予想以上にパワーがかかりました(´ཀ`」 ∠)_

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?