【Vue.js】HTML importsを用いた擬似単一ファイルコンポーネント

# はじめに
Vue.jsで単一ファイルコンポーネントを使いたい場合はWebpack等のモジュールバンドラでビルドするのが本来の方法です。しかし、学習コストや環境構築の手間がそれなりにかかります。

Vue.js自体はモジュールバンドラを利用しなくても使用できますが、やはりHTML, JavaScript, CSSを一つのファイルにまとめられる単一ファイルコンポーネントは魅力的です。そこで、HTML importsを用いて、ビルドなしで利用できる擬似的な単一ファイルコンポーネントを作成してみました。

普段はjQueryばかり使っていて、Vue.jsやWeb Componentsについては初心者ですので、おかしいところがあればどんどんご指摘をお願いします。

# コンポーネントの作成例
例示用なので実用性はありませんが、親子構造にしてあります。灰色のボックスが親コンポーネント、ボタンが子コンポーネントです。

デモ: http://autumn-leaves.sakura.ne.jp/demo/vue/pseudo-single-file-component/main.html

### 呼び出し元のhtml

- IE11などの古いブラウザはHTML importsに対応していないため、polyfillとしてwebcomponents.jsを使用しています。webcomponents-loader.jsはロードするファイルを必要に応じて選んでくれますが、非同期なので WebComponentsReady イベントを待ってから処理を行う必要があります。

（参考） https://github.com/webcomponents/webcomponentsjs

```html:main.html
<!-- ########## 擬似単一ファイルコンポーネント ##########-->
<!DOCTYPE html>
<html>
<head><!-- =============== HTML Head =============== -->
<meta charset="utf-8">
<title>擬似単一ファイルコンポーネント</title>
<style></style>

<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>

<!-- HTML imports非対応ブラウザ（IE11など）向けpolyfill -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.0.22/webcomponents-loader.js"></script>

<!-- 親コンポーネントの定義ファイルをインポート -->
<link rel="import" href="parent-component.html">

</head>

<body><!-- =============== HTML Body =============== -->

<p>擬似単一ファイルコンポーネント</p>

<div id="app">
	<parent-component v-bind="name"></parent-component>
</div>

<script> // =============== JavaScript =============== //
// イベントリスナはwebcomponents-loader.jsを使用する場合に必要（読み込みが非同期のため）
window.addEventListener('WebComponentsReady', function() {
	var app = new Vue({
		el: '#app',
		data: {
			name: {
				firstName: "John",
				lastName: "Doe"
			}
		}
	});
});
</script>
</body>
</html>
```

### 親コンポーネント

- First name と Last name から Full name を作成し、これらを表示するためのボタンを子コンポーネントとして持ちます。
- 子コンポーネントからbutton-clickedイベントで識別用IDを受け取り、表示を変更します。
- documentオブジェクトは呼び出し元のHTMLドキュメントを指しているので、document.currentScript.ownerDocumentで現在のファイルのdocumentオブジェクトを別に取得する必要があります。
- 拡張子は.vueなどでもOKです。エディタのシンタックスハイライトの機能やファイルの管理方針に応じて選べばよいと思います。

```html:parent-component.html
<!-- ########## 親コンポーネント ##########-->

<!-- 子コンポーネントの定義ファイルをインポート -->
<link rel="import" href="child-component.html">

<!-- ========== Html part ========== -->
<template id="vue_component-parent-component">
	<div class="vue_component-parent-component">
		<p>{{ display }}</p>
		<child-component button-id="first_name" v-bind:value="firstName" v-on:button-clicked="selectButton"></child-component>
		<child-component button-id="last_name" v-bind:value="lastName" v-on:button-clicked="selectButton"></child-component>
		<child-component button-id="full_name" v-bind:value="fullName" v-on:button-clicked="selectButton"></child-component>
	</div>
</template>

<!-- ========== Javascript part ========== -->
<script>
(function() {
	var doc = document.currentScript.ownerDocument;
	var template = doc.querySelector('#vue_component-parent-component');

	Vue.component('parent-component', {
		template: template,
		props: ['firstName', 'lastName'],
		data: function() {
			return {
				selected: "not_selected"
			};
		},
		computed: {
			fullName: function() {
				return this.firstName + ' ' + this.lastName;
			},
			display: function() {
				switch(this.selected) {
					case "first_name":
						return this.firstName;
						break;

					case "last_name":
						return this.lastName;
						break;

					case "full_name":
						return this.fullName;
						break;

					default:
						return "click!";
						break;
				}
			}

		},
		methods: {
			// button-clickedイベントのコールバック
			selectButton: function(buttonId) {
				this.selected = buttonId;
			}
		}
	});
})();
</script>

<!-- ========== CSS part ========== -->
<style>
.vue_component-parent-component {
	display: inline-block;
	padding: 10px;
	border: solid 2px gray;
	background-color: lightgray;
	color: red;
}

.vue_component-parent-component p {
	text-align: center;
}
</style>
```

### 子コンポーネント

- 識別用IDと表示用テキストを設定できるボタンです。
- クリックすると、buttton-clickedイベントを発生させて識別用IDを親に返します。

```html:child-component.html
<!-- ########## 子コンポーネント ##########-->

<!-- ========== Html part ========== -->
<template id="vue_component-child-component">
	<button class="vue_component-child-component" v-bind:value=value v-on:click="clicked">{{ value }}</button>
</template>

<!-- ========== Javascript part ========== -->
<script>
(function() {
	var doc = document.currentScript.ownerDocument;
	var template = doc.querySelector('#vue_component-child-component');

	Vue.component('child-component', {
		template: template,
		props: ['buttonId', 'value'],
		methods: {
			clicked: function() {
				this.$emit("button-clicked", this.buttonId);
			}
		}
	});
})();
</script>

<!-- ========== CSS part ========== -->
<style>
.vue_component-child-component {
	color: blue;
}
</style>
```

# 注意点

- CSSがグローバルで適用されてしまうので、他ファイルの要素に適用されないようにクラス名を工夫するなど注意する必要があります（HTML importsの意図的には、将来的にスコープが分離されるはず？）。

# 感想

- 本来の単一ファイルコンポーネントでの.vueファイルと構成がほぼ同じなので移行は容易だと思います。 
- HTML importsを含めたWeb Componentsは色々と応用が効きそうなので、早くブラウザの対応が進んで欲しいです。
- Vue.jsは学習コストが低いとよく言われているようですが、フレームワーク初心者にとっては簡潔に書ける分だけ暗黙の了解が多く、基本的な概念を理解するは意外と難しい気がします。他のフレームワークよりは簡単なのでしょうが・・・。
- ES2015で書けることなどのメリットを考えると、最終的にはやはりモジュールバンドラを使うことになると思います。