ryota21silvaの技術ブログ

Funna(ふんな)の技術ブログ

これまで学んだ技術の備忘録。未来の自分が救われることを信じて

【超Vue.js】コンポーネント間でデータを受け渡す方法【セクション7 】

87. 親子間のデータの受け渡しの必要性を理解する

コンポーネント

アプリケーションのApp.vueは親コンポーネントLikeNumberLikeHeaderは子コンポーネント

→App.vueではLikeNumber、LikeHeaderを使っている。

(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{{ number }}</h2>
  <LikeNumber :total-number="number" @my-click="incrementNumber"></LikeNumber>
  <LikeNumber :total-number="number"></LikeNumber>
</div>
</template>

なぜコンポーネント間でデータの受け渡しを行うの?そのコンポーネント自身がdataを持てばいいのでは?という疑問を解消

ex.)全てのブログの内容は日々更新されるモノで、DBから取ってくるもの。

createdでサーバーから取ってきたデータはApp.vueにある
コンポーネントApp.vueの中で分かれているので、LikeHeaderコンポーネントに「ブログのタイトルはこれですよ」と渡してあげることになる。

88. propsを使用して親から子にデータを渡そう

コンポーネントでpropsを定義する。

→配列で書く。使いたいプロパティ名を書く

(src/components/LikeNumber.vue)
<template>
  <div>
    <!-- 受け取ったデータを使用している  -->
    <p>いいね({{ number / 2 }})</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  // データの受け口。プロパティ名を指定
  props: ["number"],
  methods: {
    increment() {
      this.number += 1;
    }
  }
};
</script>

親では属性のように書く

以下は静的なデータの受け渡し。

(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{{ number }}</h2>
   <!-- データの渡し口。属性っぽい書き方 -->
  <LikeNumber number="6"></LikeNumber>
  <LikeNumber number="6"></LikeNumber>
</div>
</template>

動的なデータを渡すにはv-bindを使う。

(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{{ number }}</h2>
  <!-- データの渡し口にv-bindを使う -->
  <LikeNumber :number="number"></LikeNumber>
  <LikeNumber :number="number"></LikeNumber>
</div>
</template>

<script>
import LikeHeader from "./components/LikeHeader.vue";

export default {
  data() {
    return {
      number: 10
    };
  },
  components: {
    LikeHeader: LikeHeader
  }
}
</script>

89. propsの名前はJavaScriptではキャメルケース、HTML内ではケバブケースにすることをお勧めします

propsの書き方...キャメルとケバブがある。

→子ではキャメル。親ではケバブを推奨(html文だから)

(src/components/LikeNumber.vue)
<template>
  <div>
    <p>いいね({<200c>{ totalNumber / 2 }})</p>
    <button @click="increment">+1</button>
  </div>
</template>
 
<script>
export default {
  // キャメルケース
  props: ["totalNumber"],
  methods: {
    increment() {
      this.number += 1;
    }
  }
};
</script>
(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{<200c>{ number }}</h2>
 <!-- キャメルでもケバブでもいける。ケバブを推奨 -->
  <LikeNumber :totalNumber="number"></LikeNumber>
  <LikeNumber :total-number="number"></LikeNumber>
</div>
</template>

90. コンポーネント内でpropsを扱う時はdataのように使用する

script内で使う

コンポーネント内でpropsで受け取ったデータは、普通のdataのアクセスと同じようにthisでアクセスできる。

(src/components/LikeNumber.vue)
<template>
  <div>
    <p>いいね({<200c>{ halfNumber / 2 }})</p>
    <button @click="increment">+1</button>
  </div>
</template>
 
<script>
export default {
  props: ["totalNumber"],
  computed: {
    // ES6の関数の書き方
    halfNumber() {
      //propsで受け取ったデータに、 thisでコンポーネント内からアクセスしている
      return this.totalNumber
    }
  },
  methods: {
    increment() {
      this.number += 1;
    }
  }
};
</script>

91. バリデーションを使用して、プロパティに意図しない値が渡されるのを防ぐ

オブジェクトを扱える

propsで指定した型と、親から受け取ったデータの型が違うと、エラーが出る。

(src/components/LikeNumber.vue)
<script>
export default {
  props: {
    // 右辺に型を指定
    totalNumber: Number
  },

[Vue warn]: Invalid prop: type check failed for prop "totalNumber". Expected String with value "10", got Number with value 10.

(src/components/LikeNumber.vue)
<script>
export default {
  props: {
    // 右辺に型を指定
    totalNumber: {
      type: Number,
      required: true
    }
  },
 

(src/App.vue)
<LikeNumber></LikeNumber>

[Vue warn]: Missing required prop: "totalNumber"

defaultは配列、オブジェクトの型などは関数にして初期値を設定する。

(src/components/LikeNumber.vue)
<script>
export default {
  props: {
    // 右辺に型を指定
    totalNumber: {
      type: Number,
      default: 7
    }
  },

92. 複数のpropsをつける方法

複数のpropsを指定

(src/components/LikeNumber.vue)
<script>
export default {
  props: {
    // totalNUmberを受け取る
    totalNumber: {
      type: Number,
      default: 30
    },
    // testPropsを受け取る
    testProps: {
      type: String
    }
  },
(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{<200c>{ number }}</h2>
  <LikeNumber></LikeNumber>
  <!-- 渡し口を2つ書いている -->
  <LikeNumber :total-number="number" :test_props="test"></LikeNumber>
</div>
</template>

<script>
import LikeHeader from "./components/LikeHeader.vue";

export default {
  data() {
    return {
      number: 14
    };
  },
  components: {
    LikeHeader: LikeHeader
  },
  methods: {
    incrementNumber(value) {
      this.number = value
    }
  }
}
</script>

配列でも複数のpropsを指定できる

(src/components/LikeNumber.vue)
<script>
export default {
  // 配列で複数のpropsを指定
  props: ["totalNumber", "testProps"],
  computed: {
    halfNumber() {
      return this.totalNumber
    }
  },
  methods: {
    increment() {
      this.number += 1;
    }
  }
};
</script>

93. $emitメソッドを使って子から親にデータを渡す(厳密ではない)

propsのおさらい

コンポーネントpropsというdataの受け口があり、親コンポーネント<LikeNumber :total-number="number"></LikeNumber>という渡し口があった。

$emit

子から親は$emitを使う。
イベントを発生させてdataの受け渡しを行う

インスタンス内だからthisを使って、this.$emitとする。

$emitの引数

  1. データの名前(カスタムイベント名)。何でもいい
  2. データ自身をいれる(コンポーネント内でthisでアクセス)

コンポーネント

clickした瞬間に$emitが発火して、dataが親に渡ってnumberに代入する

(src/components/LikeNumber.vue)
<template>
  <div>
    <p>いいね({{ halfNumber }})</p>
    <!-- ①クリックした瞬間にincrementメソッドの$emitが発火する --> 
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  // 親からデータを受け取る
  props: ["totalNumber"],
  computed: {
    halfNumber() {
      return this.totalNumber / 2 ;
    }
  },
  methods: {
    increment() {
      // 子に$emitを書く
      // 第1引数にデータの名前(カスタムイベント名)、第2引数にデータ自身(ここではtotalNumberに+1している)をいれる
      // ②$emitが発火することで、親コンポーネントに、my-clickイベントが発火するという情報を渡す
      this.$emit('my-click', this.totalNumber + 1);
    }
  }
};
</script>

コンポーネント

まずv-onを書く。v-onの引数に自分で作ったイベント名を入れる。
js式を書けるので、$eventでデータを受け取る。
(numberという)変数にdataを入れる。

(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{{ number }}</h2>
  <!-- my-clickイベントが発火 -->
  <!-- ③$eventでデータを受け取り、numberという変数にdataを入れる -->
  <LikeNumber :total-number="number" v-on:my-click="number = $event"></LikeNumber>
  <LikeNumber :total-number="number"></LikeNumber>
</div>
</template>

<script>
import LikeHeader from "./components/LikeHeader.vue";

export default {
  data() {
    return {
      number: 14
    };
  },
  components: {
    LikeHeader: LikeHeader
  }
}
</script>

軽くまとめると...

$emitは子コンポーネントの好きなタイミングで、親コンポーネントのメソッドを発火できる。
親のメソッドを発火させる起動スイッチみたいなもので、それに便乗して第二引数でdataを渡していただけ。
$emitはカスタムイベントを作るモノといえる。

<LikeNumber :total-number="number" @abort="number = $event"></LikeNumber>

メソッドで書きたい場合

value部分にemitの第二引数のデータが入る

(src/components/LikeNumber.vue)
<LikeNumber :total-number="number" @my-click="incrementNumber"></LikeNumber>

<script>
import LikeHeader from "./components/LikeHeader.vue";

export default {
  data() {
    return {
      number: 14
    };
  },
  components: {
    LikeHeader: LikeHeader
  },
  methods: {
    // value部分にemitの第二引数が入る
    incrementNumber(value) {
      this.number = value
    }
  }
}
</script>

子供が親のdataを変えることはできない(親が子のdataに依存することはないから)

94. なぜ$emitはpropsと違ってこんなに複雑なのか

$emitという送り口から
this.totalNumber + 1の結果を
my-clickという名前で
送る

以下は子コンポーネントの中では値が変わってるけど、親の値は変わっていない

以下のemitを使わない書き方だと、親のdataは変わらない。具体的には子のカウントボタンをクリックして、子コンポーネントのデータは増加しているのに、親コンポーネントのデータは増加しない。

→なぜなら、子から親のdataは変えられないから!
→子から親のデータを変えられるとアプリケーションとして挙動が読みにくくなる。こうやって制限するからデータフローがわかりやすくなる。
$emitを使って、dataの値を上手く調理するのは親の仕事!

(src/components/LikeNumber.vue)
<template>
  <div>
    <p>いいね({{ halfNumber }})</p>
    <button @click="increment">+1</button>
  </div>
</template>
<script>
export default {
  props: ["totalNumber"],
  computed: {
    halfNumber() {
      return this.totalNumber / 2 ;
    }
  },
  methods: {
    increment() {
      // 以下のemitを使わない書き方だと、親のdataは変わらない(子のdataだけ変わる)
      // 子から親のdataは変えられないから
      this.totalNumber += 1;
      // 一旦$emitをコメントアウト
      // this.$emit('my-click', this.totalNumber + 1);
    }
  }
};
</script>
(src/App.vue)
<template>
<div>
  <LikeHeader></LikeHeader>
  <h2>{{ number }}</h2>
  <LikeNumber :total-number="number" @my-click="incrementNumber"></LikeNumber>
  <LikeNumber :total-number="number"></LikeNumber>
</div>
</template>
<script>
import LikeHeader from "./components/LikeHeader.vue";
export default {
  data() {
    return {
      number: 14
    };
  },
  components: {
    LikeHeader: LikeHeader
  },
  methods: {
    incrementNumber(value) {
      this.number = value
    }
  }
}
</script>

95. propsで配列とオブジェクトを渡す時は、参照渡しになるので注意する

propsは親のデータに依存して、子の値がぽんぽん変わる。

→親のデータが変わるだけで、子のデータも変わる

配列とオブジェクトとなっているdataをpropsで受け取った場合、その値を書き換える場合は、親の値も変わる。
→これは参照渡し(同じメモリから参照している)がされており、親も子もおなじオブジェクトを参照しているから。 props: ["totalNumber"]のtotalNumberの値がオブジェクトの時などのこと。

96. $emitで作るカスタムイベント名はケバブケース(kebab-case)にする

カスタムイベント名はケバブケース推奨

this.$emit('my-click', this.totalNumber + 1); ブラウザはhtmlを読んでからjsを読みこむ
→htmlは大文字と小文字の区別をしない
→カスタムイベントをjsで使うことは絶対ない。あくまで以下のようなhtmlで使われる

<LikeNumber :total-number="number" @my-click="incrementNumber"></LikeNumber>

→じゃあ、html推奨のケバブケースにしようぜ。

【超Vue.js】ゼロから始めるコンポーネント【セクション6 】

74. コンポーネントを使用して、再利用可能なVueインスタンスを作る

コンポーネントの中でデータを使う時は、データを関数にする必要がある。

以下はグローバルコンポーネント

Vue.component('my-component', {
  data: function(){
    return {
        number: 12
    }
  },
  template: '<p>いいね{<200c>{number}}</p>'
})
 
new Vue({
    el: '#app',
})

75 dataはなぜコンポーネントにおいて関数である必要があるのか?

なぜ、コンポーネント内では、dataを関数にする必要があるの?

コンポーネント内のdataをオブジェクトにしてしまうと、そのコンポーネントを使用している3つのビューにおいて、オブジェクトのデータが参照によって共有されてしまう
つまり、同じデータを共有するということ(=一つのメモリに共有するということ)

例えば、一つのmy-componentをいじると、他のmy-componentも変わってしまう。

ex.) 以下のようにコンポーネントのdataをオブジェクトにした場合、ボタンを押すとmy-component全部の数字の値が増えてしまう。

<div id="app">
  <my-component></my-component>
  <my-component></my-component>
  <my-component></my-component>
</div>

var data = {
number: 12
}

Vue.component('my-component', {
data: function(){
    return data
  },
  template: '<p>いいね{{number}}<button @click="increment">+1</button></p>',
  methods: {
  increment: function() {
    this.number += 1;
  }
  }
})

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

関数にすると、thisを使えば、押したmy-componentの値だけが増える。

Vue.component('my-component', {
data: function(){
    return {
    number: 12
    }
  },
  template: '<p>いいね{{number}}<button @click="increment">+1</button></p>',
  methods: {
  increment: function() {
    this.number += 1;
  }
  }
})

76. コンポーネントにおける、ローカル登録とグローバル登録の違いを理解する

グローバル登録

// コンポーネントをVueインスタンスの外側に置いてる。
Vue.component('my-component', {
data: function(){
    return {
    number: 12
    }
  },
  template: '<p>いいね{{number}}<button @click="increment">+1</button></p>',
  methods: {
  increment: function() {
    this.number += 1;
  }
  }
})

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

new Vue({
el: '#app2',
})

ローカル...特定のVueインスタンスでのみ使用できる

書き方
  • 変数を作ってその中にグローバルコンポーネントの中で書いたようなオブジェクトを書く。

  • インスタンスの中でcomponentオプションを書く

  • キーに反映させたいhtmlの記述、バリューに変数名

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<div id="app">
  <my-component></my-component>
  <my-component></my-component>
  <my-component></my-component>
  <hr>
</div>

<div id="app2">
  <my-component></my-component>
  <my-component></my-component>
  <my-component></my-component>
</div>

// 変数の設定
var component = {
  data: function(){
      return {
        number: 12
      }
    },
  template: '<p>いいね{{number}}<button @click="increment">+1</button></p>',
  methods: {
      increment: function() {
      this.number += 1;
      }
  }
}

new Vue({
el: '#app',
        // コンポーネントオプション
        components: {
        // キーに反映させたいhtmlの記述、バリューに変数名
          'my-component': component
     }
})

new Vue({
el: '#app2',
})

77. 「 import App from ‘./App.vue'」の意味を理解する

(main.js)
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

import App from './App.vue'とは

App.vueコンポーネントのオブジェクトをimportしている。
template、script、styleに分かれてる.Vueファイル(=単一ファイルコンポーネント)をimportすることで、main.jsコンポーネントのオブジェクト(グローバルコンポーネントでいう第二引数で取るオブジェクト)をAppという形で利用できるようになる。
render: h => h(App)と書ける。
3つのセクションに分けれるというのが良いところ。

(src/App.vue)
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

78

(src/components/LikeNumber.vue)
<template>
  <p>いいね({{ number }})</p>
</template>

<script>
export default {
  data: function() {
    return {
    number: 5
    }
  }
}
</script>

<style>
</style>

ES6の書き方

export default {
  data() {
    return {
    number: 5
    }
  }
}

79. 実際に単一ファイルコンポーネントを作成してグローバル登録する

全てのインスタンスでLikenumberコンポーネントが使えるようになる。

(src/components/LikeNumber.vue)
<template>
  <LIkeNumber></LIkeNumber>
</template>
(src/main.js)
import Vue from 'vue'
import App from './App.vue'

// グローバル登録
// コンポーネントのオブジェクトをLikeNumberという形で利用できるようになる
import LikeNumber from "./components/LikeNumber.vue";

Vue.config.productionTip = false
// グローバル登録(Vue.componentの形)
// 第一引数がコンポーネント名、第二引数にコンポーネントに関する情報をオブジェクトでセット
Vue.component('LikeNumber', LikeNumber);

new Vue({
  render: h => h(App),
}).$mount('#app')
(public/index.html)
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected <script>を勝手に追加してくれる -->
  </body>
</html>

80. templateはルート要素を1つにしなければならないことに注意する

templateはルート要素を一つにしないとだめ

以下だと、divなどで囲っていないからエラー

<template>
    <p>いいね({{ number }})</p>
    <button @click="increment">+1</button>
</template>
error The template root requires exactly element

単一ファイルコンポーネントを使う時は必ず一つの要素で括ってあげなければならない

<template>
  <div>
    <p>いいね({{ number }})</p>
    <button @click="increment">+1</button>
  </div>
</template>

以下もエラー

<template>
  <div>
    <p>いいね({{ number }})</p>
    <button @click="increment">+1</button>
  </div>
  <div></div>
</template>

81. シングルファイルコンポーネントをローカル登録する

ローカル登録

Likeheader.vueをApp.vueでだけ使えるようにする
(src/components/LikeHeader.vue)
<template>
  <div>
    <h1>いいね</h1>
  </div>
</template>
(src/App.vue)
<template>
    <div>
      <LikeHeader></LikeHeader>
      <LikeHeader></LikeHeader>
      <LikeNumber></LikeNumber>
    </div>
</template>

<script>
// ローカル登録(componentsオプション?みたいな)
// LikeHeader.vueという単一ファイルコンポーネントをimportすることで、コンポーネントのオブジェクトをLikeHeaderという形で利用できるようになった
import LikeHeader from "./components/LikeHeader.vue";
export default {
  components: {
    // コンポーネント名: コンポーネント(の)オブジェクト
    LikeHeader: LikeHeader
    // ES6はkeyとvalueが同じならLikeHeaderに省略可能
  }
}
</script>

f:id:ryota21silva:20200630233147p:plain

82. componentsフォルダを作成して、綺麗なフォルダ構造にする

src直下に全部置くと煩雑になる
→componentsディレクトリを作って、その中にコンポーネントファイルをおく

83. コンポーネントの名前はケバブケースかパスカルケースにする

コンポーネント命名規則 ケバブorパスカル

パスカルケースのがよき

html要素はケバブケースだから、パスカルで書く方が見分けが付きやすい

<template>
<div>
  <!-- ケバブケース -->
  <like-header></like-header>
  <!-- パスカルケース -->
  <LikeHeader></LikeHeader>
</div>
</template>

<script>
import LikeHeader from "./components/LikeHeader.vue";
export default {
  components: {
    // コンポーネント名をパスカルケースにしておくと、templateではケバブ、パスカルどっちでも書ける
    LikeHeader: LikeHeader
  }
}
</script>

DOMテンプレートの中ではケバブケースじゃないとダメ

ブラウザは大文字と小文字の区別を付けられないから。
→だからhtmlはケバブケースを使っている。

DOMテンプレートとは
キャメルケース...最初が小文字、その次大文字。likeHeader

コンポーネントに使える。
しかし、Vueではキャメルケースを使うことは想定されていないらしい。
パスカルケースを推奨。

84. スコープ付きCSSと、Vue.jsがそれをどのように実装しているのか理解する

App.vueのstyleタグにCSSを書く

スコープ付きCSS
(src/components/LikeNumber.vue)
<template>
  <div>
    <p>いいね({{ number }})</p>
    <button @click="increment">+1</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
    number: 5
    };
  },
  methods: {
    increment() {
      this.number += 1;
    }
  }
};
</script>

<!-- スタイルをつけた! -->
<style>
  div {
    border: 1px solid red;
  }
</style>
(src/App.vue)
<template>
<div>
  <like-header></like-header>
  <LikeHeader></LikeHeader>
  <LikeNumber></LikeNumber>
</div>
</template>

<script>
// ローカル登録
// LikeHeader.vueという単一ファイルコンポーネントをimportすることで、コンポーネントのオブジェクトをLikeHeaderという形で利用できるようになった
import LikeHeader from "./components/LikeHeader.vue";
export default {
  components: {
    // コンポーネント名: コンポーネント(の)オブジェクト
    // コンポーネント名をパスカルケースにしておくと、templateではケバブ、パスカルどっちでも書ける
    LikeHeader: LikeHeader
    // ES6はketとvalueが同じならLikeHeaderに省略可能
  }
}
</script>

<!-- スタイルをつけた! -->
<style>
  div {
    border: 1px solid blue;
  }
</style>

以下のように、App.vueのdivタグにも(しかも全てのdivタグに)redが反映されちゃってる。
あるテンプレートにだけ適用したいと思っていたのに... f:id:ryota21silva:20200630233828p:plain

scopedを使う

この中に書くcssは自分のコンポーネントにだけ適用させますよ、というもの。

(src/components/LikeNumber.vue)
<style scoped>
  div {
    border: 1px solid red;
  }
</style>
(src/App.vue)
<style scoped>
  div {
    border: 1px solid blue;
  }
</style>

f:id:ryota21silva:20200630234048p:plain

src/App.vueのstyleを消すと以下のようになったり。 f:id:ryota21silva:20200630234333p:plain

styleタグとbodyタグの中で属性を指定することで、cssの適用範囲を指定している。

div[data-v-276663f0]
<div data-v-276663f0 ....>

f:id:ryota21silva:20200630234239p:plain

【超Vue.js】5. Vue CLIを使った実践的な開発をはじめる方法【セクション5 】

67. なぜVue CLIを使う必要があるのか

Vue CLI(コマンドラインインターフェイス)...大規模な本格的な開発に必要。

なぜ必要?

  1. ファイルの分割

    →大規模な開発で、ファイルを分けたり、くっつけたりしたい。

  2. 最終的なコードの軽量化

    →改行、空白、コメントアウトとかを消してデプロイ

  3. Babel、Typescript、ESLintなどのpluginの使用
  4. HMR(Hot Module Replacement)

    →ファイルを保存するとリアルタイムにブラウザに反映される。開発スピードの向上。

  5. .vue、TS、SCSS、Pug、ES6などの使用

68. Vue CLIのインストールと初期設定を行う

npmはnodejs版パッケージ管理マネジャー(node.jsをインストールすれば使える)。rubyでいうgem。

npm install -g @vue/cli

vue create プロジェクト名

Yarnはパッケージマネジャー →npmでもおk

nodejsでdevelopmentサーバーを使う

$ ls
udemy-vue
~/workspace/udemy/vue_study
$ cd udemy-vue
~/.../vue_study/udemy-vue
$ ls                                                                                                                                                                                              
README.md   babel.config.js node_modules    package.json  public        src     yarn.lock
~/workspace/udemy/vue_study
$ vue --version
@vue/cli 4.4.6
~/workspace/udemy/vue_study
$ vue create udemy-vue


Vue CLI v4.4.6
? Please pick a preset: default (babel, eslint)
? Pick the package manager to use when installing dependencies: Yarn


Vue CLI v4.4.6
✨  Creating project in /Users/funesakisuke/workspace/udemy/vue_study/udemy-vue.
  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...

yarn install v1.22.4
info No lockfile found.
[1/4]   Resolving packages...
[2/4]   Fetching packages...
[3/4]   Linking dependencies...
[###########################################

success Saved lockfile.
✨  Done in 20.46s.
⚓  Running completion hooks...

  Generating README.md...

  Successfully created project udemy-vue.
  Get started with the following commands:

$ cd udemy-vue
$ yarn serve

$ ls
udemy-vue
~/workspace/udemy/vue_study
$ cd udemy-vue
~/.../vue_study/udemy-vue
$ ls                                                                               
README.md babel.config.js node_modules package.json public src yarn.lock
$ yarn serve  
yarn run v1.22.4
$ vue-cli-service serve
INFO  Starting development server...
98% after emitting CopyPlugin

DONE  Compiled successfully in 9508ms                                                                                                                                                                  20:22:16


  App running at:
  - Local:  http://localhost:8080/
  - Network: http://192.168.1.4:8080/

  Note that the development build is not optimized.
  To create a production build, run yarn build.

サーバーが立ち上がった f:id:ryota21silva:20200630225603p:plain

69. Vue CLIが作成したそれぞれのファイルの説明

設定ファイルなどVue CLIが作成したファイルの説明

  • node_modulesファイル...npm installとかする時にインストールするパッケージが入ってる。

  • babe.configはbabelの設定ファイル
    →babelは開発ではES6の最新状態を、公開時にはES5にトランスファイルするためにある。基本的にはいじらない

  • webpack
    いろんな分割したファイルを一つのファイルにくっつけてくれるソフトウェア。そしてプロダクションに。
    →設定ファイルとsrcを見て一つのフォルダにして公開してくれる。

  • component

  • main.jsがindex.htmlに

【超Vue.js】Vueインスタンスとその内部構造はこうなっている【セクション4 】

50. イントロダクション

Vueインスタンス

Vueインスタンスって複数作れるの?他のjsからアクセスできるの?とかの内容

51. Vue インスタンスは複数作ることができる

Vueインスタンスはなるべく複数使用しない方がいい

完全に独立してるなら使ってもおk。
#app2押すと、#app1に何らかの処理が起きるとかはやめよう。そのような処理は一つのインスタンスの中(#app1の中)にまとめる。

52. 外側からVue インスタンスにアクセスする方法

<div id="app1">
  <p>{<200c>{ message }}</p>
</div>
<div id="app2">
  <p>{<200c>{ message }}</p>
  <button @click="changeMessage1">変更</button>
</div>
 
// 変数を設定
var vm1 = new Vue({
    el: '#app1',
  data: {
    message: 'インスタンス1'
  }
})
 
// 変数を設定
var vm2 = new Vue({
    el: '#app2',
  data: {
    message: 'インスタンス2'
  },
  methods: {
    changeMessage1: function() {
        // #app2から#app1のインスタンスにアクセス
        vm1.message = 'インスタンス2からアクセスして変更しました'
    }
  }
})

53. リアクティブシステム(getter、setter、Watcher)がどのように動いているかを確認し、プロパティを後から追加できないことを理解する

外部から新しいプロパティの追加はできない

厳密に言うとリアクティブにならない

リアクティブVue側のデータが変わると、HTMLのデータも変わる(ブラウザの表示が変わる)。これはvueがやってくれている。

リアクティブになる理由

まずインスタンス内のdataに、リアクティブにしたいプロパティを最初に置いておく。以下ではmessage

Vue.jsがやってくれることは、インスタンスのプロパティウォッチャを作ってくれる(=getterとsetterを用意してくれる)

getter...その変数(プロパティ)が参照された時に、関数を実行する setter...その変数(プロパティ)が変更された(書き換わった)時に、関数を実行する。
ウォッチャとは...getter,setterをトリガーに関数を実行してくれる。

var vm = new Vue({
    el: '#app',
  data: {
    message: 'こんにちは'
  }
})
 
// インスタンス生成後に、インスタンスの外部から後付けで作ったプロパティ
vm.name = 'よしぴー'
 
console.log(vm);

上記を書いてconsoleをみてみると、以下がある。

get message: ƒ proxyGetter()
set message: ƒ proxySetter(val)

→これがあるから、リアクティブになる!!

一方で、外部から作ったプロパティvm.nameにはgetterとsetterがない、つまり、clickイベントで値を変えようとしても変わらないのです。

54. Vueインスタンスプロパティの$dataの紹介

Vueインスタンスが使えるようなメソッドは$メソッドという形になっている。

外部のデータを、インスタンス内にセットすることはできる。
// 外部のデータ
var data = {
 message: 'こんにちは'
}
 
var vm = new Vue({
 el: '#app',
 // valueに上記の変数dataを入れている
 data: data
})
 
console.log(data === vm.$data);
console.log(vm);

55. インスタンスの内側から、Vueインスタンスのプロパティやメソッドにアクセスする

内部からはthis

var data = {
    message: 'こんにちは'
}
 
var vm = new Vue({
    el: '#app',
   data: data,
    computed: {
      myData: function() {
            return this.$data;
                  }
            }
})

56. VueのAPI一覧はここに載っています。

Vueインスタンスのプロパティやメソッドはどう使うのかなあ
→公式のAPI一覧を見よう!!

57. $mountメソッドを使用して、elプロパティの代わりにする

$mount

たまに使う。elプロパティの代わりになる

// まだインスタンス内で、elで指定していない
var vm = new Vue({
  data: data,
  computed: {
    myData: function() {
        return this.$data;
    }
  }
})
// ビューテンプレートを指定している(マウントしている)
vm.$mount('#app')

58. templateプロパティを使って、文字列のみでテンプレートを書く

テンプレートの書き方は3種類ある

1. 普通の書き方

<p>{<200c>{ message }}</p>
<button @click="message = 'ボタンから変更'">変更</button>

2. templateプロパティ(この書き方は複数行あると大変)

<div id="app2"></div>
 
new Vue({
  el: '#app2',
  data: {
    name: 'りょうた'
  },
  // teplateプロパティの値に、そのまま全部文字列として入れる
  template: '<p>こんにちは、{<200c>{ name }}</p>'
})

$mountを末尾に付けても同じように書ける

new Vue({
  data: {
    name: 'りょうた'
  },
  template: '<p>こんにちは、{<200c>{ name }}</p>'
}).$mount('#app2')

3. render関数(次回)

59. render(描画)関数を使用して、仮想ノードを作ってDOMの描画を行う

render関数

createElementはデフォルトで使える

<div id="app3"></div>

new Vue({
  data: {
  name: 'りょうた'
  },
  render: function(createElement) {
  // createElementはメソッドになっている
  return createElement('h1', 'こんにちは' + this.name);
  },
}).$mount('#app3')

以下のようにhで書くことも多い

render: function(h) {
  return h('h1', 'こんにちは' + this.name);

上記はdocument.createElementのcreateElementとは全くの別物

DOM

var dir = document.createElement('div');
console.log(dir);
console.log(document);
console.dir(document);

documentはdocument(=ブラウザ)がHTMLを受け取ってDOMに変換してオブジェクトにしてくれる。
objectはキーと値
modelは形
DOMを読み取ってHTMLぽく表示してくれる

return createElement('h1', 'こんにちは' + this.name)は仮想Nodeを作っている。
→VNodeは仮想Node→オブジェクト(ただの情報=文字列)を返している
→DOMを触っていない
→仮想DOMを作る
f:id:ryota21silva:20200630224142p:plain

document.createElement('div');は直接DOMにアクセスしている f:id:ryota21silva:20200630224156p:plain

仮想DOMと、その必要性を理解する

仮想DOMを作るために仮想Nodeを返す

仮想DOM...DOM要素を模したJS用のオブジェクト

何故使う?

ボタンを推した時に表示が変わるようにする
→変更する時に、部分的にDOMを使うのが効率良い
→DOMを一回作って、新しく作ったDOMと見比べて、変更がある箇所(差分)だけ変える、この新しいDOMを作るのがめっちゃ効率悪い。
→仮想的なDOM(自分のjsで仮想的なDOMオブジェクト)を作って、前の仮想DOMと見比べて変更する。効率が良い。

普通のDOM

DOMにアクセスして、追加、削除、変更とかするからめっちゃ遅い。パフォーマンス悪い。
普通のDOMはブラウザにあるから(documentとか)、DOMにアクセスする=ブラウザにアクセスするから遅い

62. Vue インスタンスライフサイクルの全体像を見て、Vueがどのように動いているかを理解する

ライフサイクル

Vuejsが作られてから、どのような動きをして〜までの流れ。

  1. new Vue()...beforeCreate()
  2. インスタンスが作られる(データがリアクティブになる)...created() (elがあるかどうか)
  3. ある場合、templateをrender関数にコンパイル(ない場合、vm.$mountが呼ばれた時に、templateをrender関数にコンパイル)
  4. $elを作って実際のDOMに追加
  5. mounted(⇄データが変わるとDOMを再描画)
  6. Destroyed(手動でやることは基本ない)

63. ライフサイクルフックのタイミングを実際にコードで確かめる

<div id="app4">
  <p>こんにちは、{<200c>{name}}</p>
  <button @click="name = '太郎'">名前変更</button>
  <button @click="destroy">インスタンスを破壊</button>
</div>
 
new Vue({
  el: '#app4',
  data: {
    name: 'りょうた'
  },
  beforeCreate: function() {
    console.log('beforeCreate');
  },
  created: function() {
    console.log('Created');
  },
  beforeMount: function() {
    console.log('beforeMount');
  },
  mounted: function() {
    console.log('mounted');
  },
  beforeUpdate: function() {
    console.log('beforeUpdate');
  },
  updated: function() {
    console.log('updated');
  },
  beforeDestroy: function() {
    console.log('beforeDestroy');
  },
  destroyed: function() {
    console.log('destroyed');
  },
  methods: {
    destroy: function() {
        this.$destroy()
    }
  }
})

64. コンポーネントを使用して、同じようなインスタンスを使い回す

コンポーネントを登録した後に、インスタンスで使う

Vue.component('hello', {
  template: '<h1>こんにちは</h1>'
});
<div id="app">
  <hello></hello>
  <hello></hello>
</div>
 
var data = {
    message: 'こんにちは'
}
 
Vue.component('hello', {
  template: '<h1>こんにちは</h1>'
});
 
var vm = new Vue({
  data: data,
  computed: {
    myData: function() {
        return this.$data;
    }
  }
})
 
vm.$mount('#app')

【超Vue.js】条件付きレンダリングとリストレンダリング【セクション3 】

37. v-elseを使って、v-ifがfalseの場合の処理を書く

<div id="app">
  <p v-if="ok">Hello</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    ok: true,
  }
})

38. v-else-ifを使って、複雑な条件式を作る

<div id="app">
  <p v-if="ok">OK</p>
  <p v-elseif="maybeOk">maybe OK</p>
  <p v-else>Not OK</p>
</div>
 
 
new Vue({
    el: '#app',
  data: {
    ok: false,
    maybeOk: true
  }
})

39. templateタグを使用して、不必要な要素を加えずにv-ifを複数の要素に適応させる

検証ツールなどでも全く表示されないモノ。グループ化するよ

v-ifを複数選択したい場合、以下をどのようにまとめるか。

<div id="app">
  <p>Hello</p>
  <template>
    <p v-if="ok">OK</p>
    <p v-if="ok">OK</p>
    <p v-if="ok">OK</p>
  </template>
</div>

[解決策]templateでまとめる

<div id="app">
  <p>Hello</p>
  <template  v-if="ok">
    <p>OK</p>
    <p>OK</p>
    <p>OK</p>
  </template>
</div>

40. v-showを使って、頻繁に何かを切り替える処理のパフォーマンスを高める

<p v-show="ok">Show</p>

display: none;になる。

v-showはtemplateでは使えない

→初期描画が遅い(真偽関係なくDOMに追加してしまうから、falseでもdisplat: none;の状態でセットしてしまう)
→切り替えは早い

v-ifでは切り替え毎に、要素を削除してる(DOMレベルで消してる)から、切り替えが遅い。

41. v-forディレクティブを使用して、配列に基づいてリストを描画する

配列をレンダリングさせる

fruitの部分は変数となっており、名前は何でもおk

<div id="app">
  <ul>
    <li v-for="fruit in fruits">{<200c>{ fruit }}</li>
  </ul>
</div>
 
new Vue({
    el: '#app',
  data: {
    fruits: ['りんご', 'バナナ', 'ぶどう']
  }
})

42. 2つ目の引数に配列のインデックスを取ってv-forを使用する

配列のインデックス

<li v-for="(fruit, index) in fruits">({‌{index}}){‌{ fruit }}</li>

43. オブジェクトに対してv-forディレクティブを使用する

オブジェクトをレンダリングする

<ul>
    <li v-for="value in object">{<200c>{ value }}</li>
  </ul>
 
new Vue({
    el: '#app',
  data: {
    fruits: ['りんご', 'バナナ', 'ぶどう'],
    object: {
        firstName: '太郎',
      lastName: '田中',
      age: 21
    }
  }
})

44. オブジェクトのv-forには、第2引数と第3引数にキーとインデックスを取る

第二引数にキー、第三引数にインデックスを取る

 <ul>
    <li v-for="(value, key, index) in object">{<200c>{index}}-{<200c>{key}}-{<200c>{ value }}</li>
  </ul>
 
new Vue({
    el: '#app',
  data: {
    fruits: ['りんご', 'バナナ', 'ぶどう'],
    object: {
        firstName: '太郎',
      lastName: '田中',
      age: 21
    }
  }
})

45. templateタグを使用して、不必要な要素を加えずにv-forを複数の要素に適応させる

divタグの代わりにtemplateタグを使う

→templateは消えるから、副作用がないし、簡潔にかける

<template v-for="fruit in fruits">
      <li>{<200c>{ fruit }}</li>
      <hr>
</template>

46. n in 10 のように、整数値に対してv-forを適用する

v-forの引数には配列、オブジェクト以外に整数も取れる

<ul>
    <li v-for="n in 10">{<200c>{ n }}</li>
</ul>

47. inの代わりにofを使用してJavaScriptイテレータ構文に近い書き方をする

inはofに変えれる

<template v-for="fruit of fruits">

48. key属性を付ける必要性を学び、予期せぬバグを起こさないv-forを作る

v-forを使う時は必ずkey属性をつけること!!!

→v-forは、要素の移動を最小限に抑えるアルゴリズムを使用し、可能な限りその場で同じタイプの要素を再利用しようとする、という問題がある。
???

先頭のdivタグを削除しても、

<div id="app">
  <ul>
    <div v-for="fruit in fruits">
      <p>{{ fruit }}</p>
      <input type="text">
    </div>
  </ul>
  <button @click="remove">yeah</button>
</div>

new Vue({
el: '#app',
  data: {
  fruits: ['りんご', 'バナナ', 'ぶどう']
  },
  methods: {
    remove: function() {
    // shiftで先頭を削除
      this.fruits.shift()
    }
  }
})
<ul>
    <div v-for="fruit in fruits" :key="fruit">
      <p>{{ fruit }}</p>
      <input type="text">
    </div>
</ul>

一意のkeyを与えることで、一つ一つのdivが一意な存在となり、divタグと配列の対応付けが可能となる

注意点

  • templateタグにkey属性は使えない(templateはDOMに表示されないから)
  • indexを指定してしまう
<div v-for="(fruit, index) in fruits" :key="index">

→先頭のdivタグを削除すると、indexの番号が変わってしまう - keyにはオブジェクトを使って一意なidを付けてあげたりする

【超Vue.js】これがVue.jsの基礎、テンプレート構文だ【セクション2 】

はじめに

以下にUdemyの動画、超Vue.js2のメモを備忘録として書き連ねていきます。セクション毎に記事を分けます。

1. Vueの良いところ

簡単、柔軟、高性能
HTMLファイル内の一部にVueを取り入れる、一つのファイルをVueだけで作る、アプリ全てをVueにするSPA等もできる。

18. v-on使用時に、イベントオブジェクトを使ってイベントの情報を取得する

イベントオブジェクト...そのイベントの情報が入ったオブジェクト。イベントが発生した時にイベントに関する全ての情報が入ったオブジェクト。

ex.) マウスでクリック→マウスの位置が入っている=イベントオブジェクト

マウスを文字の上に載せた時に、x軸とy軸の情報が入ってくる=イベントオブジェクト

<p v-on:mousemove="changePosition(10, $event)">マウスを載せる</p> 
<p>{<200c>{x}}{<200c>{y}}</p>

function(event)と引数を入れるだけで、イベントオブジェクトを取得できる。

 changePosition: function(event) {
      this.x = event.clientX
      this.y = event.clientY
    }

19. v-onに指定しているメソッドに引数を持たせる

v-onで指定したメソッドに、引数を取る

以下は、ボタンを押すとカウントが3増える。

<button v-on:click="countUp(3)">カウントアップ</button>
 
 
methods: {
    countUp: function(times) {
        this.number += 1 * times
    }

引数イベントオブジェクトを両方取りたいとき

$イベントを使う。$イベントにイベントオブジェクトが入る。

<p v-on:mousemove="change(10, $event)">マウスを載せる</p>
 
// divideNumberに10が、eventに$eventが入る
change: function(divideNumber, event) {
        this.x = event.clientX / divideNumber;
        this.y = event.clientY / divideNumber;
    }

$イベント

<p v-on:mousemove="change(10, $event)">マウスを載せる</p>
  <p>{<200c>{x}}{<200c>{y}}</p>
 
methods: {
    countUp: function(times) {
        this.number += 1 * times
    },
    change: function(divideNumber, event) {
        this.x = event.clientX / divideNumber;
        this.y = event.clientY / divideNumber;
    }
  }

20. stopと.preventという2つの例を見ながら、頻繁に行われる処理を簡略化したイベント修飾子を理解する

イベント修飾子

  • stop...VueではstopPropagationを簡単に書ける
  • prevent...preventDefaultを簡単に書いた

後ろに伝盤しない(stop)

<p v-on:mousemove="change(10, $event)">マウスを載せる<span v-on:mousemove="noEvent">反応しないで</span></p>
  <p>{<200c>{x}}{<200c>{y}}</p>

noEvent: function(event) {
        event.stopPropagation()
    }

以下は上と同じ処理
stopというイベント修飾子を使用している。

<p v-on:mousemove="change(10, $event)">マウスを載せる<span v-on:mousemove.stop>反応しないで</span></p>
  <p>{‌{x}}、{‌{y}}</p>

aタグのdefaultの挙動を妨げる(prevent)

<a @click="noEvent" href="https://google.com">Google</a>

noEvent: function(event) {
        event.preventDefault();
    }

以下はpreventを使用している。

<a @click.prevent href="https://google.com">Google</a> 

21. キー修飾子を使って、特定のキーボードを押した時にイベントを発火するようにする

キー修飾子

キーボードに対するDOMイベントのこと。 何かキーを離した瞬間にイベントが発生する(rでもpでもスペースでもEnterでも)

<input type="text" v-on:keyup="myAlert">
 
myAlert() {
        alert('アラート!');
    }

上のやり方は不便。
→特定のキーボードだけイベント発火対象として認識させる。

<input type="text" v-on:keyup.enter="myAlert">
<input type="text" v-on:keyup.space="myAlert">
<input type="text" v-on:keyup.spaceenter="myAlert">

22. v-onディレクティブの引数を[ ]を使って動的に表現する

v-onの引数を動的に取る

v-bindと同じように[ ]を使う。

<div id="app">
  <p>現在{<200c>{ number }}回</p>
  <button v-on:[event]="countUp">カウントアップ</button>
</div>
 
new Vue({
    el: '#app',
  data: {
    number: 0,
    event: 'click'
  },
  methods: {
    countUp: function() {
        this.number += 1
    }
  }
})

24. v-modelを使用して、双方向バインディングを作成する

* 双方向…データが双方向に

* バインディング...Vueインスタンスのdataと、ビュー画面に表示されている所が結合されている。

モデルのデータをテンプレートから変更できるようになる!

<div id="app">
  <input type="text" v-model="message">
  <p>{<200c>{ message }}</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    message: 'こんにちは'
  }
})

25. computedプロパティを使って、動的なデータを表現する

computedプロパティ...動的なプロパティを使う時に用いるモノ。

データは動的なモノは表現できない。あくまで初期値を扱う。
→そこでcomputedを使い、動的なモノを表現する

  • computedの中で関数(function)を書くこと

  • あくまでプロパティだからreturnする必要あり

以下で三項演算子を扱ってみた。counterプロパティのデータによって、表現を動的に変える

<div id="app">
  <p>{<200c>{ counter }}</p>
  <button @click="counter += 1">+1</button>
  <p>{<200c>{ lessThanThree }}</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    counter: 0,
  },
  computed: {
    lessThanThree: function() {
        return this.counter > 3 ? '3より上' : '3以下'
    }
  }
})

26. computedとmethodの違いを理解し、算出プロパティのリアクティブな依存関係にもとづきキャッシュされるという性質を理解する

computedと同じことをmethodでも表現できるが、動的なプロパティを仕様する時は、computedプロパティを使うようにすること!

2つの違いは、いつ関数が実行されているか、それに注目する
{{ }}にメソッドを書いてしまうと、テンプレートが少しでも再描画された時に毎回メソッドが実行されてしまう。絶対使うな!

以下では、othercounterは本来関係ないのにメソッドが実行されてしまう。

<div id="app">
  <p>{{ counter }}</p>
  <button @click="counter += 1">+1</button>
  <p>{{ otherCounter }}</p>
  <button @click="otherCounter += 1">別の+1</button>
  <p>{{ lessThanThree }}</p>
  <p>{{ lessThanThreeMethod() }}</p>
</div>

new Vue({
el: '#app',
  data: {
  counter: 0,
    otherCounter: 0
  },
  computed: {
  lessThanThree: function() {
    console.log('Computed');
    return this.counter > 3 ? '3より上' : '3以下'
    }
  },
  methods: {
  lessThanThreeMethod: function() {
    console.log('Method');
    return this.counter > 3 ? '3より上' : '3以下'
    }
  }
})

computedは依存関係に基づいてキャッシュされる。
→参照先this.counterの値が変わった時のみ、computedプロパティが実行されるようになっている。

27. ウォッチャを使って、データが変わった時に特定の処理をする

watch...特定のデータが変わった時に何か処理をする。counterが変わった時に処理をするみたいな。

computedとの比較

基本watchは使わない。
watchは非同期の処理で使う。

  • returnはあくまで同期処理として返すモノ。だからcomputedは非同期には使わない。
  • 非同期処理にthisは使えない。
    →変数にthisを入れて使う感じ
new Vue({
    el: '#app',
  data: {
    counter: 0,
    otherCounter: 0
  },
  computed: {
    lessThanThree: function() {
        return this.counter > 3 ? '3より上' : '3以下'
    }
  },
  watch: {
    counter: function() {
        var vm = this;
        setTimeout(function(){
        vm.counter = 0
      }, 3000)
    }
  }
})

28. 丸カッコ( )は、二重中括弧とv-onディレクティブにおいて、いつ必要なのか

ココむずい!!

丸括弧()をつけるのかどうか

computed

→{‌{ }}の中では必ず付けない

メソッド

→{‌{ }}の中ではつける

@clickとかDOMイベントの中では、つける時も付けない時もある。

→この中ではあくまでJSの式を書いてる

29. クラスをデータにバインディング(紐付け)する方法その1。オブジェクトを渡し、真偽値を使って動的に適応させるクラスを切り替える

真偽値を使ってclass属性を動的に切り替える

<div id="app">
  <p :class="{ red: true, 'bg-blue': false}">Hello</p>
</div>
 
.red {
  color: red;
}
 
.bg-blue {
  background-color: blue;
}

クリックでbg-blueとかを動的に切り替え

<div id="app">
  <p :class="{ red: isActive, 'bg-blue': !isActive }">Hello</p>
  <button @click="isActive = !isActive">切り替え</button>
</div>
 
new Vue({
    el: '#app',
  data: {
    isActive: true
  }
})

インラインをVueインスタンスに移す

<div id="app">
  // classObjectにしてVueインスタンスに持たせる
  <p :class="classObject">Hello</p>
  <button @click="isActive = !isActive">切り替え</button>
</div>
 
new Vue({
    el: '#app',
  data: {
    isActive: true
  },
  computed: {
    classObject: function() {
        return { 
        red: this.isActive,
        'bg-blue': !this.isActive
      }
    }
  }
})

30. クラスをデータにバインディングする方法その2。配列を使って、適応させたいクラスを並べる

配列を使って、動的にbg-blueを記述する
<h1 :class="[{red: isActive}, bg]">Hello</h1>
 
 
new Vue({
    el: '#app',
  data: {
    isActive: true,
    color: "red",
    bg: "bg-blue"
  },
  computed: {
    classObject: function() {
        return { 
        red: this.isActive,
        'bg-blue': !this.isActive
      }
    }
  }
})

31. スタイル属性を、オブジェクトを用いて動的にバインディングする

32章の書き方がオススメ

  1. オブジェクトで書く
  2. 配列で書く
<div id="app">
  <p :style="{color: textColor, 'background-color': bgColor}">Hello</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    // v-modelを使用しているから、以下を変えるだけでスタイル属性が動的に切り替わる
        textColor: 'red',
        bgColor: 'blue'
  }
})

32. スタイルオブジェクトをデータに書いて、コードを見やすくする

この書き方が推奨

<div id="app">
  <p :style="styleObject">Hello</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    styleObject: {
        // v-model使ってるから、この中をいじるだけでおk
           color: 'red',
          'background-color': 'blue'
    }
  }
})

33. 複数のスタイルオブジェクトを配列構文を用いて適応させる

inlineスタイルのオブジェクトを配列構文で適応。

styleObjectが複数
→汎用的なモノと特有なモノ

<div id="app">
  <p :style="[styleObject, baseStyles]">Hello</p>
</div>
 
new Vue({
    el: '#app',
  data: {
    styleObject: {
         color: 'red',
        'background-color': 'blue'
    },
     baseStyles: {
         fontSize: '60px'
    }
  }
})

selializerを使ったAPIの作成と、リクエストスペックについて

jsonのserializerとは?

jsonを生成する仕組みのこと。
この生成したJSONのデータをレスポンスとして返す。

ActiveRecordは ActiveModel::Serialization を include している。
なので、そのまま以下のような記述もできる。

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        render json: articles
      end
    end
  end
end

PostmanでAPIを叩くとJSONレスポンスが返ってきていることが分かる。

[今回の要件]

fast_jsonapiというserializerを使って、記事一覧のjson responseを返す。
fast_jsonapi/readme

Articleモデル。Userとは一対多の関係。

class Article < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy

  has_one_attached :eye_catch
  enum status: { draft: 0, in_review: 10, published: 20, archived: 30 }
end

Articleモデルのシリアライザーを作る

$ be rails g serializer Article title contents status                                    
Running via Spring preloader in process 21798
      create  app/serializers/article_serializer.rb

リアライザーの設定

(app/serializers/article_serializer.rb)
class ArticleSerializer
  include FastJsonapi::ObjectSerializer
  attributes :title, :contents, :status
  belongs_to :user
end

APIのコントローラ
Articleのデータから、jsonシリアライズする。
コメントアウトしているが、to_jsonというメソッドを使うのもあり。

(app/controllers/api/v1/articles_controller.rb)
# frozen_string_literal: true

module Api
  module V1
    class ArticlesController < BaseController
      def index
        articles = Article.all
        json_string = ArticleSerializer.new(articles).serialized_json
        # json_string = ArticleSerializer.new(articles).serializable_hash.to_json
        render json: json_string
      end
    end
  end
end

PostmanでAPIを叩いてみると、ちゃんと記事一覧のJSONレスポンスが取れた!

{
    "data": [
        {
            "id": "1",
            "type": "article",
            "attributes": {
                "title": "title-1",
                "contents": "contents-1",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "1",
                        "type": "user"
                    }
                }
            }
        },
        {
            "id": "2",
            "type": "article",
            "attributes": {
                "title": "title-2",
                "contents": "contents-2",
                "status": "draft"
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "2",
                        "type": "user"
                    }
                }
            }
        },
...
......
........

APIのリクエストスペック

  • HTTP 動詞に対応するメソッド(get 、post 、delete 、 patch)を使う。
  • HTTPのレスポンスのステータスコードと、実際のデータを含んだレスポンスボディを返せばよい。

以下【Rails】APIテストの書き方より、ステータスコードの定義を参照。

200: OK - リクエストは成功し、レスポンスとともに要求に応じた情報が返される。
401: Unauthorized - 認証失敗。認証が必要である。
403: Forbidden - 禁止されている。リソースにアクセスすることを拒否された。
404: Not Found - 未検出。リソースが見つからなかった。

テストコード

(spec/requests/api/v1/articles_spec.rb)
require 'rails_helper'

RSpec.describe "Api::V1::Articles", type: :request do
  describe "GET /api/v1/articles" do
    it "掲示板一覧を返す" do
      user = create(:user)
      create_list(:article, 10, user: user)
      get api_v1_articles_path, headers: {
          'CONTENT_TYPE': 'application/json',
          'ACCEPT': 'application/json'
        }
      # JSON.parse(body)でもいけたけど、response.bodyにしておく
      json = JSON.parse(response.body)
      expect(response).to have_http_status(200)
      expect(json["data"].count).to eq(10)
    end
  end
end
JSON.parseについて

与えられた JSON 形式の文字列を Ruby オブジェクトに変換して返してくれる。
JSON形式の文字列からRubyのHashへ変換することで、APIで取得したJSONデータをRubyで使えるようになるって感じ。

JSON.parse(response.body)["data"]の結果

JSON.parse(response.body)["data"]
=> [{"id"=>"1", "type"=>"article", "attributes"=>{"title"=>"title-1", "contents"=>"contents-1", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"1", "type"=>"user"}}}},
 {"id"=>"2", "type"=>"article", "attributes"=>{"title"=>"title-2", "contents"=>"contents-2", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"2", "type"=>"user"}}}},
 {"id"=>"3", "type"=>"article", "attributes"=>{"title"=>"title-3", "contents"=>"contents-3", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"3", "type"=>"user"}}}},
 {"id"=>"4", "type"=>"article", "attributes"=>{"title"=>"title-4", "contents"=>"contents-4", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"4", "type"=>"user"}}}},
 {"id"=>"5", "type"=>"article", "attributes"=>{"title"=>"title-5", "contents"=>"contents-5", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"5", "type"=>"user"}}}},
 {"id"=>"6", "type"=>"article", "attributes"=>{"title"=>"title-6", "contents"=>"contents-6", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"6", "type"=>"user"}}}},
 {"id"=>"7", "type"=>"article", "attributes"=>{"title"=>"title-7", "contents"=>"contents-7", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"7", "type"=>"user"}}}},
 {"id"=>"8", "type"=>"article", "attributes"=>{"title"=>"title-8", "contents"=>"contents-8", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"8", "type"=>"user"}}}},
 {"id"=>"9", "type"=>"article", "attributes"=>{"title"=>"title-9", "contents"=>"contents-9", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"9", "type"=>"user"}}}},
 {"id"=>"10", "type"=>"article", "attributes"=>{"title"=>"title-10", "contents"=>"contents-10", "status"=>"draft"}, "relationships"=>{"user"=>{"data"=>{"id"=>"10", "type"=>"user"}}}}]
リクエストヘッダーをセットしておく。

これはヘッダ情報がないとテストが通らないことがあるため。ちょっと勉強不足ではある。

get api_v1_articles_path, headers: {
      'CONTENT_TYPE': 'application/json',
      'ACCEPT': 'application/json'
    }
headers情報をlet使って格納しておくのも良い
let(:headers) do
  { 'Content-Type' => 'application/json',
    'Accept' => 'application/json'
  }
end
数字を変数(numとか)に入れておくのもアリ
RSpec.describe 'Api::V1::Articles', type: :request do
  describe 'GET /articles' do
    let(:article_num) { 10 }

    before do
      user = create(:user)
      create_list(:article, article_num, user: user)
    end

    it 'returns fincle articles in json format' do
      get api_v1_articles_path, headers: { CONTENT_TYPE: 'application/json', ACCEPT: 'application/json', }

      expect(JSON.parse(body)['data'].count).to eq(article_num)
      expect(response).to be_successful
      expect(response).to have_http_status(:ok)
    end
  end
end

thisを使って、methodsからdataにアクセスする

下記の例文にthisを使ってみる

以下が例文。sayHi関数の結果を出力しているだけ。

<div id="app">
  <p>{{ SayHi() }}</p>
</div>
new Vue({ 
  el: "app",
  data: {
    message: "Hello world!",
  },
  methods: {
    sayHi: function() {
      return 'Hi' ;
    }
  }
})

出力結果

Hi

さあ、thisを使おう

sayHi関数からインスタンス内のデータ(ここではmessageプロパティ)にアクセスしたい時はthisを使う。
テンプレート内でthisを使わず、インスタンス内でthisを使うことに注意する。

<div id="app">
  <!-- コチラの記述は変わらず -->
  <p>{{ SayHi() }}</p>
</div>
new Vue({ 
  el: "app",
  data: {
    message: "Hello world!",
  },
  methods: {
    sayHi: function() {
      // this.messageに変更している
      return this.message ;
    }
  }
})

出力結果

Hello world!

Vueのコンポーネント(追記していく)

はじめに

今日もとりあえずVueの内容について殴り書きしていく。

componentとは

Webサイトの画面はヘッダー、メイン、フッターなど色々な部品から構成されている。そこで、コンポーネントという機能を使えば、ヘッダーなどの部品(コンポーネント)ごとに、HTML, CSS, JSを1セットにしたものを作れる。しかもコンポーネントは再利用が可能。

コンポーネントの基本の書き方
<script>
Vue.component('コンポーネント名(タグ名)', {
  data() {
    // 処理
  },
  template: '描画したいHTMLの内容'
})
</script>
ex.)ボタンをクリックするとカウントが増えていく処理
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

<div id="app">
  <button-counter></button-counter>
</div>

<script>
  Vue.component('button-counter', {
    data() {
      return {
        count: 0
      }
    },
    template: '<button v-on:click="count ++">{{ count }}</button>'
  })
  
  new Vue({
    el: '#app'
  })
</script>
以下のように、登録したコンポーネントは再利用できる。
<div id="app">
    <button-counter></button-counter>
    <button-counter></button-counter>
    <button-counter></button-counter>
</div>

コンポーネントから子コンポーネントへのデータの渡し方

コンポーネントを登録するとコンポーネント名が与えられるが、そのコンポーネント名は Vue.component の第一引数で指定すること(以下ではtodo-item)。

そして、Vueインスタンス(親コンポーネント)のdataは、子コンポーネントに記述しているpropsで受け取ることができる。

ちなみにprops: ['val']のvalは何でもおk。valに親のデータ(=data)が入ってくることは忘れないようにしたい。

templateには利用したいHTMLを記述する。以下ではtemplate: '<li>{{ val.text }}</li>'と記述している。

(index.html)
<div id="app">
  <ol>
    <todo-item
      v-for="item in groceryList"
      v-bind:val="item"
    ></todo-item>
  </ol>
</div>
(main.js)
<script>
// 子コンポーネント
Vue.component('todo-item', {
  props: ['val'],
  template: '<li>{{ val.text }}</li>'
})

// 親コンポーネント
new Vue({
  el: '#app',
  data: {
    groceryList: [
      { id: 0, text: 'Vegetables' },
      { id: 1, text: 'Cheese' },
      { id: 2, text: 'Whatever else humans are supposed to eat' }
    ]
  }
})
</script>
>> 出力結果
1. Vegetables
2. Cheese
3. Whatever else humans are supposed to eat

上記のmain.jsのtemplate内でval.idとすれば、描画内容が変わる。

(main.js)
<script>
// 子コンポーネント
Vue.component('todo-item', {
  props: ['val'],
  template: '<li>{{ val.id }}</li>'
})
...
.....
</script>
>> 出力結果
1. 0
2. 1
3. 2

グローバル登録とローカル登録

  • グローバル登録...全ての(ルート)Vueインスタンス(new Vue({ ... }))で利用できる。 グローバル登録したコンポーネントは使用しない場合でもビルドされるので、ダウンロードするJSのファイルサイズが大きくなり負荷がかかる。
  • ローカル登録..登録したVueインスタンスでのみ利用できる。
グローバル登録
<div id="app1">
  <h1>app1</h1>
  <hello-component></hello-component>
</div>

<div id='app2'>
  <h2>app2</h2>
  <hello-component></hello-component>
</div>
<script>
  // hello-component を登録
  Vue.component('hello-component', {
    template: '<div>Hello Vue.js</div>'
  })

  // 複数のVueインスタンス
  new Vue({
    el: '#app1'
  })
 new Vue({
  el: '#app2'
  })
</script>
>> 出力結果
app1
Hello Vue.js
app2
Hello Vue.js
ローカル登録

el: '#app1'で紐付けたDOM要素(<div id="app1">)にlocal-component1を登録。

<div id="app1">
  <h1>app1</h1>
  <local-component1></local-component1>
</div>

<div id="app2">
  <h2>app2</h2>
  <local-component1></local-component1>
</div>
<script>
// elで紐付けたapp1にだけ、local-component1を登録
new Vue({ 
    el: '#app1',
    components: {
      'local-component1': {
        template: '<div>local 1</div>'
      }
    } 
  })
</script>
>> 出力結果
app1
local1
app2

コンポーネントから親コンポーネントへのデータの渡し方

子でemitを使って、親にデータを渡す。

$emit('イベント名', 渡すデータA, 渡すデータB, ...);

以下では、子コンポーネントのボタンのクリック数を、親コンポーネントで参照している。

<script>
// 子コンポーネント
Vue.component('button-counter', {
    template: '<button v-on:click="childEvent">{{ counter }}</button>',
    data:  ()=> {
        return {
            counter: 0
        }
    },
    methods: {
        childEvent: function () {
            this.counter += 1
            this.$emit('increment')
        }
    },
})

// 親コンポーネント
new Vue({
    el: '#counter-event',
    data: {
        total: 0
    },
    methods: {
        parentEvent: function () {
            this.total += 1
        }
    }
})
</script>
<div id="counter-event">
    <p>クリック数:{{ total }}</p>
    <button-counter v-on:increment="parentEvent"></button-counter>
</div>
処理の流れ
  1. コンポーネントでボタンが押され、childEventが発火→子コンポーネントの数値が加算される。
  2. this.$emit('increment')でincrementイベントが発火→親コンポーネント(counter-eventというプロパティで紐付けたVueインスタンス)のparentEventメソッドが実行される。
  3. parentEventメソッドで親コンポーネントの数値が加算される。

出力結果
https://i.gyazo.com/a65dc8188bdfe157b4680f500131a376.mp4

ミックスイン

DBの整合性を守るtransactionについて

transactionとは

複数のSQL文をtransactionという一つの単位にまとめることで、それぞれのSQLの処理が全て成功した場合にのみ、SQLの処理結果をDBに反映するよというもの。

典型的なのは銀行口座の例で、
①A口座の残高から¥1,000引き落とす
②B口座の残高を¥1,000増やす
という2つの処理を1つのtransactionにまとめる。

これをtransactionにしないと、①の処理は成功したけど②の処理が途中で中断してしまった、という時に、DBの整合性を守れないし、現実世界でトラブルとなることは容易に想像がつく。

Railsのtransactionの例

(app/controllers/admin/articles/article_blocks_controller.rb)
Article.transaction do
   @article.body = @article.build_body(self)
   @article.save!
end

上記の例では
@article.body = @article.build_body(self)

@article.save!
がtransactionになっている。 2つの処理のうち、どちらかが失敗すると、ブロック内(do~end)SQL処理を全部ロールバックする。

そしてRailsでは、ロールバックが発火するには、「例外」の発生が必要。

Railsでは

  • saveは保存できない時にfalseを返す。
  • save!は保存できない時にActiveRecord::RecordInvalidという処理を返す。

なので、save!が失敗した時にロールバックが発火してくれる。

ついでにSQLについて殴り書き

transactionの開始と終了には以下のパターンがある(他にあるんかな?)。

  • BEGIN〜COMMIT
  • BEGIN〜ROLLBACK

参考資料

book.impress.co.jp