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
元のコードはこれ。生成時になにかしてアップデート時にもなにかする。よくやる。
created () {
this.fetchUserList()
}
watch: {
searchText() {
this.fetchUserList()
}
}
まず、watchは関数名を文字列で受け取れる。
created () {
this.fetchUserList()
}
watch: {
searchText: {
handler: 'fetchUserList'
}
}
immediateをtrueにするとコンポーネントがreadyの時点で実行される!
watch: {
searchText: {
handler: 'fetchUserList',
immediate: true
}
}
きゃー!!便利
2. Component Registration
コンポーネント中で別のコンポーネントを使うために
<BaseInput
v-mode='searchText'
@keydown.enter='search'
/>
<BaseButton @click='search'>
<BaseIcon name='search'/>
</BaseButton>
あちこちにimport書くの面倒くさいよね
import BaseButton from './base-button'
import BaseIcon from './base-icon'
import BaseInput from './base-input'
export default {
components: {
BaseButton,
BaseIcon,
BaseInput
}
}
特定のフォルダ内のコンポーネントをprefixとか使って自動で登録するといいよね!
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” とかに書いとけばいい感じ。
便利ちゃあ便利。バンドルサイズでかくなったりするから使いすぎ厳禁。
componentConfig.default || componentConfig
おまけ。
こう書いておくとexport defaultしてるときとしてないときとどっちでもいけるよね。ハッピー。
3. Module Registration
2と似てる話し。
Vuexモジュールとか使うときにいちいち読み込むの面倒だよね。
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つのフォルダに突っ込んで自動でやっちゃおうぜ。
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
シンプルになりまーす。
import auth from './modules'
export default new Vuex.Store({
modules
})
うむ。まぁ、うむ。
Radical Tweaks - 革命的な微調整
4. Cleaner Views
1つのコンポーネントで複数ページを使うときのテクニック。
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して初期化処理入れとかないと不安。コンポーネントが再利用されちゃうから。
data() {
return {
loading: false,
error: null,
post: null
}
},
created () {
this.getPost(this.$route.params.id)
},
methods: {
getPost(postId) {
// ...
}
}
こんなふうにシンプルにかけたら良いんだけど、、、
できます!この魔法の一行を書いとけば。
<router-view :key="$route.fullPath"><router-view>
フルパスが変わったらコンポーネントが同一でも再描画!
ひぃやっはーー!!!
まぁ、ほんのちょっとパフォーマンス落ちるだろうけど、コンポーネントシンプルに書けるほうが良いよね。ね!
5. Transparent Wrappers
とりあえずシンプルな入力があるとすんじゃん?
例えば、BaseInputコンポーネントね。
<template>
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
</template>
で、このコンポーネント使うときにinputイベント以外のイベントを受け取りたい(inputはemitしてるから飛んでくる)と思ったら毎回.native
書かなきゃいけないじゃん?
<BaseInput @focus.native="doSomething">
ちなみに.native
書くとコンポーネントのルート要素のイベント取れる感じね。
でも、、、
<template>
<label>
{{ label }}
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
</label>
</template>
まぁ、ルート要素が変わったら破綻するよね。破綻っ!
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をイベント指定せずにオブジェクトだけ指定すると、そこで定義されたすべてのリスナーを監視できる。うわぉ!
<BaseInput @focus="doSomething">
はい、.native
さようなら〜
未来の不安も払拭。
もうちょっと。
<BaseInput
placeholder="What's your name?"
@focus="doSomething"
/>
こんな感じでplaceholder書いたらどうなるの?
<template>
<label>
{{ label }}
<input
:value="value"
v-on="listeners"
>
</label>
</template>
こいつのlabelにplaceholder設定される。
Vueでは、propsに書いてないattributeはコンポーネントのルート要素に設定される。。。知らなかった、、、
俺は、placeholderをinput要素に渡したいんだ!!
つ v-bind=$attrs"
<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つじゃなきゃだめってやつ。
こういうときに困るんだよね、実は。
<template>
<li
v-for="route in routes"
:key="route.name"
>
<router-link :to="route">
{{ route.title }}
</router-link>
</li>
</template>
<template>
<ul>
<NavBarRoutes :routes="persistentNavRoutes"/>
<NavBarRoutes
v-if="loggedIn"
:routes="loggedInNavRoutes"
/>
<NavBarRoutes
v-else
:routes="loggedOutNavRoutes"
/>
</ul>
</template>
NavBarRoutesのなかでリストのli要素をv-forで複数作って返す感じ。これはエラーになる。
render関数をつかって書くファンクショナルコンポーネントでは、実は複数ルートを返せる!What!!?
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(地図を描画できる超クールなやつ。熱いやつ。)を例として。
// 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のコードじゃないよね。なんか気に入らないよね。
<MapboxMap>
<MapboxMarkers
:items="cities"
primary
>
<template slot-scope="city">
<h3>{{ city.name }}</h3>
</template>
</MapboxMarkers>
<MapboxNavigation/>
</MapboxMap>
こんな感じのAPIだったら幸せなのに、、、
はーい、こんなふうにしたらできますよー。
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つもまとめてみよう。