Svelte ナメてたけど結構スゴい

<img width="916" alt=" 2017-04-02 23.40.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/25679/153d832f-90cd-0583-30ae-9a8130a71093.png">

[Front-end Developer Handbook 2017](https://frontendmasters.gitbooks.io/front-end-handbook-2017/) でも言及されていたりと少しずつ注目を集めている印象の [Svelte](https://svelte.technology/) ですが、「はいはいまた JS の流行り物 FW でしょ」とか「No Framework って Serverless みたいなバズワードになるんでしょ〜」などと正直ナメてたんですが、ナメっぱなしではいけない、とガイドを一通りさらったところ「なるほどこれは新しい勢力だな」と思い至ったので同じくナメてる人向けにスゴいところを紹介します。一応断っておきますが、さすがに商用環境に投入できるかというとまだ全然出来る気がしないので、今すぐあなたの xxx を置き換えるものではないです。

# Svelte のルック&フィール

https://svelte.technology/guide

Svelte は Riot.js のコンポーネントや Vue.js のコンポーネントのように html ファイルにテンプレートとロジックを書いていくのが基本になっています。

```Time.html
<h1>{{time}}</h1>

<script>
  export default {
    data () {
      return {
        time: new Date()
      }
    }
  }
</script>
```

単一のコンポーネントであれば `svelte-cli` (https://www.npmjs.com/package/svelte-cli) を使って

```
$ svelte compile --format iife Time.html > index.js
```

でコンパイルできます。ブラウザで確認するために簡単な html を書きます。

```index.html
<div id="app"></div>
<script src="index.js"></script>
<script>
  new Time({
    target: document.getElementById('app')
  })
</script>
```

ひとまずこれで日付もろもろが表示されるページが作成できました。ここで、コンパイルされた js を読んでみると

```js
function Time ( options ) {
	options = options || {};
	this._state = Object.assign( template.data(), options.data );
	
	this._observers = {
		pre: Object.create( null ),
		post: Object.create( null )
	};
	
	this._handlers = Object.create( null );
	
	this._root = options._root;
	this._yield = options._yield;
	
	this._torndown = false;
	
	this._fragment = renderMainFragment( this._state, this );
	if ( options.target ) this._fragment.mount( options.target, null );
}
```

というようにコンポーネントそれ自体が他の依存なく動く形にコンパイルされています。テンプレート部分は `renderMainFragment` に含まれていて

```js
function renderMainFragment ( root, component ) {
	var h1 = createElement( 'h1' );
	
	var last_text = root.time;
	var text = createText( last_text );
	appendNode( text, h1 );

	return {
		mount: function ( target, anchor ) {
			insertNode( h1, target, anchor );
		},
		
		update: function ( changed, root ) {
			var __tmp;
		
			if ( ( __tmp = root.time ) !== last_text ) {
				text.data = last_text = __tmp;
			}
		},
		
		teardown: function ( detach ) {
			if ( detach ) {
				detachNode( h1 );
			}
		}
	};
}
```

というような調子です。ここで `createElement` とか `createText` ってなんでしょう、という話なんですが

```js
function createElement( name ) {
	return document.createElement( name );
}

function detachNode( node ) {
	node.parentNode.removeChild( node );
}

function insertNode( node, target, anchor ) {
	target.insertBefore( node, anchor );
}

function createText( data ) {
	return document.createTextNode( data );
}

function appendNode( node, target ) {
	target.appendChild( node );
}
```

こんな感じで JS の関数の薄いラッパーになっていることが分かります。ただ、この後に Observe あたり向けのコードがちょっとあるため合わせて80行ほどランタイムっぽいコードが挿入されることになります。

CSS は html ファイルの `style` タグを使って実装されています。HTML に

```html
<style>
  .time {
    background-color: #ccc;
  }
</style>
```

と書き加えると

```js
var addedCss = false;
function addCss () {
	var style = createElement( 'style' );
	style.textContent = "\n  [svelte-398421373].time, [svelte-398421373] .time {\n    background-color: #ccc;\n  }\n";
	appendNode( style, document.head );

	addedCss = true;
}
```

というような style タグを挿入するコードが追加されて、HTML には `svelte-398421373` という属性が追加されて擬似的な Scoped CSS を実現しています。また、CSS はコンパイラで分離したりもできます。

# Computed property

続いて Vue や knockout などでおなじみの computed property を試します。`Time.html` を以下のように書き換えます。

```Time.html
<h1>{{hours}}:{{minutes}}:{{seconds}}</h1>

<script>
  export default {
    data () {
      return {
        time: new Date()
      }
    },
    computed: {
      hours: time => time.getHours(),
      minutes: time => time.getMinutes(),
      seconds: time => time.getSeconds()
    }
  }
</script>
```

これを先述したコマンドで同様にコンパイルしてブラウザで実行すると、時間のみが表示されるようになります。ここで `computed` のプロパティの関数の引数の `time` はどうやって参照されるのでしょうか？コンパイルされた JS を読むと

```js
function recompute ( state, newState, oldState, isInitial ) {
	if ( isInitial || ( 'time' in newState && differs( state.time, oldState.time ) ) ) {
		state.hours = newState.hours = template.computed.hours( state.time );
	}
	
	if ( isInitial || ( 'time' in newState && differs( state.time, oldState.time ) ) ) {
		state.minutes = newState.minutes = template.computed.minutes( state.time );
	}
	
	if ( isInitial || ( 'time' in newState && differs( state.time, oldState.time ) ) ) {
		state.seconds = newState.seconds = template.computed.seconds( state.time );
	}
}
```

新たに `recompute` 関数が追加されています。

```js
isInitial || ( 'time' in newState && differs( state.time, oldState.time ) )
```

で変更を検査して、更新されていたら

```js
state.seconds = newState.seconds = template.computed.seconds( state.time )
```

で computed property に指定した関数に `state` の当該 property を引数で渡していることが分かります。つまり、コンパイル時点で引数の名前から解決しているわけです。ここら辺はコンパイラとして作ってる利点かなとおもいます。

# Lifecycle

次に、時間を毎秒更新していくように変更します。

```Time.html
<h1>{{hours}}:{{minutes}}:{{seconds}}</h1>

<script>
  export default {
    data () {
      return {
        time: new Date()
      }
    },
    computed: {
      hours: time => time.getHours(),
      minutes: time => time.getMinutes(),
      seconds: time => time.getSeconds()
    },
    oncreate () {
      this.interval = setInterval(() => this.set({
        time: new Date()
      }), 1000)
    },
    ondestroy () {
      clearInterval(this.interval)
    }
  }
</script>
```

これを同様にコンパイルすると時間が動き出します。

コンポーネントのライフサイクルは至ってシンプルで `oncreate` と `ondestroy` のみです。また、コンポーネントには `get` と `set` が定義されているのでこれを使って状態を更新します。`set` は

```js
Time.prototype._set = function _set ( newState ) {
	var oldState = this._state;
	this._state = Object.assign( {}, oldState, newState );
	recompute( this._state, newState, oldState, false )
	
	dispatchObservers( this, this._observers.pre, newState, oldState );
	if ( this._fragment ) this._fragment.update( newState, this._state );
	dispatchObservers( this, this._observers.post, newState, oldState );
};
```

というように新しい `state` を作って `recompute` で computed property を更新して observer に通知するだけ、という単純なものです。

# Nested components

次に、`date` を受け取って UTC を表示する子コンポーネントを組み合わせてみましょう。現在のところ Svelte 自体にバンドルする仕組みはないようで、Browserify や Webpack 向けの Plugin が README で紹介されています。今回は熟れている Browserify と sveltify (https://www.npmjs.com/package/sveltify) でバンドルします。

HTML からエントリポイントを引き剥がして

```index.html
<div id="app"></div>
<script src="index.js"></script>
```

のように変更し、

```index.js
const Time = require('./Time.html')

new Time({
  target: document.getElementById('app')
})
```

新たにエントリファイルを作成しました。続いて子コンポーネントを作成します。

```UTC.html
<h2>{{hours}}:{{minutes}}:{{seconds}}</h2>

<script>
  export default {
    computed: {
      hours: time => time.getUTCHours(),
      minutes: time => time.getUTCMinutes(),
      seconds: time => time.getUTCSeconds()
    }
  }
</script>
```

`time` は親から受け取るので computed property のみです。

```Time.html
<h1>{{hours}}:{{minutes}}:{{seconds}}</h1>
<UTC time='{{time}}' />

<script>
  import UTC from './UTC.html'

  export default {
    data () {
      return {
        time: new Date()
      }
    },
    computed: {
      hours: time => time.getHours(),
      minutes: time => time.getMinutes(),
      seconds: time => time.getSeconds()
    },
    oncreate () {
      this.interval = setInterval(() => this.set({
        time: new Date()
      }), 1000)
    },
    ondestroy () {
      clearInterval(this.interval)
    },
    components: {
      UTC
    }
  }
</script>
```

子コンポーネントとなる html ファイルを `import` してきて親コンポーネントの　`components` に渡してやると HTML で参照できるようになります。

```
$ browserify -t sveltify index.js > public/index.js
```

これでバンドルされた JS を生成してブラウザで確認するとローカルゾーンの時間の下に UTC の時間が表示されるようになります。

ここで、バンドルされた JS を確認すると当たり前ですが先述したヘルパ関数などが重複してしまいます。ここは
 Svelte の特性上しょうがない部分と割り切ってるのかもしれないですが、コンパイラがバンドルまで含めて最適化された JS が出せると尚いいんじゃないかな〜とおもいました。

# 微妙なところ

こういう手合いの HTML のシンタックスを拡張したテンプレートを強要してくるタイプにありがちですが、テンプレートはかなり微妙だとおもいました。`on:click='{{}}'` とか `#each ...`, `#if ...` とか。

また、Two-way binding はまだ実装が途中のようですが `bind:value` のようなシンタックスで input の変更があったら state を更新できます。ここもヘンにシンタックスで解決するよりは React のように value と onChange を指定する方がしっくりくるのは私だけでしょうか...。

# おわりに

Svelte はベンチマーク (http://www.stefankrause.net/js-frameworks-benchmark5/webdriver-ts/table.html) でパフォーマンスも良いようですし、ホビー程度なら採用する選択肢に入れてもいいんじゃないかなとおもいました。今年こそ dynamic import がなんとかなりそうな気配がするので、非同期に Svelte の小さいコンポーネントを読んできてちょっとしたところで(検索ボックスとか、モーダルとか)使ったりできるようになるかもしれません。

ところで、ベンチマークの記事で知ったんですが React ライクなインターフェイスの [dio.js](https://dio.js.org/) はめちゃくちゃ速いですね。また、今年は React が随分長いこと取り組んでいた Fiber が出そうだったり、Angular が 4 になったりと、昨年は「止まって見える」なんて言われてた JS 界隈が大きく前進するといいですね。