この記事はVue.js #2 Advent Calendar 2018の18日目の記事です。
Vue.jsを使った開発では、特別な理由がない限り.vue
ファイルで記述するのが主流かと思います。.vue
の場合、テンプレートの定義は<template>
で行うことになるでしょう。
自分も何の疑問が湧くことなく<template>
を使っていましたが、ある日ふとrender
関数に思いを馳せ、いくつかの個人プロジェクトで試してみました。
本記事では、その試行錯誤から得たrender
関数についてのあれこれを記しています。
<template>
がコンパイルされるとどうなるか
render
関数の前に、まずは<template>
がコンパイルされるとどうなるかについて目を向けてみます。
.vue
の<template>
をコンパイルした結果どのように変換されるのか、あまり知らずにVue.jsを使っている方もいるかと思います。意識せずとも使えますし、別に知らなくても特に問題となるようなものでもありません。
<template>
はcreateElement
を使った関数になる
たとえばApp.vue
の<template>
を次のように定義します。
<template>
<div id="app">
<HelloWorld/>
</div>
</template>
これがコンパイルされると、次のような関数に変換されます(見やすいように整形してます)。
var Appvue_type_template_id_54d52fb2_render = function() {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c("div",
{
attrs: {
id: "app"
}
}, [_c("HelloWorld")], 1);
};
このように<template>
はcreateElement
でVDOMを作成する関数へと変換されます。
変数名のAppvue_type_template_id_54d52fb2_render
から、これがApp.vue
の<template>
をrender
にしたもの、と読み取れます。
render
といえば、render
関数ですね。
render
関数の基本
render
関数は<template>
と同じく、Vueコンポーネントにテンプレートを定義するためのものです。
簡単なコードは次のような感じになります。
export default {
data() {
return { msg: "hello render function"}
},
render(h) {
return h("div", {}, this.msg)
}
}
render
関数は引数にcreateElement
を受けるので、それをh
という名前にして使うのが一般的な作法です。
render
関数がコンパイルされるとどうなるか
render
関数で定義したものをコンパイルするとどうなるでしょう。
次のrender
は前述した<template>
の例をcreateElement
を使ったものに置き換えたやつです。
export default {
render(h) {
return h("div", {
attrs: {
id: "app"
}
}, [h(HelloWorld)]);
}
}
これをコンパイルすると次のようになります。
var Appvue_type_script_lang_js_ = ({
render: function render(h) {
return h("div", {
attrs: {
id: "app"
}
}, [h(HelloWorld)]);
}
});
コンパイルされる前と一緒になりました。render
関数でcreateElement
を使って定義されたテンプレートは、変換の必要がない(そのまま実行できる)ということになります。
【ネタ】ビルド速度を突き詰めるとcreateElement
!?【真似しないで】
これは計測もしてない完全な推測ですが、いろいろなものを犠牲にしてもいいなら、ビルドの速度を速くするだけのために<template>
を捨ててcreateElement
にする、みたいなのも考えられるかもしれません。
考えられないし絶対やらないですけど。
宣言的vs命令的
.vue
の<template>
は宣言的にテンプレートを記述できるのが特徴であり、一つの利点です。render
関数はこれと逆を行くもので、関数によってDOMを組み立てるように命令を記述します。
では命令的な記述が必ずしも宣言的なテンプレートに劣っているか?と言われると全くそうではないと思います。それぞれにメリデメがありますし、何を選択するかは使う人に委ねられているでしょう。
ただしcreateElement
をそのまま使ってテンプレートを定義するのは実用に耐えるものではない(めんどくさいし読みづらい)と思います。
createElement
は現実的じゃないけどJSXは結構いける
render
関数ではJSXが使えます。JSXでの記述は、createElement
以上<template>
未満レベルで宣言的であると思います。
ちなみに、Vue CLI V3で作成したプロジェクトはBabelのプリセットに@vue/app
が指定されていて、この中にJSXを使うのに必要なものが入っているので、パッケージの追加インストールなしにJSXが使えます。
export default {
render(h) {
return <div id="app">
<HelloWorld/>
</div>
}
}
JSXを使ったrender
はcreateElement
に変換される
render
関数でcreateElement
を使った場合は、コンパイルされても変化はありませんでしたが、JSXを使っている場合はcreateElement
を使った関数に変換されます。
次のコードは上記JSXの変換結果です。
var Appvue_type_script_lang_js_ = ({
render: function render(h) {
return h("div", {
attrs: {
id: "app"
}
}, [h(HelloWorld)]);
}
});
前述したcreateElement
を使ったrender
関数と同じものに変換されました。
render
with JSXの可能性を探る
「VueでJSX使うくらいならReact使えばよくない?」みたいなことを何度も見たり聞いたりしますが、そんな単純な話ではないと思います。
Vueの書き味やエコシステムといった部分に乗っかりつつ、要所要所でテンプレートをJSX(render
関数)で書くのは何もおかしいところはないと思います。
JSX(render
関数)の可能性を探るために、<template>
を使わずにJSXだけを使ってアプリケーションを作成してみました。そこから得た知見?と感想をまとめておきます。
JSXとcreateElement
は共存できる
属性やイベントを細かく指定したい場合、JSXで書くよりcreateElement
のほうが記述しやすい場合があります。JSXとcreateElement
は共存できるので、次のような書き方もできます。
render(h) {
return <div id="app">
<HelloWorld/>
{h("div", {}, "hoge")}
</div>
}
JSXで書いているとピンポイントでcreateElement
を使いたい場面もでてくるかと思いますので、覚えておいて損はないです。
render
でしかできないこと
render
ではできて、<template>
ではできないことがいくつかありました。
components
に登録せずコンポーネントを使える
import
したコンポーネントを<template>
で使う場合、components
またはVue.component
で登録しないと使えません。
render
関数はJavaScriptのスコープにいるので、import
したコンポーネントは登録せずに使うことができます。前述しているrender
関数の例ではHelloWorld
コンポーネントを使ってますが、components
に登録せずに使っています。
import HelloWorld from './components/HelloWorld.vue'
export default {
render(h) {
return <div id="app">
<HelloWorld/>
</div>
}
}
使うコンポーネントが増える度に、わざわざcomponents
に登録する手間がrender
ではありません。
別ファイルの定数をそのまま使える
前述したコンポーネントと同じ話ですが、別の.js
ファイルに定義した定数をコンポーネント内で使う場合、computed
やdata
に割り当ててからでないと<template>
からは参照できません。
render
関数ではimport
した値はそのまま使えます。
import constants from './constants'
export default {
render(h) {
return <div id="app">
{constants.message}
</div>
}
}
イベントなどをすべて定数にする
<template>
で@customEvent
な記法をするときに、このcustomEvent
の値を変数を参照する形で定数にしたい、と思っていろいろ試したができませんでした。
render
関数では次のように記述できます。
import Channel from "../components/Channel.vue"
import types from "../store/types"
import events from "../variables/events"
export default {
name: "Channels",
render(h) {
return h(Channel, {
class: "Channel",
on: {
[types.REACTION_TO_MESSAGE]: this.reactionToMessage,
[events.CLICK_REACTION]: this.reactionToMessage,
},
})
},
}
$emit
やdispatch
などに使うイベント名を定数にして、テンプレートで同じ値を参照して使うようにできます。
(<template>
ではcomputed
でオブジェクトを返してv-on
に指定することでできなくもないが、これ以外の方法があれば教えて欲しい。)
render propで<div>
を挟まない
HOCに代わるパターンとしてrender propが市民権を得つつあります(もう得た?)。
Vueでrender propをする場合、スコープ付きスロット(scoped slot)を使うことになりますが、<template>
はルート要素として<slot>
を許容しないので、<div>
で挟む必要があります。
render
関数であれば、次のように記述することで綺麗にrender propできます。
render(h) {
return this.$scopedSlots.default({
message: this.message,
})
}
<template>
を使わずに書いていると、<style>
も削れないかな?と思ってしまうのが人間。
そんな願いを叶えてくれるのがvue-styled-components
github.com
JSX+vue-styled-componentsを使うと、もはや.vue
にする必要もなく.js
ですべてを記述できます。
import styled from 'vue-styled-components'
import HelloWorld from './components/HelloWorld.vue'
const Wrapper = styled.div`
padding: 1em;
font-size: 1.5em;
text-align: center;
`
export default {
render(h) {
return <Wrapper>
<HelloWorld/>
</Wrapper>
}
}
ただし、ここまで来るとさすがに「これVueでやる必要あるか〜???」という声が自分の中から聞こえてきます。
(この組み合わせ、別に推奨してるわけじゃないです。)
Vue 3.0が来たらワンチャン?
Vue 3.0ではTypeScript対応が強化されるとのことなので、.tsx
で書くのがわりと普通な状況も来そうだなと予想しています。そうなるとJSX+vue-styled-componentsも候補にあがってもおかしくないのかなと思います。
おわりに
いろいろと書きましたが、Vueのrender
関数(JSX)に対する印象が少しでも変わったなら幸いでございます。