Posted at

Vue.jsの完全ビルドとランタイムビルドの違い


  • この記事は下書きに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する必要ないし、軽いほうがいい。


書いてたコード(失敗編)

前のプロジェクトで作ってたように、以下のような構成で作成した。


foo-component.vue

<template>

<div>適当なHTMLタグ</div>
</template>
<script>
// 適当なコード
export default {
}
</script>
<style>
/*適当なスタイル*/
</style>


index.html

<div id="vue-root">

<foo-component></foo-component>
</div>


index.js

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のプリコンパイルを実行させている。


index.js

(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が出るのか全く理由がわからなかったが、

いろいろ試している間に、以下のコードに修正したら動作した。


index.html

<div id="vue-root"></div>



index.js

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を使うようにした。

元の記載方法、


index.html

<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でそのイベントを発火させる形にした。


client.js

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>


a.vue

<template>

<div>
<button @click="clicked">押す</button>
</div>
</template>
<script>
export default {
methods: {
clicked() {
this.$emit('click.button'); //動かない
},
},
}
</script>

しかし、上記のコードでは click.button イベントが発火しなかった。

this._events(イベントが格納されるプロパティ)を出力しても空になっている。


いろいろ調べてみる


a.vue

  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
},
},
}


client.js

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にも値がちゃんと入っている



  • つまりインスタンスが別物

以下のようにした場合、それぞれコンポーネント外のインスタンスと同じものにアクセスできた。


a.vue

  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)したインスタンスがコンポーネントと別物なのが納得行かない


なぜthisVueComponentインスタンスになってしまうか


  • コンポーネントとして登録しているから


解釈(推測も含む)

以下の形で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インスタンスになるのではないか?


client.js

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を使う。


client.js

import a_vue from './vue/a.vue';

const a = new Vue(a_vue);
a.$mount('#a');

これで無事にブラウザに表示された。

また、.vueファイルのメソッドでthisVue$3インスタンスとなり、無事にイベントは実行される。

マウントを手動で行うには上記のコードだが、

単純に以下のようにnew Vue()する前にプロパティを追加しても動作する。


client.js

import a_vue from './vue/a.vue';

a_vue.el = "#a";
const a = new Vue(a_vue);

.vueファイル内でelを書き込んで置いても動作する。


a.vue

  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が最強