JavaScript
vue.js

「Vueコンサルが教えたくない7つの真実」を勉強してみた

Vueの便利なテクニック7つ

Youtubeで見つけたので勉強ついでにまとめてみる。

出展

Chris Fritz さん

Youtube
https://www.youtube.com/watch?v=7YZ5DwlLSt8

資料(Github)
https://github.com/chrisvfritz/7-secret-patterns

まとめ - 動画とはバージョン違い(英語)
https://www.vuemastery.com/conferences/vueconf-2018/7-secret-patterns-vue-consultants-dont-want-you-to-know-chris-fritz/

Productivity Boost - 生産性向上

1. Smarter Watcher

元のコードはこれ。生成時になにかしてアップデート時にもなにかする。よくやる。

js
created () {
    this.fetchUserList()
}
watch: {
    searchText() {
        this.fetchUserList()
    }
}

まず、watchは関数名を文字列で受け取れる。

js
created () {
    this.fetchUserList()
}
watch: {
    searchText:  'fetchUserList'
}

immediateをtrueにするとコンポーネントがreadyの時点で実行される!

js
watch: {
    searchText:  'fetchUserList',
    immediate: true
}

きゃー!!便利

2. Component Registration

コンポーネント中で別のコンポーネントを使うために

html
<BaseInput
  v-mode='searchText'
  @keydown.enter='search'
/>
<BaseButton @click='search'>
  <BaseIcon name='search'/>
</BaseButton>

あちこちにimport書くの面倒くさいよね

js
import BaseButton from './base-button'
import BaseIcon from './base-icon'
import BaseInput from './base-input'

export default {
  components: {
    BaseButton,
    BaseIcon,
    BaseInput
  }
}

特定のフォルダ内のコンポーネントをprefixとか使って自動で登録するといいよね!

js
import Vue from 'vue'
import upperFirst from 'lodash/upperFirst'
import camelCase from 'lodash/camelCase'

// Require in a base component context
const requireComponent = require.context(
  ./components, false, /base-[\w-]+\.vue$/
)

requireComponent.keys().forEach(fileName => {
  // Get component config
  const componentConfig = requireComponent(fileName)

  // Get PascalCase name of component
  const componentName = upperFirst(
    camelCase(fileName.replace(/^\.\//, '').replace(/\.\w+$/, ''))
  )

  // Register component globally
  Vue.component(componentName, componentConfig.default || componentConfig)
})

“src/main.js” とか “/components/_globals.js” とかに書いとけばいい感じ。

便利ちゃあ便利。バンドルサイズでかくなったりするから使いすぎ厳禁。

js
componentConfig.default || componentConfig

おまけ。
こう書いておくとexport defaultしてるときとしてないときとどっちでもいけるよね。ハッピー。

3. Module Registration

2と似てる話し。

Vuexモジュールとか使うときにいちいち読み込むの面倒だよね。

js
import auth from './modules/auth'
import posts from './modules/posts'
import comments from './modules/comments'
// ...

export default new Vuex.Store({
  modules: {
    auth,
    posts,
    comments,
    // ...
  }
})

1つのフォルダに突っ込んで自動でやっちゃおうぜ。

/modules/index.js
import camelCase from 'lodash/camelCase'
const requireModule = require.context('.', false, /\.js$/)
const modules = {}

requireModule.keys().forEach(fileName => {
  // Don't register this file as a Vuex module
  if (fileName === './index.js') return

  const moduleName = camelCase(
    fileName.replace(/(\.\/|\.js)/g, '')
  )
  // namespaceつけとくと便利だぜ!
  modules[moduleName] = {
    namespaced: true,
    ...requireModule(fileName),
  }
  // namespaceいらなければこんな感じ
  // modules[moduleName] = requireModule(fileName)
})
export default modules

シンプルになりまーす。

js
import auth from './modules'

export default new Vuex.Store({
  modules
})

うむ。まぁ、うむ。

Radical Tweaks - 革命的な微調整

4. Cleaner Views

1つのコンポーネントで複数ページを使うときのテクニック。

js
data() {
  return {
    loading: false,
    error: null,
    post: null
  }
},
watch: {
  '$route': {
    handler: 'resetData',
    immediate: true
  }
},
methods: {
  resetData() {
    this.loading = false
    this.error = null
    this.post = null
    this.getPost(this.$route.params.id)
  },
  getPost(postId) {
    // ...
  }
}

idでページ切り替えるようなときはルートをwatchして初期化処理入れとかないと不安。コンポーネントが再利用されちゃうから。

js
data() {
  return {
    loading: false,
    error: null,
    post: null
  }
},
created () {
  this.getPost(this.$route.params.id)
},
methods: {
  getPost(postId) {
    // ...
  }
}

こんなふうにシンプルにかけたら良いんだけど、、、

できます!この魔法の一行を書いとけば。

html
<router-view :key="$route.fullPath"><router-view>

フルパスが変わったらコンポーネントが同一でも再描画!

ひぃやっはーー!!!

まぁ、ほんのちょっとパフォーマンス落ちるだろうけど、コンポーネントシンプルに書けるほうが良いよね。ね!

5. Transparent Wrappers

とりあえずシンプルな入力があるとすんじゃん?
例えば、BaseInputコンポーネントね。

BaseInput
<template>
  <input
    :value="value"
    @input="$emit('input', $event.target.value)"
  >
</template>

で、このコンポーネント使うときにinputイベント以外のイベントを受け取りたい(inputはemitしてるから飛んでくる)と思ったら毎回.native書かなきゃいけないじゃん?

html
<BaseInput @focus.native="doSomething">

ちなみに.native書くとコンポーネントのルート要素のイベント取れる感じね。

でも、、、

BaseInput
<template>
  <label>
    {{ label }}
    <input
      :value="value"
      @input="$emit('input', $event.target.value)"
    >
  </label>
</template>

まぁ、ルート要素が変わったら破綻するよね。破綻っ!
BaseInput使ってて突然動かなくなるというバグに遭遇する危険大。。。恐怖!!

これが解決策だ!ワン、ツー、スリー!

BaseInput
<template>
  <label>
    {{ label }}
    <input
      :value="value"
      v-on="listeners"
    >
  </label>
</template>

<script>
computed: {
  listeners() {
    return {
      ...this.$listeners,
      input: event => 
        this.$emit('input', event.target.value)
    }
  }
}
</script>

リスナーを全部返しちゃう。
v-onをイベント指定せずにオブジェクトだけ指定すると、そこで定義されたすべてのリスナーを監視できる。うわぉ!

html
<BaseInput @focus="doSomething">

はい、.nativeさようなら〜
未来の不安も払拭。

もうちょっと。

html
<BaseInput
  placeholder="What's your name?"
  @focus="doSomething"
/>

こんな感じでplaceholder書いたらどうなるの?

BaseInput
<template>
  <label>
    {{ label }}
    <input
      :value="value"
      v-on="listeners"
    >
  </label>
</template>

こいつのlabelにplaceholder設定される。

Vueでは、propsに書いてないattributeはコンポーネントのルート要素に設定される。。。知らなかった、、、

俺は、placeholderをinput要素に渡したいんだ!!

v-bind=$attrs"

BaseInput
<template>
  <label>
    {{ label }}
    <input
      v-bind=$attrs"
      :value="value"
      v-on="listeners"
    >
  </label>
</template>

うぉぉ!まぶしい!!!

propsに指定されてないattributeは全部ここにバインドされるぜ。

ここ大事!
これを使うときには、attributeを引き継ぐデフォルトの挙動をオフにするために、
inheritAttrs: false
をコンポーネント(ここではBaseInput)に指定しときましょう。

Unlocked Posibilities - 開放された可能性

6. Single-Root Components

このエラーよく遭遇するよね。まじで。

(Emitted value instead of an instance of Error)
  Error compiling template:

  <div></div>
  <div></div>

  - Component template should contain exactly one root element. 
    If you are using v-if on multiple elements, use v-else-if 
    to chain them instead.

コンポーネントのルート要素は1つじゃなきゃだめってやつ。

こういうときに困るんだよね、実は。

NavBarRoutes
<template>
  <li
    v-for="route in routes"
    :key="route.name"
  >
    <router-link :to="route">
      {{ route.title }}
    </router-link>
  </li>
</template>
html
<template>
  <ul>
    <NavBarRoutes :routes="persistentNavRoutes"/>
    <NavBarRoutes
      v-if="loggedIn"
      :routes="loggedInNavRoutes"
    />
    <NavBarRoutes
      v-else
      :routes="loggedOutNavRoutes"
    />
  </ul>
</template>

NavBarRoutesのなかでリストのli要素をv-forで複数作って返す感じ。これはエラーになる。

render関数をつかって書くファンクショナルコンポーネントでは、実は複数ルートを返せる!What!!?

vue
functional: true,
render(h, { props }) {
  return props.routes.map(route =>
    <li key={route.name}>
      <router-link to={route}>
        {route.title}
      </router-link>
    </li>
  )
}

JSX便利(そこじゃない

まぁ、render関数知らなきゃいけないのぐらいが面倒なところかな。

7. Rendering non-HTML

HTMLじゃなくてWebGLとか別のやつをレンダリングする物を使うときのテクニック。

ここではMapGL(地図を描画できる超クールなやつ。熱いやつ。)を例として。

js
// Init
const map = new mapboxgl.Map(/* ... */)

map.on('load', () => {
  // Data
  map.addSource(/* ... */)

  // Layers
  map.addLayer(/* ... */)
  map.addLayer(/* ... */)

  // Hover effects
  map.on('mousemove', /* ... */)
  map.on('mouseleave', /* ... */)
})

まぁ、こんなコードを書きますと。

これって、、、宣言的なVueのコードじゃないよね。なんか気に入らないよね。

html
<MapboxMap>
  <MapboxMarkers
    :items="cities"
    primary
  >
    <template slot-scope="city">
      <h3>{{ city.name }}</h3>
    </template>
  </MapboxMarkers>
  <MapboxNavigation/>
</MapboxMap>

こんな感じのAPIだったら幸せなのに、、、

はーい、こんなふうにしたらできますよー。

js
created() {
  const { map } = this.$parent
  map.on('load', () => {
    map.addSource(/* ... */)
    map.addLayer(/* ... */)
  })
},
beforeDestroy() {
  const { map } = this.$parent
  map.on('load', () => {
    map.removeSource(/* ... */)
    map.removeLayer(/* ... */)
  })
},
render(h) {
  return null
}

だが、$parentはこういうときだけは許すが、基本的には絶対使うなよ!

うーん、この例はいまいちよく理解できてない。

さいごに

この7つのうち2つが違うバージョンもGitリポジトリにあるので、気が向いたらその2つもまとめてみよう。