protonvpn

在 Eleventy(11ty)中優雅地使用 Svelte:一套實用的 Vite-based islands 架構

你應該試試看 Svelte,用在小元件很剛好

執行範例

這是用 svelte 寫的元件,整合在 markdown file.

為什麼我想在 11ty 裡用 Svelte?

Eleventy(11ty)是一個我非常喜歡的靜態網站產生器:

  • 簡單
  • 不綁框架
  • 非常適合寫技術文章

但寫久了總會遇到一個問題:

文章裡想放一點互動的東西

例如:

  • 小型計算器
  • Demo widget
  • 可重複使用的互動範例
  • 教學用的即時操作元件

這時候我並不想:

  • 把整個網站搬去 SvelteKit
  • 或為了一個元件引入一整套 SPA 架構

於是我選擇了一條折衷但非常實用的路:

11ty 負責靜態內容
Svelte 只負責「局部互動」

這篇文章會介紹什麼?

這篇會完整說明我目前在用的一套做法:

  • 在 11ty 文章中用 shortcode 插入 Svelte 元件
  • Svelte 元件集中管理、每個元件一個檔案
  • 透過 Vite 自動打包整合到 11ty build
  • 支援 Barba.js 無刷新換頁
  • 最後會比較這套做法和 11ty is-land 的差異

整合步驟

整個流程其實很單純:

  1. Markdown 文章中放一個「Svelte 插槽」
  2. 11ty build 時輸出普通 HTML
  3. 瀏覽器載入後:
    • JS 掃描插槽
    • 找到對應的 Svelte 元件
    • mount() 掛載上去
Markdown
  ↓
HTML(含 data-svelte-component 插槽)
  ↓
Vite bundle(含 Svelte)
  ↓
Client-side mount

專案目錄結構: 把 Svelte 元件集中管理:

src/
├─ svelte/            # 專門放 Svelte 元件
│  ├─ Counter.svelte
│  └─ MyWidget.svelte
└─ assets/
   └─ js/
      ├─ site.js
      └─ svelte-loader.js

好處很明確:

  • 元件與文章內容完全解耦
  • 同一個元件可以在不同文章重複使用
  • 不會污染 11ty 的模板系統

Step 1:在 11ty 中啟用 Svelte(Vite)

我使用的是官方的 plugin:

  • @11ty/eleventy-plugin-vite
  • @sveltejs/vite-plugin-svelte

修改 eleventy.config.js

import EleventyVitePlugin from "@11ty/eleventy-plugin-vite"; // 11ty 官方 Vite 插件
import { svelte } from "@sveltejs/vite-plugin-svelte"; // Svelte Vite 插件

export default async function (eleventyConfig) {
  eleventyConfig.addPlugin(EleventyVitePlugin, { // 啟用 11ty Vite 插件
    viteOptions: {
      plugins: [
        svelte(), // 讓 Vite 能編譯 .svelte 檔案
      ],
    },
  });

  return {}; // 返回 11ty 配置物件
}

這裡的重點觀念是:

11ty 不負責編譯 Svelte
Svelte 完全交給 Vite

Step 2:建立一個 Svelte shortcode(關鍵)

這一步是整個架構的靈魂,也沒那麼誇張啦,就是使用 11ty shortcode 產生 html 放 svelte 元件位置

修改 eleventy.config.js

eleventyConfig.addShortcode("svelte", (name, props = {}) => { // 註冊 "svelte" shortcode
  const propsString = JSON.stringify(props) // 將 props 物件序列化為 JSON 字串
    .replace(/'/g, "'"); // 將單引號轉義為 HTML 實體,避免 HTML 屬性解析錯誤

  return ` // 返回 HTML 字串,創建一個空的 div 作為 Svelte 元件掛載點
    <div
      data-svelte-component="${name}" // 指定要掛載的 Svelte 元件名稱
      data-svelte-props='${propsString}'> // 以 JSON 格式傳遞 props 給元件
    </div>
  `;
});

這個 shortcode 做的事情只有一個:

在 HTML 中留下「我之後要掛哪個 Svelte 元件」的資訊

Step 3:在 Markdown 文章中使用

放在文章任何位置都行。


{% svelte "Counter", { "count": 10, "label": "目前點擊數" } %} 

<!-- 使用 svelte shortcode 插入 Counter 元件,並傳遞初始 props -->

此時請記住一件事:

這裡不會渲染 Svelte 只會產生 shortcode 設定的 HTML,例如:

<div
  data-svelte-component="Counter"
  data-svelte-props='{"count":10,"label":"目前點擊數"}'>
</div>

Step 4:撰寫一個 Svelte 元件

ex: Counter.svelte

<script>
  let { count = 0, label = "Count" } = $props(); // 解構 props,設定預設值
</script>

<div class="p-4 border rounded my-4"> <!-- 使用 Tailwind CSS 樣式 -->
  <p>{label}: {count}</p> <!-- 顯示標籤和計數值 -->

  <button onclick={() => count--}>-1</button> <!-- 減少按鈕 -->
  <button onclick={() => count++}>+1</button> <!-- 增加按鈕 -->
</div>

這是一個非常普通的 Svelte 元件,沒有任何 11ty 耦合。

Step 5:Client-side Svelte Loader(核心)

基本版本(全部一起打包)

ex: svelte-loader.js

import { mount } from "svelte"; // Svelte 5:掛載元件

const modules = import.meta.glob("../../svelte/*.svelte", { eager: true }); // 小元件:直接全打包
const components = {}; // { ComponentName: Component }

for (const filePath in modules) { // "../../svelte/Counter.svelte"
  const name = filePath.split("/").pop().replace(".svelte", ""); // "Counter"
  components[name] = modules[filePath].default; // 註冊元件
}

export function initSvelteComponents(root = document) { // 允許傳入 Barba container
  root.querySelectorAll("[data-svelte-component]").forEach((el) => {
    if (el.dataset.svelteInitialized) return; // 已初始化就跳過

    const name = el.dataset.svelteComponent; // 元件名稱
    const Component = components[name]; // 取得元件

    if (!Component) { // 以前你這邊是 return(太安靜)
      console.warn(`[svelte] component not found: "${name}"`);
      return;
    }

    let props = {};
    const raw = el.dataset.svelteProps || "{}"; // props 字串(預設空物件)

    try {
      props = JSON.parse(raw); // parse props JSON
    } catch (err) {
      console.warn(`[svelte] invalid props JSON for "${name}": ${raw}`, err); // 以前你是吞掉
      props = {};
    }

    mount(Component, { target: el, props }); // 掛載元件
    el.dataset.svelteInitialized = "true"; // 標記完成
  });
}

這裡利用了 Vite 的 import.meta.glob

  • 自動掃描 src/svelte/
  • 不需要手動 import 每個元件
  • 新增元件只需要放檔案即可

Step 6:整合 Barba.js

如果你使用 Barba.js 或 PJAX 類工具,這一步非常必要:

import { initSvelteComponents } from "./svelte-loader.js"; // 匯入 Svelte 元件初始化函數

document.addEventListener("DOMContentLoaded", () => { // 頁面首次載入時
  initSvelteComponents(); // 初始化所有 Svelte 元件
});

barba.hooks.afterEnter(() => { // Barba.js 換頁完成後
  initSvelteComponents(); // 重新初始化新頁面的 Svelte 元件
});

原因很簡單:

Barba 換頁不會重新載入 JS,但新頁面需要重新掛載 Svelte


那這套和 11ty is-land 有什麼不同?

簡單對照

項目本文做法11ty is-land
架構CSR islandsSSR + hydration
初始 HTML插槽是空的可預渲染
SEO不太好較佳
載入策略手動控制宣告式(on:visible)
Barba 相容非常好需額外處理
複雜度不好說不好說

實務建議

  • 部落格互動元件 → 本文這套
  • SEO 關鍵內容元件 → is-land
  • 已有 Vite / Barba 架構 → 本文這套會非常順

大多數使用 Svelte 的目的應該是為了互動操作,如果是為了 SEO 產生內容,那這樣用 Svelte 就不太適合了。

結語

這套做法本質上是一種:簡單可控的 islands 架構

它不追求「什麼都幫你包好」,而是讓你清楚知道:

  • 元件什麼時候被載入
  • JS 什麼時候被執行
  • 換頁時發生了什麼事

如果你正在用 11ty 寫技術部落格,又偶爾需要一點互動,svelte 是一個還不錯的方式。