Svelteの深い理解(1)- Svelteのコンパイルプロセス
この記事を読む前に、読者はSvelteまたは他のフロントエンドフレームワークの使用経験があり、実装原理に興味を持っていることを前提としています。
「Svelteのコンパイル方法(0)- 抽象構文木とは何ですか?」をまだ読んでいない場合は、先に読んでからこの記事を読むことをおすすめします。
この記事では、次のいくつかの質問に答えたいと思います:
- なぜSvelteはコードをJavaScriptにコンパイルできるのか
- Svelteではテンプレートエンジンのような文法({#if} {#await}など)を使用できる理由と、一般的なテンプレートエンジンとの違いは何か
序文
最終的なコードを生成するために、Svelteはコンポーネントを1回コンパイルして必要な情報を取得する必要があります。Svelteのコンパイルプロセスは、コードの生成までの間にいくつかのステージを経ます。
- JavaScript、HTML + Svelteテンプレート構文、CSS構文をASTに解析する
new Component(ast)
を呼び出してSvelte
コンポーネントを生成する。コンポーネントにはinstance
、fragment
、vars
などの情報が含まれます(src/compiler/compile/Component.ts)renderer.render
を呼び出してjs
とcss
を生成する
実際のフローと処理は、上記よりもはるかに複雑です(intro、outroの制御、イベントリスナー、変数トラッキングなど)。全体のフローは、次の図を参照してください:
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>
生成された抽象構文木(右側):
解析後、html
、css
、instance
の3つのASTが生成されます。instance
は、<script>
タグ内に含まれるJavaScriptコードを指します。
2. Svelteコンポーネント(Component)を生成する
このステージでは、SvelteはASTの中で必要な情報をComponent
クラスに格納します。これには、コンポーネントのHTML(Svelteではfragment
と呼ばれる)、宣言された変数、instanceのASTなどが含まれます。
次に、instance
(上記の図で<script>
で囲まれた部分)を走査し、すべての変数の使用状況を把握します。この時点で、変数が宣言されているが使用されていないか、変数に$
がプレフィックスとして付いているかどうかなどを検出できます。
その後、HTMLの部分を走査し、fragment
を作成します。これは、Svelteコンパイルの中でも最も重要なロジックの1つです。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}`);
}
}
異なるタイプの各fragment
には、Svelteが処理を容易にするために個別にクラスが作成されます。各クラスの実装の詳細には触れませんが、いくつかの例を挙げます:
Element
は通常のHTMLタグに対応し、イベントハンドラや属性のチェック、a11yのチェックなどを処理します。- たとえば、tagNameが
a
でありhref
が指定されていない場合、警告が表示されます(ソースコード)
- たとえば、tagNameが
IfBlock
は{#if}
{:else}
の構文を処理しますEachBlock
は{#each}
の構文を処理します
その後、Svelteは対応するCSSにハッシュを追加して名前の衝突を防ぎ、css
スタイルを生成します。
3. fragmentとblocksを作成する
ついにコード生成のステージに入ります。コード生成のロジックは、src/compiler/render_dom/Renderer.tsにあります(SvelteはSSRかDOMかに応じてrendererを選択するため、ここではDOMを例に説明します)。
最初に、Wrapper
内のrender
関数を使用してfragmentを作成します。たとえば、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
メソッドを使用して構文木をコードに変換します。次に、Svelteの実装を見てみましょう(EachBlock.tsを例に挙げますが、他の生成は比較的複雑です)。生成されるコードの元のソースコードを見てみましょう:
// each_block_else.c()をコンポーネントの作成時に呼び出す
block.chunks.create.push(b`
if (${each_block_else}) {
${each_block_else}.c();
}
`);
// SSRの場合は、each_block_else.l()をclaim時に呼び出す
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`
if (${each_block_else}) {
${each_block_else}.l(${parent_nodes});
}
`);
}
// each_block_else.m()をmount時に呼び出す
block.chunks.mount.push(b`
if (${each_block_else}) {
${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
}
`);
ここで呼び出されているb
は、code-red
のAPIの1つであり、これらが実際に生成されるコードです。
なぜSvelteはコードをJavaScriptにコンパイルできるのか?
この記事の冒頭での質問に戻りますが、Svelteはコードを事前にコンパイルし、解析するため、.svelte
ファイルをJavaScript
にコンパイルできるのです。
Svelteではなぜテンプレートエンジンのような文法を使用できるのか、一般的なテンプレートエンジンとはどのように異なるのか
Svelteは独自のパーサーを実装しており、通常のHTMLだけでなく、{}
の中の文法も解析することができます。そして、上記で説明したコンパイルフローを経て、対応するJavaScript
コードを生成します。一般的なテンプレートエンジンとの違いは、Svelteの文法はリアクティブ
であり、一般的なテンプレートエンジンの文法は静的な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のコード生成プロセスについてある程度理解していただければ幸いです。
参考資料
- Li Hau Tan - The Svelte Compiler Handbook: Svelteのコンパイルプロセスについて非常に詳細に説明されていますので、ぜひ読んでみてください!