Edited at

「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: {
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つもまとめてみよう。