- この記事は下書きに1年以上入れたままだったものをサルベージしたものです
- 明確なバージョンは書いていませんが、時期的に2.5系が基準になっています
完全ビルドとランタイムビルドの違い
Electron (node.js) 環境でVue.jsを入れた際に、実行すると以下のWarningがConsoleに出力された。
You are using the runtime-only build of Vue where the template compiler is not available.
Either pre-compile the templates into render functions, or use the compiler-included build.
イマイチわからないのでぐぐったら以下のページで解決法が載っていた。
Rails5.1でVue.jsで単一ファイルコンポーネントのエラーがでる
この記事からリンクされていた公式キュメント さまざまなビルドについて 。
どうやら、Vue.jsには完全ビルドとランタイム限定ビルドというものがあるらしい。
用語
完全: コンパイラとランタイムの両方が含まれたビルドです。コンパイラ: テンプレート文字列を JavaScript レンダリング関数にコンパイルするためのコードです。
ランタイム: Vue インスタンスの作成やレンダリング、仮想 DOM の変更などのためのコードです。基本的にコンパイラを除く全てのものです。
WebpackなどでVueのソースをbundleした場合に、ランタイム限定ビルドとしてbundleされるためにこのWarningが発生する。
記事や公式ドキュメントにあった解決法は、完全ビルドのVue.jsをbundleする方法。
bundlerに明示的に vue/dist/vue.esm.js
などをvueとして指定することで、完全ビルドのVue.jsをbundleしてしまうという解決法になる。
完全ビルドは30%重い
上記の解決法が必要になる理由は、ランタイム限定ビルドが現在Vue.jsのデフォルトになっているからであるが、
先の記事によると
完全ビルドよりも最初の設定の方が30%軽量なんですね。だから公式は初期設定をデフォルトにしているわけですね。
とのこと。
想定としては 単一ファイルコンポーネント を作って、.vue
ファイルをjsからimportして、
js自体もWebpackだったり、Electronアプリを今は作っているからRollup.jsでプリコンパイルして使う予定なのだから、
.vue
ファイルをコンパイルするコンパイラbundleする必要ないし、軽いほうがいい。
書いてたコード(失敗編)
前のプロジェクトで作ってたように、以下のような構成で作成した。
<template>
<div>適当なHTMLタグ</div>
</template>
<script>
// 適当なコード
export default {
}
</script>
<style>
/*適当なスタイル*/
</style>
<div id="vue-root">
<foo-component></foo-component>
</div>
import Vue from 'vue';
import fooComponent from './vue/foo-component.vue';
new Vue({
el: '#vue-root',
component: {
'foo-component': fooComponent,
}
});
で、rollup後のソースが以下。
プラグインの rollup-plugin-vue2を使ってvueのプリコンパイルを実行させている。
(function () {'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var Vue = _interopDefault(require('vue'));
/*適当なスタイル*/
// 適当なコード
var fooComponent = {
render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_vm._v("適当なHTMLタグ")])},
staticRenderFns: [],
data() {
return {
};
}
}
new Vue({
el: '#vue-root',
component: {
'foo-component': fooComponent,
}
});
}());
//# sourcemappingURL=index.js.map
importしていたfoo-component.vueはしっかりとJSのコードに直されている( render:
の行)。
プリコンパイルを行なわない場合は、完全ビルド版のVue.jsは読み込んだ.vue
ファイルからrender行にあるようなJSコードを動的に作成して実行しているということ。
そりゃ、プリコンパイルしたほうが軽くなるよなー。
…しかし、このコードは冒頭のエラーが出て動きません
Warningが出る理由
プリコンパイルをしているのに、なんでpre-compileしろというWarningが出るのか全く理由がわからなかったが、
いろいろ試している間に、以下のコードに修正したら動作した。
<div id="vue-root"></div>
import Vue from 'vue';
import fooComponent from './vue/foo-component.vue';
new Vue({
el: '#vue-root',
render: h => h(fooComponent),
});
違いは、componentの指定を止めVueのルートになるエレメントの中にコンポーネントを指定するのではなく、
直接コンポーネントにするエレメントを指定するようにしたこと。
また、Warningにも書いていたが、render functionを使うようにした。
元の記載方法、
<div id="vue-root">
<foo-component></foo-component>
</div>
これは、ルートにdivを指定しその中に独自のコンポーネントを指定していたが、この形は インラインテンプレート という手法になる。
これ自体は便利な機能だが、インラインテンプレートの中のHTMLは実行時にコンパイルされている為に完全ビルドライブラリが必要になっていた模様。
気づけばそりゃそうだなという感じだが、結構ハマった。
ライブラリビルドを維持して解決する方法まとめ
- インラインテンプレートも使わずにコンポーネントにする
-
.vue
ファイルを作らずに、render functionの中にjsをゴリゴリ書いていく形でもOK。但し書きづらい読みづらいと全く意味は無い -
new Vue()
するときにtemplate:
でHTMLを書く方法もプリコンパイルされないので、ライブラリビルドでは動作しない - よって、
.vue
ファイルを作ってプリコンパイルさせる方法がベスト
-
- コンポーネント内でコンポーネントを使いたい場合は普通に
.vue
ファイルの<tamplate>
の中で入れ子に書いておき、new Vue()
する際にcomponent:
で指定する
単一ファイルコンポーネントとマウント
コンポーネント外から$onで指定したイベントが$emitできない
以下のような構成で、ボタンクリック時のイベントは外部から指定することを想定し、
コンポーネント内では単純に$emitでイベントを発火するだけにしたいと思い、
new Vue
したインスタンスに$on
でイベントを設定、
コンポーネント内からは$emit
でそのイベントを発火させる形にした。
import a_vue from './vue/a.vue';
const a = new Vue({
el: '#a',
render: h => h(a_vue),
});
a.$on('click.button', () => {
console.log('clicked');
});
<div id="a"></div>
<template>
<div>
<button @click="clicked">押す</button>
</div>
</template>
<script>
export default {
methods: {
clicked() {
this.$emit('click.button'); //動かない
},
},
}
</script>
しかし、上記のコードでは click.button
イベントが発火しなかった。
this._events
(イベントが格納されるプロパティ)を出力しても空になっている。
いろいろ調べてみる
export default {
methods: {
clicked() {
console.log(this); //VueComponent {_uid: 4, _isVue: true, $options: Object, _renderProxy: Proxy, _self: VueComponent…}
console.log(this._events); //Object No Properties
},
},
}
import a_vue from './vue/a.vue';
const a = new Vue({
el: '#a',
render: h => h(a_vue),
});
a.$on('click.button', () => {
console.log('clicked');
});
console.log(a); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
console.log(a._events); //Object click.button: Array(1)
- コンポーネント内でのthisは
VueComponent
インスタンスになっている- _uidも4
- コンポーネント外での
new Vue()
のインスタンスはVue$3
インスタンスになっている- _uidは3
- _eventsにも値がちゃんと入っている
- つまりインスタンスが別物
以下のようにした場合、それぞれコンポーネント外のインスタンスと同じものにアクセスできた。
export default {
methods: {
clicked() {
console.log(this.$parent); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
console.log(this.$root); //Vue$3 {_uid: 3, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
console.log(this.$parent._events); //Object click.button: Array(1)
},
},
}
これを踏まえて、以下のようにすると外側で設定したイベントを発火できた。
this.$root.$emit('click.button');
嫌すぎる
-
a.$on
でイベントバインディングしているのに、呼び出すときにthis.$root.$emit
にしなきゃいけない非対称感が気持ち悪い - コンポーネントから見た
this
がコンポーネント自体なのはわかる -
new Vue(a_vue)
したインスタンスがコンポーネントと別物なのが納得行かない
なぜthis
がVueComponent
インスタンスになってしまうか
- コンポーネントとして登録しているから
解釈(推測も含む)
以下の形でrender()
に単一ファイルコンポーネントを渡すと、Vue$3
インスタンス内のコンポーネントとして登録される。
const a = new Vue({
el: '#a',
render: h => h(a_vue),
});
この場合のnew Vue()
したa
は、.vue
で作成したコンポーネントではなく、
htmlに記載した<div id="a">
自体を表し、これがVue$3
インスタンスになっている。
a.vue
ファイルをプリコンパイルした結果のJSコードは、render()
を既に持っている単純なObjectとなっている。
var a_vue = {
render: function () {
var _vm = this;
var _h = _vm.$createElement;
var _c = _vm._self._c || _h;
return _c('div', [_c('button', {on: {"click": _vm.clicked}}, [_vm._v("押す")])])
}, staticRenderFns: [],
methods: {
clicked() {
console.log(this);
console.log(this.$root);
console.log(this.$parent);
},
},
}
ということは、元のコードは、render()内で更にrenderを持っているオブジェクトを渡していたことになる。
なので、import
してきた.vue
ファイルをそのままnew Vue()
の引数に渡せば
VueComponent
ではなくVue$3
インスタンスになるのではないか?
import a_vue from './vue/a.vue';
const a = new Vue(a_vue);
console.log(a); //Vue$3 {_uid: 2, _isVue: true, $options: Object, _renderProxy: Proxy, _self: Vue$3…}
結果、エラーもなく生成された。
仕上げ
但し、ここまでの自体だとブラウザには何も表示されない。
当然と言えば当然で、elを指定していないのでブラウザにマウントされていない。
この辺の説明はさらっとこの辺に書いている。
https://jp.vuejs.org/v2/api/#vm-mount
Vue インスタンスがインスタンス化において el オプションを受け取らない場合は、DOM 要素は関連付けなしで、”アンマウント(マウントされていない)” 状態になります
手動でマウントするには、$mount
を使う。
import a_vue from './vue/a.vue';
const a = new Vue(a_vue);
a.$mount('#a');
これで無事にブラウザに表示された。
また、.vue
ファイルのメソッドでthis
もVue$3
インスタンスとなり、無事にイベントは実行される。
マウントを手動で行うには上記のコードだが、
単純に以下のようにnew Vue()
する前にプロパティを追加しても動作する。
import a_vue from './vue/a.vue';
a_vue.el = "#a";
const a = new Vue(a_vue);
.vue
ファイル内でel
を書き込んで置いても動作する。
export default {
el: "#a",
methods: {
clicked() {
//
},
},
}
とは言え、この書き方は.vue
とhtmlファイルが密結合になるのであまり綺麗じゃないので、
JS内でmountするかelを指定するほうが綺麗だとは思う。
まとめ
https://github.com/vuejs-templates/webpack/issues/215#issuecomment-287652462 これで十分
-
new Vue()
ではrender function
に沿って新たなVue$3
インスタンスを作成する - 単一ファイルコンポーネントは、importしてきたとき
render function
を持っているが、ただのObjectである -
render function
の引数が全てJSコードであれば、インスタンスの生成はライブラリビルドでも実行できる -
render function
の中にrender function
を入れ子に含めると、内側はVueComponent
インスタンスになる - これらを踏まえて、実装時にはコンポーネントとするか単一のVueインスタンスとするかを意識しないとハマる
蛇足の雑感
- 公式ドキュメントはわかりづらい
- 日本語は直訳だし元の文章がよくなさそう
- マウントとかアンマウントの話も$mountの中にしか書いてなかったりするし
-
vuejs-templatesは結構役立つ
- マウントの様々な仕方は このへんを参考にした
- ただ、vuejs-templatesもやっぱり見づらい
- WEB上のFAQはVue.jsに限ってはあまりアテにならない
- この問題でも
this.$root.$emit
使うとか書いてるところが多かったし - ライブラリビルドの件も根本に踏み込んでるところが出てこなかったし
- でも、WEB上の記事がわかりづらくなっているのは、Vue.jsが2から仕様が結構変わったのとかも影響してそう
- この問題でも
- 結局
console.log
が最強