54
67

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 2017-03-23
1 / 18

はじめに

社内発表用として作成中の資料です。
(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ファイルを実装する。

sample.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が出る)。

以下のように書くことで、親の変更を子へ反映させることができる。

parent.html
<child v-bind:msg="parentMsg"></child>
parent.js
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がでます。

child.html
<input type="text" v-model="msg">
child.js
export default {
  props: {
    msg: {
      type: String,
    }
  }
}

それでも変更したい時は、子コンポーネント内で別のプロパティとして定義する。

child.html
<input type="text" v-model="childMsg">
child.js
export default {
  props: {
    msg: {
      type: String,
    }
  },
  data: function () {
    return {
      childMsg: this.msg
    }
  }
}

単一のコンポーネントでは、双方向のデータバインディングは許されるが、
複数のコンポーネント間(親子関係の)では、単方向のデータバインディングで実装する。


子コンポーネントで起きたイベントを親へ伝える

子で出力している値(テキストボックスの値とかチェックボックスのチェック状態とか)を親で管理している場合、
子で起きたイベントを親へ伝えるような実装にする必要がある。

以下のようにchildMsgとonChangeMsgを子へpropsで渡す。
onChangeMsgはコールバック関数として渡せるので、イベント発生時に親へ通知できる。

parent.html
<child :childMsg="childMsg" :onChangeMsg="onChangeMsg"></child>
parent.js
export default {
  data: function () {
    return {
      childMsg: "child message!!!"
    }
  },
  methods: {
    onChangeMsg: function (msg) {
      this.childMsg = msg
    }
  }
}

子ではprops経由で受け取ったonChangeMsgをv-onを使って書くことで、
イベント発生時に親へ伝えることができる。

child.html
<input type="text" v-on="change:onChangeMsg">{{ childMsg }}</input>
child.js
export default {
  props: {
    childMsg: {
      type: String,
    },
    onChangeMsg: {
      type: Function,
    }
  }
}

↓ v-onは省略可。

<input type="text" @change="onChangeMsg">{{ childMsg }}</input>

親子関係まとめ

先程のことを意識して実装すると、再利用性が高いコンポーネントを作りやすくなる。
また大規模アプリケーションを検討する際に、Fluxのようなアーキテクチャの導入・移行がかなり楽になる。


Fluxの導入について

Viewから状態管理に関わる処理を分離する為に導入する。
中・大規模なアプリケーションになってくると、親コンポーネントが肥大化してくるので、
状態管理に関わる処理の部分は、Flux系のフレームワークに任せて、
親コンポーネントは状態変更させるアクションと変更後の状態を受け取り、
子コンポーネントに渡す役割をすることで、Viewの実装をシンプルにしてくれる。


Fluxについて

 2017-03-17 14.15.06.png ※画像出典:[UNIDIRECTIONAL USER INTERFACE ARCHITECTURES](http://staltz.com/unidirectional-user-interface-architectures.html)

Actions:Viewの入力内容を基にデータを作成
Dispatcher:Actionの命令に合わせて、Storeにデータを送る
Store:データを更新して保管
View:データを表示

Flux導入のメリット

データを更新した際の流れが単方向であり、MVVMフレームワークのような双方向な流れが無いため、分かりやすい

デメリット

Viewでデータを更新する際はActionから実装するため、冗長構成になりやすい


Redux

 2017-03-17 14.58.06.png ※画像出典:[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

 2017-03-17 14.02.57.png ※画像出典:[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のオプションで指定できる。

webpack.config.js
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(コードの静的検証)を導入している。

webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$|\.js$/,
        use: 'eslint-loader',
        exclude: /node_modules/
      },
    ]
  }
}

Vue.jsのLintはeslint-plugin-vue,eslint-config-vueを使用した。

.eslintrc
"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で使われてるライブラリと一緒です。
(いくつか他のライブラリも検討しましたが、結局これにしました。)

karma.conf.js
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に設定しておく。

.babelrc
{
  "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での例

webpack.config.js
module.exports = {
  resolve: {
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
}

番外編3 IEについて

IE以外のブラウザはJSの部分は問題無く動くと思いますが、IEは別途対応が必要。

IE8以下

サポート対象外

IE9以上

babel-polyfillが無いと動かなかった。
Webpackでbabel-polyfillを読み込んであげると動くようになる。

webpack.config.js
module.exports = {
  entry: {
    bundle: [
      `${__dirname}/../node_modules/babel-polyfill/dist/polyfill.js`,
      `${__dirname}/../src/index.js`
    ]
  }
}
54
67
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
54
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?