カランのブログ

ソフトウェアエンジニア / 台湾人 / 福岡生活

今のモード ライト

Svelteの深い理解(1)- Svelteのコンパイルプロセス

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

Svelteのコンパイル方法(0)- 抽象構文木とは何ですか?」をまだ読んでいない場合は、先に読んでからこの記事を読むことをおすすめします。

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

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

序文

最終的なコードを生成するために、Svelteはコンポーネントを1回コンパイルして必要な情報を取得する必要があります。Svelteのコンパイルプロセスは、コードの生成までの間にいくつかのステージを経ます。

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

実際のフローと処理は、上記よりもはるかに複雑です(intro、outroの制御、イベントリスナー、変数トラッキングなど)。全体のフローは、次の図を参照してください:

Svelteのコンパイルフロー

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タグや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が指定されていない場合、警告が表示されます(ソースコード
  • 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鐵人賽のビデオ「生成元件程式碼」を参照してください。

https://youtu.be/lxd6vsmL7RY

たとえば、動的に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のコード生成プロセスについてある程度理解していただければ幸いです。

参考資料

次の記事

Svelte (0) についての深い理解—抽象構文ツリーとは?

前の記事

2022年にスヴェルトを学ぶべき4つの理由

この文章が役に立つと思うなら、下のリンクで応援してくれると大変嬉しいです✨

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Kalan です。台湾出身で、2019年に日本へ転職し、福岡に住んでいます。フロントエンド開発に精通しているだけでなく、IoT、アプリ開発、バックエンド、電子工作などの分野にも挑戦しています。 最近、エレキギターを始めました。ブログを通じて、より多くの人と交流できればと思っています。気軽に絡んでください