半熟前端

軟體工程師 / 台灣人 / 在前端的路上一邊探索其他領域的可能性

前端

深入理解 Svelte(1)— Svelte 編譯過程

深入理解 Svelte(1)— Svelte 編譯過程

深入理解 Svelte(1)— Svelte 編譯過程

在閱讀本篇文章之前,預期讀者已有 Svelte 或其他前端框架使用經驗,並且對實作原理有興趣。

如果還沒有看過「 Svelte 如何編譯(0)— 什麼是抽象語法樹?」,建議先閱讀後再來閱讀本篇

今天這篇文章想要回答幾個問題:

  • 為什麼 Svelte 可以將程式碼編譯為 JavaScript
  • 為什麼在 Svelte 中可以使用類似模板引擎的語法({#if} {#await} 等),與一般模板引擎的語法有什麼不同

前言

為了生成最後的程式碼,Svelte 必須將元件編譯一次獲取必要資訊,Svelte 的編譯過程到生成程式碼主要會通過幾個階段:

  • 將 JavaScript、HTML + Svelte 模板語法、CSS 語法解析為 AST

    • JavaScript (包在 <script> 裡與 {})以 acron 解析為 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 Compile flow

1. 將原始碼解析為 AST

Svelte 首先會將元件拆為三大部分: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 三個 AST,其中 instance 指得是包在 <script> 當中的 JavaScript 程式碼。

2. 生成 Svelte 元件(Component)

svelte

這個階段 Svelte 會將 AST 當中必要的資訊存放在 Component 這個 class 當中,其中包含了元件的 HTML(在 svelte 當中用 fragment 命名)、宣告的變數、instance 的 AST 等等。

之後會遍歷 instance(也就是上圖中 <script> 包起來的部分),得知所有變數的使用狀況,這個時候已經可以偵測變數是否宣告了但未使用,是否有 $ 當作前綴的變數需要處理。

之後會開始遍歷 HTML 的部分,並且建立一個 fragment。這部分可以說是 Svelte 編譯當中最核心的邏輯之一。Fragment 可以分為很多種類,包含了一般的 HTML Tag、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 都會額外建立一個 class。 對於每個 class 的實作不額外展開做說明。在這邊舉幾個例子:

  • Element 對應一般的 HTML 標籤,會處理像是 event handler、attribute 檢查、a11y 檢查。

    • 比如當 tagName 為 a 但沒有加上 href 時會跳出警告(原始碼
  • IfBlock 負責處理 {#if} {:else} 的語法
  • EachBlock 負責處理 {#each} 的語法

之後 Svelte 會將對應的 CSS 加上 hash 防止命名衝突,進而生成 css 樣式。

3. 建立 fragment 與 blocks

終於來到生成程式碼的階段了,整個生成程式碼的邏輯在 src/compiler/render_dom/Renderer.ts 當中(Svelte 會根據現在是 SSR 還是 dom 選擇 renderer,這邊以 dom 作舉例)。

首先會建立一個 fragment,並且用 Wrapper 中的 render 函數定義生成程式碼的內容。比如 Text.ts 就負責處理文字生成的部分。fragment 會不斷遍歷子節點並呼叫 render 函數產生對應程式碼,並且將程式碼片段放到 block 裡頭。

之後會宣告 block,block 裡頭有非常多 code fragment(例如 mount, unmount 時要生出的程式碼),最後會被用來建立 create_fragment 函數。這部分可以說是 Svelte 裡最核心也最複雜的地方。整個實作可以到 src/compiler/render_dom/index.ts 當中查看。

生成程式碼的部分使用了作者 Rich Harris 寫的 code-red 來方便生成。這個函式庫的特別之處在於可以用 var a = 1 這樣的寫法直接產生對應的 AST 節點。比如說範例中的 variableA 實際上會變成一個 VariableDeclaration 的節點。還可以透過 template literal 語法組合方便生成程式碼。

關於生成程式碼的部分可以參考 IT 鐵人賽影片 — 生成元件程式碼

https://youtu.be/lxd6vsmL7RY

比方說我想要動態生成一個 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 不為空的時候更新畫面。

總結

這篇文章試著闡述 Svelte 從編譯到程式碼生成大致的流程,包含解析程式碼為 AST,建立 fragment 與對應節點,最後透過 renderer 產生程式碼。對於細節與實作沒有著墨太多,會在之後的文章中深入探討。希望讀者們看完這篇文章後可以對 Svelte 生成程式碼的過程有一定的理解。

參考資源