Stories

Detail Return Return

SvelteKit 最新中文文檔教程(18)—— 淺層路由和 Packaging - Stories Detail

前言

Svelte,一個語法簡潔、入門容易,面向未來的前端框架。

從 Svelte 誕生之初,就備受開發者的喜愛,根據統計,從 2019 年到 2024 年,連續 6 年一直是開發者最感興趣的前端框架 No.1

image.png

Svelte 以其獨特的編譯時優化機制著稱,具有輕量級高性能易上手等特性,非常適合構建輕量級 Web 項目

為了幫助大家學習 Svelte,我同時搭建了 Svelte 最新的中文文檔站點。

如果需要進階學習,也可以入手我的小冊《Svelte 開發指南》,語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

淺層路由

當您在 SvelteKit 應用中導航時,您會創建歷史記錄條目。點擊後退和前進按鈕會遍歷這個條目列表,重新運行所有 load 函數,並在必要時替換頁面組件。

有時,在不導航的情況下創建歷史條目是有用的。例如,您可能想要顯示一個模態對話框,用户可以通過返回導航來關閉它。這在移動設備上特別有價值,因為滑動手勢通常比直接與 UI 交互更自然。在這些情況下,沒有關聯歷史記錄條目的模態可能會令人沮喪,因為用户可能會嘗試向後滑動來關閉它,卻發現自己到了錯誤的頁面。

SvelteKit 通過 pushStatereplaceState 函數使這成為可能,這些函數允許您在不進行導航的情況下將狀態與歷史記錄條目關聯。例如,要實現一個由歷史驅動的模態:

<!--- file: +page.svelte --->
<script>
  import { pushState } from '$app/navigation';
  import { page } from '$app/state';
  import Modal from './Modal.svelte';

  function showModal() {
    pushState('', {
      showModal: true
    });
  }
</script>

{#if page.state.showModal}
  <Modal close={() => history.back()} />
{/if}

模態框可以通過返回導航(取消設置 page.state.showModal)或通過交互觸發 close 回調運行來關閉。

API

pushState 的第一個參數是相對於當前 URL 的 URL。要保持在當前 URL,使用 ''

第二個參數是新的頁面狀態,可以通過 page 對象 作為 page.state 訪問。您可以通過聲明 App.PageState 接口(通常在 src/app.d.ts 中)來使頁面狀態類型安全。

要設置頁面狀態而不創建新的歷史記錄條目,請使用 replaceState 而不是 pushState

[!舊版説明] > $app/state 中的 page.state 是在 SvelteKit 2.12 中添加的。如果您使用的是較早版本或正在使用 Svelte 4,請使用 $app/stores 中的 $page.state

為路由加載數據

在進行淺層路由時,您可能想在當前頁面內渲染另一個 +page.svelte。例如,點擊照片縮略圖可以彈出詳細視圖,而不需要導航到照片頁面。

為此,您需要加載 +page.svelte 所需的數據。一個便捷的方法是在 <a> 元素的 click 處理程序中使用 preloadData。如果元素(或其父元素)使用 data-sveltekit-preload-data,數據將已經被請求,preloadData 將複用該請求。

<!--- file: src/routes/photos/+page.svelte --->
<script>
  import { preloadData, pushState, goto } from '$app/navigation';
  import { page } from '$app/state';
  import Modal from './Modal.svelte';
  import PhotoPage from './[id]/+page.svelte';

  let { data } = $props();
</script>

{#each data.thumbnails as thumbnail}
  <a
    href="/photos/{thumbnail.id}"
    onclick={async (e) => {
      if (innerWidth < 640        // 如果屏幕太小則退出
        || e.shiftKey             // 或鏈接在新窗口中打開
        || e.metaKey || e.ctrlKey // 或新標籤頁中打開 (mac: metaKey, win/linux: ctrlKey)
        // 也應考慮鼠標滾輪點擊
      ) return;

      // 阻止導航
      e.preventDefault();

      const { href } = e.currentTarget;

      // 運行 `load` 函數(或者説,獲取由於 `data-sveltekit-preload-data`
      // 而已經在運行的 `load` 函數的結果)
      const result = await preloadData(href);

      if (result.type === 'loaded' && result.status === 200) {
        pushState(href, { selected: result.data });
      } else {
        // 出現問題!嘗試導航
        goto(href);
      }
    }}
  >
    <img alt={thumbnail.alt} src={thumbnail.src} />
  </a>
{/each}

{#if page.state.selected}
  <Modal onclose={() => history.back()}>
    <!-- 將頁面數據傳遞給 +page.svelte 組件,
         就像 SvelteKit 在導航時那樣 -->
    <PhotoPage data={page.state.selected} />
  </Modal>
{/if}

注意事項

在服務端渲染期間,page.state 始終是一個空對象。對於用户首次訪問的頁面也是如此 — 如果用户重新加載頁面(或從另一個文檔返回),狀態將不會應用,直到他們進行導航。

淺層路由是一個需要 JavaScript 才能工作的功能。在使用它時要謹慎,並嘗試考慮在 JavaScript 不可用時的合理後備行為。

Packaging

您可以使用 SvelteKit 來構建應用程序和組件庫,使用 @sveltejs/package 包(npx sv create 提供了設置此功能的選項)。

在創建應用程序時,src/routes 的內容是對外公開的部分;src/lib 包含應用程序的內部庫。

組件庫的結構與 SvelteKit 應用程序完全相同,區別在於 src/lib 是對外公開的部分,而根目錄下的 package.json 用於發佈包。src/routes 可能是隨庫附帶的文檔或演示站點,也可能只是開發時使用的沙箱。

運行 @sveltejs/package 提供的 svelte-package 命令會將 src/lib 的內容生成到一個 dist 目錄中(可以配置),其中包括以下內容:

  • src/lib 中的所有文件。Svelte 組件會被預處理,TypeScript 文件會被轉譯為 JavaScript。
  • 為 Svelte、JavaScript 和 TypeScript 文件生成類型定義(d.ts 文件)。您需要安裝 typescript >= 4.0.0 來支持此功能。類型定義文件會被放置在實現文件旁邊,手動編寫的 d.ts 文件將原樣複製。您可以禁用生成,但我們強烈建議不要這樣做 —— 使用您庫的用户可能會需要這些文件來支持 TypeScript。
[!注意] @sveltejs/package 的第 1 版會生成一個 package.json。現在不再如此,它會使用項目中的 package.json 並驗證其正確性。如果您仍然使用第 1 版,請查看此 PR 獲取遷移説明。

package.json 的結構

因為您現在正在為公共使用構建一個庫,因此 package.json 的內容變得更為重要。通過它,您可以配置包的入口點、發佈到 npm 的文件以及庫的依賴。我們將逐一介紹最重要的字段。

name

這是您包的名稱,其他人可以使用該名稱安裝您的包,並可在 https://npmjs.com/package/<name> 網站上看到它。

{
    "name": "your-library"
}

在此處閲讀關於它的更多內容。

license

每個包都應有一個 license 字段,以告知人們如何使用它。目前非常流行的一種許可證是 MIT,它在分發和複用方面非常寬鬆且無需擔保。

{
    "license": "MIT"
}

在此處閲讀關於它的更多內容。請注意,應在包中包含一個 LICENSE 文件。

files

該字段告訴 npm 哪些文件將被打包並上傳到 npm。它應包含輸出文件夾(默認為 dist)。您的 package.jsonREADMELICENSE 文件會始終被包括在內,因此您不需要指定它們。

{
    "files": ["dist"]
}

要排除不必要的文件(如單元測試,或者僅從 src/routes 導入的模塊等)可以將它們添加到 .npmignore 文件中。這將導致包更小,安裝速度更快。

在此處閲讀關於它的更多內容。

exports

"exports" 字段包含包的入口點。如果您通過 npx sv create 設置了一個新的庫項目,它會設置為單一出口,即包的根目錄:

{
    "exports": {
        ".": {
            "types": "./dist/index.d.ts",
            "svelte": "./dist/index.js"
        }
    }
}

這告訴打包工具和工具鏈,您的包只有一個入口點,即根目錄,所有內容應通過以下方式導入:

// @errors: 2307
import { Something } from 'your-library';

typessvelte 鍵是導出條件,它們告訴工具在查找 your-library 導入時應引入哪個文件:

  • TypeScript 看到 types 條件,會查找類型定義文件。如果您不發佈類型定義,請忽略此條件。
  • 支持 Svelte 的工具會看到 svelte 條件,知道這是一個 Svelte 組件庫。如果您發佈的庫不導出任何 Svelte 組件,並且也可以在非 Svelte 項目中使用(如 Svelte store 庫),您可以將此條件替換為 default
[!注意] 早期版本的 @sveltejs/package 還添加了一個 package.json 導出。這不再是模板的一部分,因為所有工具都可以處理沒有明確導出的 package.json

您可以根據需要調整 exports 並提供更多入口點。例如,如果您想直接暴露 src/lib/Foo.svelte 組件而不是通過 src/lib/index.js 文件重新導出組件,您可以創建以下導出映射……

{
    "exports": {
        "./Foo.svelte": {
            "types": "./dist/Foo.svelte.d.ts",
            "svelte": "./dist/Foo.svelte"
        }
    }
}

……然後您的庫的使用者可以用如下方式導入該組件:

// @filename: ambient.d.ts
declare module 'your-library/Foo.svelte';

// @filename: index.js
// ---cut---
import Foo from 'your-library/Foo.svelte';
[!注意] 請注意,如果您提供類型定義,採用此方式可能需要額外處理。在此處閲讀關於此問題的更多詳細信息。

通常,exports 映射的每個鍵都是用户從您的包中導入某些內容的路徑。而值則是將被導入的文件的路徑或包含這些文件路徑的導出條件映射。

在此處閲讀關於 exports 的更多內容。

svelte

這是一個遺留字段,用於讓工具識別 Svelte 組件庫。如果使用 svelte 導出條件,它已不再必要,但為了向尚未了解導出條件的過時工具提供兼容性,建議保留它。它應指向您的根入口點。

{
    "svelte": "./dist/index.js"
}

sideEffects

package.json 中的 sideEffects 字段用於讓打包工具判斷模塊是否可能包含副作用。如果模塊在被導入時對其他腳本可見的行為產生變化(例如修改全局變量或內置 JavaScript 對象的原型),則視為有副作用。由於副作用可能會影響應用程序的其他部分,這些文件/模塊無論其導出是否在應用程序中使用,都會被包括在最終的打包文件中。

sideEffects 字段中指定的模塊會幫助打包工具更積極地從最終的打包文件中剔除未使用的導出(即 tree-shaking),從而生成更小更高效的打包文件。不同的打包工具以不同的方式處理 sideEffects。儘管 Vite 不需要此配置,但建議為庫聲明所有 CSS 文件具有副作用,以保持與 webpack 兼容。新創建的項目中的默認配置如下:

/// file: package.json
{
    "sideEffects": ["**/*.css"]
}
如果您的庫中的腳本存在副作用,請確保更新 sideEffects 字段。在新創建的項目中,所有腳本默認標記為無副作用。如果錯誤地將包含副作用的文件標記為沒有副作用,可能會導致功能異常。

如果您的包中有副作用的文件,可以通過數組指定這些文件:

/// file: package.json
{
    "sideEffects": ["**/*.css", "./dist/sideEffectfulFile.js"]
}

這樣只會將指定的文件視為有副作用的文件。

TypeScript

即使您自己不使用 TypeScript,也應為您的庫提供類型定義,這樣使用您庫的人可以獲得正確的智能提示。@sveltejs/package 讓生成類型的過程對您來説基本上是透明的。默認情況下,在打包您的庫時,會為 JavaScript、TypeScript 和 Svelte 文件自動生成類型定義。您只需要確保 exports 映射中的 types 條件指向正確的文件。當通過 npx sv create 初始化庫項目時,會自動設置為根導出。

然而,如果您除了根導出還有其他內容,例如提供 your-library/foo 導入,您需要額外注意提供類型定義。不幸的是,默認情況下 TypeScript 不會 為這種導出解析 types 條件,比如 { "./foo": { "types": "./dist/foo.d.ts", ... }}。相反,它會從庫的根目錄(即 your-library/foo.d.ts 而不是 your-library/dist/foo.d.ts)查找 foo.d.ts 文件。為了解決這個問題,您有兩種選擇:

第一種選擇是要求使用您庫的人在其 tsconfig.json(或 jsconfig.json)中將 moduleResolution 選項設置為 bundler(從 TypeScript 5 開始可用,未來是最佳推薦選項)、node16nodenext。這會使 TypeScript 實際查看 exports 映射並正確解析這些類型。

第二種選擇是濫用 TypeScript 的 typesVersions 特性連接類型。typesVersionspackage.json 中的一個字段,TypeScript 根據 TypeScript 版本檢查不同類型定義,同時也包含路徑映射功能。我們利用該路徑映射功能來滿足需求。對於上面提到的 foo 導出,相應的 typesVersions 定義如下:

{
    "exports": {
        "./foo": {
            "types": "./dist/foo.d.ts",
            "svelte": "./dist/foo.js"
        }
    },
    "typesVersions": {
        ">4.0": {
            "foo": ["./dist/foo.d.ts"]
        }
    }
}

>4.0 表示如果使用的 TypeScript 版本大於 4,則 TypeScript 會檢查內部映射。內部映射告訴 TypeScript your-library/foo 的類型定義在 ./dist/foo.d.ts 中,這實際上是對 exports 條件的複製。您還可以使用 * 通配符一次性提供多個類型定義而無需重複。如果選擇使用 typesVersions,您需要通過它聲明所有類型導入,包括根導入(定義為 "index.d.ts": [..])。

您可以在此處 閲讀有關該功能的更多信息。

最佳實踐

除非您計劃將包僅供其他 SvelteKit 項目使用,否則應避免在包中使用 SvelteKit 特定模塊(如 $app/environment)。例如,與其使用 import { browser } from '$app/environment',不如使用 import { BROWSER } from 'esm-env'(參見 esm-env 文檔)。您可能還希望將當前 URL 或導航操作作為 prop 傳入,而不是直接依賴 $app/state$app/navigation 等。這種更通用的編寫方式還會使測試、UI 演示等工具的設置變得更加容易。

svelte.config.js(而非 vite.config.jstsconfig.json)中通過 aliases 添加別名,以便它們被 svelte-package 處理。

應仔細考慮對包的更改是錯誤修復、新功能還是重大更改,並相應地更新包版本。注意,如果從現有庫中移除任何 exports 路徑或其內的任何 export 條件,應將其視為重大更改。

{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
// 將 `svelte` 更改為 `default` 是重大更改:
---            "svelte": "./dist/index.js"---
+++            "default": "./dist/index.js"+++
    },
// 移除此項是重大更改:
---        "./foo": {
      "types": "./dist/foo.d.ts",
      "svelte": "./dist/foo.js",
      "default": "./dist/foo.js"
    },---
// 添加此項是可以的:
+++        "./bar": {
      "types": "./dist/bar.d.ts",
      "svelte": "./dist/bar.js",
      "default": "./dist/bar.js"
    }+++
  }
}

選項

svelte-package 接受以下選項:

  • -w/--watch — 監聽 src/lib 的文件更改並重新構建包
  • -i/--input — 包含包所有文件的輸入目錄。默認為 src/lib
  • -o/--output — 處理後的文件寫入的輸出目錄。您的 package.jsonexports 應指向該文件夾內的文件,files 數組也應包含該文件夾。默認為 dist
  • -t/--types — 是否創建類型定義(d.ts 文件)。我們強烈建議這樣做,因為它有助於提升生態系統庫的質量。默認為 true
  • --tsconfig — tsconfig 或 jsconfig 的路徑。如果未提供,則會在工作區路徑中搜索最近的 tsconfig/jsconfig。

發佈

要發佈生成的包:

npm publish

限制

所有的相對文件導入需要完全指定路徑,遵守 Node 的 ESM 算法。這意味着對於像 src/lib/something/index.js 這樣的文件,必須包括文件名和擴展名:

// @errors: 2307
import { something } from './something+++/index.js+++';

如果您使用 TypeScript,您需要以同樣的方式導入 .ts 文件,但使用 .js 文件後綴而不是 .ts 文件後綴。(這是一個 TypeScript 的設計決策,超出我們的控制範圍。)在您的 tsconfig.jsonjsconfig.json 中設置 "moduleResolution": "NodeNext" 將有助於解決這個問題。

除 Svelte 文件(預處理)和 TypeScript 文件(轉換為 JavaScript)外,所有文件都按原樣複製。

Svelte 中文文檔

點擊查看中文文檔:

  1. SvelteKit 淺層路由
  2. SvelteKit Packaging

系統學習 Svelte,歡迎入手小冊《Svelte 開發指南》。語法篇、實戰篇、原理篇三大篇章帶你係統掌握 Svelte!

此外我還寫過 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答讀者問等 14 個系列文章, 全系列文章目錄:https://github.com/mqyqingfeng/Blog

歡迎圍觀我的“網頁版朋友圈”、加入“冴羽·成長陪伴社羣”,踏上“前端大佬成長之路”。

user avatar evans_bo Avatar linong Avatar liulhf Avatar shine_zhu Avatar
Favorites 4 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.