スベルテの深い理解 (1) — スヴェルテのコンパイルプロセス

作成者:カランカラン
💡

質問やフィードバックがありましたら、フォームからお願いします

本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください

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

この記事を読む前に、読者はSvelteまたは他のフロントエンドフレームワークの使用経験があり、実装原理に興味があることを期待しています。

Svelteはどのようにコンパイルされるか(0)— 抽象構文木とは何か?」をまだ読んでいない場合は、まずそちらをお読みください。

この記事では、以下のいくつかの質問に答えたいと思います:

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

前書き

最終的なコードを生成するために、Svelteはコンポーネントを一度コンパイルして必要な情報を取得する必要があります。Svelteのコンパイルプロセスは、コード生成まで主にいくつかの段階を経て進行します:

  • JavaScript、HTML + Svelteテンプレート構文、CSS構文をASTに解析する
    • <script>内のJavaScript{}で囲まれた部分)をacronを用いてASTに解析
    • HTML + Svelteテンプレート構文({#if}{variable}など)を自作のパーサーを使ってASTに解析
    • CSS構文をcsstreeを用いてASTに解析
  • new Component(ast)を呼び出してSvelteコンポーネントを生成し、コンポーネントには主にinstancefragmentvarsなどの情報が含まれる(src/compiler/compile/Component.ts
  • renderer.renderを呼び出してjscssを生成

実際のプロセスと処理は、上記よりもはるかに複雑です(イントロ、アウトロの制御、イベントリスニング、変数追跡など)。全体のプロセスは以下の図を参考にできます:

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)

解析後にはhtmlcssinstanceの3つのASTが生成され、instance<script>内に包まれたJavaScriptコードを指します。

2. Svelteコンポーネント(Component)を生成する

svelte

この段階で、SvelteはAST内の必要な情報Componentというクラスに格納します。これにはコンポーネントのHTML(Svelteではfragmentとして命名)、宣言された変数、instanceのASTなどが含まれます。

その後、instance(上の図で<script>で囲まれた部分)を走査して、すべての変数の使用状況を把握します。この時点で、宣言されたが未使用の変数や、$で接頭辞が付けられた変数を処理する必要があるかどうかを検出できます。

次に、HTML部分を走査し、fragmentを構築します。この部分はSvelteコンパイルの中で最も核心的なロジックの1つと言えます。Fragmentは通常のHTMLタグ、Svelteの構文(if、awaitなど)に分類されます。

// 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}`);
	}
}

異なるタイプのfragmentは、処理を容易にするためにSvelteによって追加のクラスとして作成されます。各クラスの実装についてはここで詳しい説明はしませんが、いくつかの例を挙げます:

  • Elementは通常のHTMLタグに対応し、イベントハンドラや属性検査、アクセシビリティ検査を処理します。
    • 例えば、tagNameがahrefが指定されていない場合は警告が出ます(原始コード
  • IfBlock{#if}および{:else}の構文を処理します
  • EachBlock{#each}の構文を処理します

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

3. fragmentとblocksの作成

ついにコード生成の段階に到達しました。コード生成のロジックはsrc/compiler/render_dom/Renderer.tsに記述されています(Svelteは現在がSSRかDOMかに応じてレンダラーを選択しますが、ここではDOMの例を挙げます)。

まず、fragmentを作成し、Wrapper内のrender関数で生成されるコードの内容を定義します。例えば、Text.tsはテキスト生成の部分を担当します。fragmentは子ノードを再帰的に走査し、render関数を呼び出して対応するコードを生成し、そのコードスニペットをブロック内に配置します。

次に、ブロックを宣言し、block内には非常に多くのコードスニペット(例えば、マウント時やアンマウント時に生成されるコード)が含まれ、最終的にはcreate_fragment関数を構築するために使用されます。この部分はSvelteの中で最も核心であり、最も複雑な部分と言えます。全体の実装はsrc/compiler/render_dom/index.tsで確認できます。

コード生成部分では、著者Rich Harrisが作成したcode-redを用いて生成が行われます。このライブラリの特長は、var a = 1のような書き方で直接対応するASTノードを生成できることです。例えば、サンプル内のvariableAは実際にはVariableDeclarationノードに変わります。また、テンプレートリテラル構文を使用して、コード生成を容易に行うことができます。

コード生成に関する部分は、IT鉄人戦の動画 — コンポーネントコード生成を参照してください。

https://youtu.be/lxd6vsmL7RY

例えば、動的にadd関数を生成したい場合は、以下のように書けます:

最後に、printというAPIを通じて構文木をコードに変換します。次に、Svelte内の実装を見てみましょう(EachBlock.tsを例にして、他の生成は相対的に複雑です)、生成されたコードのソースは次のように記述されています:

// コンポーネントの作成時に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});
    }
  `);
}

// コンポーネントのマウント時にeach_block_else.m()を呼び出す
block.chunks.mount.push(b`
  if (${each_block_else}) {
    ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
  }
`);

ここで呼び出されるbcode-redのAPIの1つで、これらは最終的に生成されるコードです。

なぜ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が空でない場合に画面を更新します。

まとめ

この記事では、Svelteがコンパイルからコード生成までの大まかなプロセスを説明し、コードをASTに解析し、fragmentと対応するノードを構築し、最終的にrendererを通じてコードを生成する流れを解説しました。詳細や実装についてはあまり触れませんでしたが、今後の記事で深く掘り下げていく予定です。読者の皆さんがこの記事を読んだ後に、Svelteのコード生成の過程について一定の理解を得られることを願っています。

参考リソース

この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨

Buy me a coffee