質問やフィードバックがありましたら、フォームからお願いします
本文は台湾華語で、ChatGPT で翻訳している記事なので、不確かな部分や間違いがあるかもしれません。ご了承ください
Svelteを深く理解する(1)— Svelteのコンパイルプロセス
この記事を読む前に、読者はSvelteまたは他のフロントエンドフレームワークの使用経験があり、実装原理に興味があることを期待しています。
「 Svelteはどのようにコンパイルされるか(0)— 抽象構文木とは何か?」をまだ読んでいない場合は、まずそちらをお読みください。
この記事では、以下のいくつかの質問に答えたいと思います:
- なぜSvelteはコードをJavaScriptにコンパイルできるのか
- なぜSvelteではテンプレートエンジンのような構文({#if} {#await}など)が使用でき、一般的なテンプレートエンジンの構文と何が異なるのか
前書き
最終的なコードを生成するために、Svelteはコンポーネントを一度コンパイルして必要な情報を取得する必要があります。Svelteのコンパイルプロセスは、コード生成まで主にいくつかの段階を経て進行します:
- JavaScript、HTML + Svelteテンプレート構文、CSS構文をASTに解析する
new Component(ast)
を呼び出してSvelte
コンポーネントを生成し、コンポーネントには主にinstance
、fragment
、vars
などの情報が含まれる(src/compiler/compile/Component.ts)renderer.render
を呼び出してjs
とcss
を生成
実際のプロセスと処理は、上記よりもはるかに複雑です(イントロ、アウトロの制御、イベントリスニング、変数追跡など)。全体のプロセスは以下の図を参考にできます:
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タグ、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が
a
でhref
が指定されていない場合は警告が出ます(原始コード)
- 例えば、tagNameが
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鉄人戦の動画 — コンポーネントコード生成を参照してください。
例えば、動的に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});
}
`);
ここで呼び出されるb
はcode-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のコード生成の過程について一定の理解を得られることを願っています。
参考リソース
- Li Hau Tan — The Svelte Compiler Handbook:Svelteのコンパイルプロセスを非常に詳細に説明しているので、ぜひ読んでみてください!
この記事が役に立ったと思ったら、下のリンクからコーヒーを奢ってくれると嬉しいです ☕ 私の普通の一日が輝かしいものになります ✨
☕Buy me a coffee