653
724

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コンサルが教えたくない7つの真実」を勉強してみた

Last updated at Posted at 2018-09-19

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: {
        handler: 'fetchUserList'
    }
}

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

js
watch: {
    searchText: {
        handler: '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つもまとめてみよう。

653
724
5

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
653
724

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?