· 8分で読了

Svelteを深く理解する(1)— Svelteのコンパイルプロセス

この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

Svelteを深く理解する(1)— Svelteのコンパイルプロセス

本記事を読む前に、読者がSvelteやすでに他のフロントエンドフレームワークの使用経験があり、その実装の仕組みに興味を持っていることを前提としている。

まだ「Svelteはどうコンパイルされるのか(0)— 抽象構文木とは何か?」を読んでいない場合は、先にそちらを読んでから本記事を読むことをおすすめする。

今日の記事では、いくつかの疑問に答えたい:

  • なぜSvelteはコードをJavaScriptへとコンパイルできるのか
  • なぜSvelteの中ではテンプレートエンジンのような構文({#if}、{#await} など)が使えるのか、一般的なテンプレートエンジンの構文と何が違うのか

はじめに

最終的なコードを生成するために、Svelteはコンポーネントを一度コンパイルして必要な情報を取得する必要がある。Svelteのコンパイルからコード生成までは、主に以下のフェーズを経る:

  • JavaScript、HTML + Svelteテンプレート構文、CSS構文をASTにパースする
    • JavaScript(<script>内や{}で囲まれた部分)をacornでASTにパースする
    • HTML + Svelteテンプレート構文({#if}、{variable}など)を自作のパーサーでASTにパースする
    • CSS構文をcsstreeでASTにパースする
  • new Component(ast)を呼び出してSvelteコンポーネントを生成する。コンポーネント内には主にinstance、fragment、varsなどの情報が含まれる(src/compiler/compile/Component.ts)
  • renderer.renderを呼び出してjsとcssを生成する

実際のフローや処理は上記よりもはるかに複雑だ(intro、outroの制御、イベントリスナー、変数の追跡など)。フロー全体はこの図を参考にするといい:

Svelte Compile flow

1. ソースコードをASTにパースする

Svelteはまずコンポーネントを3つの主要部分に分解する:HTML(およびSvelte構文)、CSS、そしてJavaScriptであり、それぞれ異なるパーサーでパースされる。

Svelteコンポーネントがパースされた後にどのようになるかを知りたい場合は、AST Explorerで確認できる:

<script>
  let count = 0;
  count++;
</script>

<style>
  p {
    font-size: 14px;
  }
</style>

<p>count is {count}</p>

生成された構文木(右半分):

Screenshot_2021-02-07 AST explorer(3)

パース後はhtml、css、instanceという3つのASTが生成されることがわかる。ここでinstanceは<script>の中に含まれるJavaScriptコードを指している。

2. Svelteコンポーネント(Component)の生成

svelte

このフェーズで、SvelteはASTの中から必要な情報をComponentというクラスの中に保持する。これにはコンポーネントのHTML(Svelte内ではfragmentと名付けられている)、宣言された変数、instanceのASTなどが含まれる。

その後、instance(つまり上図の<script>で囲まれた部分)を走査して、すべての変数の使用状況を把握する。この時点で、変数が宣言されたものの未使用であるかどうかや、$プレフィックスを持つ処理すべき変数があるかどうかをすでに検出できる。

続いてHTML部分の走査を開始し、fragmentを構築する。この部分は、Svelteのコンパイルにおいて最もコアなロジックの一つと言える。Fragmentは多くの種類に分類でき、通常のHTMLタグや、ifやawaitといったSvelte構文などが含まれる。

// https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/map_children.ts
function get_constructor(type) {
	switch (type) {
		case 'AwaitBlock': return AwaitBlock;
		case 'Body': return Body;
		case 'Comment': return Comment;
		case 'EachBlock': return EachBlock;
		case 'Element': return Element;
		case 'Head': return Head;
		case 'IfBlock': return IfBlock;
		case 'InlineComponent': return InlineComponent;
		case 'KeyBlock': return KeyBlock;
		case 'MustacheTag': return MustacheTag;
		case 'Options': return Options;
		case 'RawMustacheTag': return RawMustacheTag;
		case 'DebugTag': return DebugTag;
		case 'Slot': return Slot;
		case 'Text': return Text;
		case 'Title': return Title;
		case 'Window': return Window;
		default: throw new Error(`Not implemented: ${type}`);
	}
}

扱いやすくするために、Svelteは異なるタイプのfragmentごとにクラスを別途定義している。それぞれのクラスの実装についてはここでは詳しく展開しないが、いくつか例を挙げる:

  • Elementは通常のHTMLタグに対応し、イベントハンドラーや属性チェック、a11yチェックなどを処理する。
    • 例えばtagNameがaなのにhrefが付いていない場合に警告を出す(ソースコード)
  • IfBlockは{#if}や{:else}の構文の処理を担当する
  • EachBlockは{#each}の構文の処理を担当する

その後、Svelteは対応するCSSにハッシュを付与して命名の衝突を防ぎ、cssスタイルを生成する。

3. fragmentとblockの構築

いよいよコード生成のフェーズに到達した。コード生成のロジック全体はsrc/compiler/compile/render_dom/Renderer.tsにある(Svelteは現在がSSRかDOMかに応じてレンダラーを選択するが、ここではDOMを例とする)。

まずfragmentを構築し、Wrapper内のrender関数を使って生成するコードの内容を定義する。例えばText.tsはテキスト生成の部分を担当している。fragmentは子ノードを走査し続け、render関数を呼び出して対応するコードを生成し、コードスニペットをblockの中に入れていく。

次にblockを宣言する。blockの中には非常に多くのコードフラグメント(マウント時やアンマウント時に生成されるべきコードなど)があり、最終的にcreate_fragment関数を構築するために使用される。この部分はSvelteの中で最もコアであり、最も複雑な部分と言える。実装全体はsrc/compiler/render_dom/index.tsで確認できる。

コード生成の部分には、作者のRich Harrisが作成したcode-redが使われており、コード生成を容易にしている。このライブラリのユニークな点は、var a = 1のような記述で直接対応するASTノードを生成できるところだ。例えば例の中のvariableAは、実際にはVariableDeclarationのノードになる。さらにテンプレートリテラル構文を組み合わせることで、手軽にコードを生成できる。

コード生成の部分については、IT鐵人賽の動画「生成元件程式碼」も参考になる:

例えば、僕が動的にadd関数を生成したい場合はこのように書ける:

最後にprintというAPIを通して構文木をコードへと変換する。次にSvelte内での実装を見てみよう(EachBlock.tsを例にする。他の生成ロジックは比較的複雑だ)。コードを生成するソースコードがどのように書かれているかを確認してみる:

// 在元件 create 時呼叫 each_block_else.c()
block.chunks.create.push(b`
  if (${each_block_else}) {
    ${each_block_else}.c();
  }
`);

if (this.renderer.options.hydratable) {
  block.chunks.claim.push(b`
    if (${each_block_else}) {
      ${each_block_else}.l(${parent_nodes});
    }
  `);
}

// 在元件 mount 時呼叫 each_block_else.m()
block.chunks.mount.push(b`
  if (${each_block_else}) {
    ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
  }
`);

中で呼び出されているbこそがcode-redのAPIの一つであり、これらが最終的に生成されるコードとなる。

なぜSvelteはコードをJavaScriptにコンパイルできるのか?

本記事の冒頭の疑問に戻ると、Svelteはコードを事前にコンパイル・解析するため、.svelteをJavaScriptにコンパイルできるのだ。

なぜSvelteの中ではテンプレートエンジンのような構文が使え、一般的なテンプレートエンジンと何が違うのか?

Svelteはカスタムパーサーを実装しており、通常のHTMLをパースするだけでなく{}の中の構文もパースでき、上述したコンパイルプロセスを経て対応するJavaScriptコードを生成するからだ。一般的なテンプレートエンジンの構文との違いは、**Svelteの構文はリアクティブ(reactive)であるのに対し、一般的なテンプレートエンジンの構文は静的なHTMLである点だ。**erbを例に挙げると:

<% unless content.empty? %>
  <div>
    <%= content.text %>
  </div>
<% end %>

このような構文は通常、バックエンドでHTMLがレンダリングされた後に返される。しかしSvelteにおける構文:

{#if content}
  <div>
    {content.text}  
  </div>
{/if}

これはcontentが空でなくなった際に、画面をリアクティブに更新する。

まとめ

本記事では、コードをASTにパースし、fragmentと対応するノードを構築し、最終的にレンダラーによってコードを生成するまでという、Svelteのコンパイルからコード生成までの大まかなフローを説明してきた。細かいディテールや実装にはあまり深く踏み込まなかったが、今後の記事でより深く掘り下げていく予定だ。本記事を通して、Svelteがコードを生成するプロセスについて読者のみんなが一定の理解を得られたなら幸いだ。

参考リソース

関連記事

他のトピックを探索