剑子灵 發表於 2025-4-8 20:25:00

SvelteKit 最新中文文档教程(18)—— 浅层路由和 Packaging

<h2 id="前言">前言</h2>
<p>Svelte,一个语法简洁、入门容易,面向未来的前端框架。</p>
<p>从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,<strong>从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1</strong>:</p>
<p><img src="https://yayujs-blog.oss-cn-beijing.aliyuncs.com/405488775-48df16b1-939c-489b-8d52-6071869893f0.png"></p>
<p>Svelte 以其独特的编译时优化机制著称,具有<strong>轻量级</strong>、<strong>高性能</strong>、<strong>易上手</strong>等特性,<strong>非常适合构建轻量级 Web 项目</strong>。</p>
<p>为了帮助大家学习 Svelte,我同时搭建了 Svelte 最新的中文文档站点。</p>
<p>如果需要进阶学习,也可以入手我的小册《Svelte 开发指南》,语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p>
<h2 id="浅层路由">浅层路由</h2>
<p>当您在 SvelteKit 应用中导航时,您会创建<em>历史记录条目</em>。点击后退和前进按钮会遍历这个条目列表,重新运行所有 <code>load</code> 函数,并在必要时替换页面组件。</p>
<p>有时,在不导航的情况下创建历史条目是有用的。例如,您可能想要显示一个模态对话框,用户可以通过返回导航来关闭它。这在移动设备上特别有价值,因为滑动手势通常比直接与 UI 交互更自然。在这些情况下,<em>没有</em>关联历史记录条目的模态可能会令人沮丧,因为用户可能会尝试向后滑动来关闭它,却发现自己到了错误的页面。</p>
<p>SvelteKit 通过 <code>pushState</code> 和 <code>replaceState</code> 函数使这成为可能,这些函数允许您在不进行导航的情况下将状态与历史记录条目关联。例如,要实现一个由历史驱动的模态:</p>
<pre><code class="language-svelte">&lt;!--- file: +page.svelte ---&gt;
&lt;script&gt;
import { pushState } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';

function showModal() {
    pushState('', {
      showModal: true
    });
}
&lt;/script&gt;

{#if page.state.showModal}
&lt;Modal close={() =&gt; history.back()} /&gt;
{/if}
</code></pre>
<p>模态框可以通过返回导航(取消设置 <code>page.state.showModal</code>)或通过交互触发 <code>close</code> 回调运行来关闭。</p>
<h3 id="api">API</h3>
<p><code>pushState</code> 的第一个参数是相对于当前 URL 的 URL。要保持在当前 URL,使用 <code>''</code>。</p>
<p>第二个参数是新的页面状态,可以通过 page 对象 作为 <code>page.state</code> 访问。您可以通过声明 <code>App.PageState</code> 接口(通常在 <code>src/app.d.ts</code> 中)来使页面状态类型安全。</p>
<p>要设置页面状态而不创建新的历史记录条目,请使用 <code>replaceState</code> 而不是 <code>pushState</code>。</p>
<blockquote>
<p>[!旧版说明] &gt; <code>$app/state</code> 中的 <code>page.state</code> 是在 SvelteKit 2.12 中添加的。如果您使用的是较早版本或正在使用 Svelte 4,请使用 <code>$app/stores</code> 中的 <code>$page.state</code>。</p>
</blockquote>
<h3 id="为路由加载数据">为路由加载数据</h3>
<p>在进行浅层路由时,您可能想在当前页面内渲染另一个 <code>+page.svelte</code>。例如,点击照片缩略图可以弹出详细视图,而不需要导航到照片页面。</p>
<p>为此,您需要加载 <code>+page.svelte</code> 所需的数据。一个便捷的方法是在 <code>&lt;a&gt;</code> 元素的 <code>click</code> 处理程序中使用 <code>preloadData</code>。如果元素(或其父元素)使用 <code>data-sveltekit-preload-data</code>,数据将已经被请求,<code>preloadData</code> 将复用该请求。</p>
<pre><code class="language-svelte">&lt;!--- file: src/routes/photos/+page.svelte ---&gt;
&lt;script&gt;
import { preloadData, pushState, goto } from '$app/navigation';
import { page } from '$app/state';
import Modal from './Modal.svelte';
import PhotoPage from './/+page.svelte';

let { data } = $props();
&lt;/script&gt;

{#each data.thumbnails as thumbnail}
&lt;a
    href="/photos/{thumbnail.id}"
    onclick={async (e) =&gt; {
      if (innerWidth &lt; 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' &amp;&amp; result.status === 200) {
      pushState(href, { selected: result.data });
      } else {
      // 出现问题!尝试导航
      goto(href);
      }
    }}
&gt;
    &lt;img alt={thumbnail.alt} src={thumbnail.src} /&gt;
&lt;/a&gt;
{/each}

{#if page.state.selected}
&lt;Modal onclose={() =&gt; history.back()}&gt;
    &lt;!-- 将页面数据传递给 +page.svelte 组件,
         就像 SvelteKit 在导航时那样 --&gt;
    &lt;PhotoPage data={page.state.selected} /&gt;
&lt;/Modal&gt;
{/if}
</code></pre>
<h3 id="注意事项">注意事项</h3>
<p>在服务端渲染期间,<code>page.state</code> 始终是一个空对象。对于用户首次访问的页面也是如此 — 如果用户重新加载页面(或从另一个文档返回),状态将<em>不会</em>应用,直到他们进行导航。</p>
<p>浅层路由是一个需要 JavaScript 才能工作的功能。在使用它时要谨慎,并尝试考虑在 JavaScript 不可用时的合理后备行为。</p>
<h2 id="packaging">Packaging</h2>
<p>您可以使用 SvelteKit 来构建应用程序和组件库,使用 <code>@sveltejs/package</code> 包(<code>npx sv create</code> 提供了设置此功能的选项)。</p>
<p>在创建应用程序时,<code>src/routes</code> 的内容是对外公开的部分;<code>src/lib</code> 包含应用程序的内部库。</p>
<p>组件库的结构与 SvelteKit 应用程序完全相同,区别在于 <code>src/lib</code> 是对外公开的部分,而根目录下的 <code>package.json</code> 用于发布包。<code>src/routes</code> 可能是随库附带的文档或演示站点,也可能只是开发时使用的沙箱。</p>
<p>运行 <code>@sveltejs/package</code> 提供的 <code>svelte-package</code> 命令会将 <code>src/lib</code> 的内容生成到一个 <code>dist</code> 目录中(可以配置),其中包括以下内容:</p>
<ul>
<li><code>src/lib</code> 中的所有文件。Svelte 组件会被预处理,TypeScript 文件会被转译为 JavaScript。</li>
<li>为 Svelte、JavaScript 和 TypeScript 文件生成类型定义(<code>d.ts</code> 文件)。您需要安装 <code>typescript &gt;= 4.0.0</code> 来支持此功能。类型定义文件会被放置在实现文件旁边,手动编写的 <code>d.ts</code> 文件将原样复制。您可以禁用生成,但我们强烈建议不要这样做 —— 使用您库的用户可能会需要这些文件来支持 TypeScript。</li>
</ul>
<blockquote>
<p>[!注意] <code>@sveltejs/package</code> 的第 1 版会生成一个 <code>package.json</code>。现在不再如此,它会使用项目中的 <code>package.json</code> 并验证其正确性。如果您仍然使用第 1 版,请查看此 PR 获取迁移说明。</p>
</blockquote>
<h3 id="packagejson-的结构"><code>package.json</code> 的结构</h3>
<p>因为您现在正在为公共使用构建一个库,因此 <code>package.json</code> 的内容变得更为重要。通过它,您可以配置包的入口点、发布到 npm 的文件以及库的依赖。我们将逐一介绍最重要的字段。</p>
<h4 id="name">name</h4>
<p>这是您包的名称,其他人可以使用该名称安装您的包,并可在 <code>https://npmjs.com/package/&lt;name&gt;</code> 网站上看到它。</p>
<pre><code class="language-json">{
        "name": "your-library"
}
</code></pre>
<p>在此处阅读关于它的更多内容。</p>
<h4 id="license">license</h4>
<p>每个包都应有一个 license 字段,以告知人们如何使用它。目前非常流行的一种许可证是 <code>MIT</code>,它在分发和复用方面非常宽松且无需担保。</p>
<pre><code class="language-json">{
        "license": "MIT"
}
</code></pre>
<p>在此处阅读关于它的更多内容。请注意,应在包中包含一个 <code>LICENSE</code> 文件。</p>
<h4 id="files">files</h4>
<p>该字段告诉 npm 哪些文件将被打包并上传到 npm。它应包含输出文件夹(默认为 <code>dist</code>)。您的 <code>package.json</code>、<code>README</code> 和 <code>LICENSE</code> 文件会始终被包括在内,因此您不需要指定它们。</p>
<pre><code class="language-json">{
        "files": ["dist"]
}
</code></pre>
<p>要排除不必要的文件(如单元测试,或者仅从 <code>src/routes</code> 导入的模块等)可以将它们添加到 <code>.npmignore</code> 文件中。这将导致包更小,安装速度更快。</p>
<p>在此处阅读关于它的更多内容。</p>
<h4 id="exports">exports</h4>
<p><code>"exports"</code> 字段包含包的入口点。如果您通过 <code>npx sv create</code> 设置了一个新的库项目,它会设置为单一出口,即包的根目录:</p>
<pre><code class="language-json">{
        "exports": {
                ".": {
                        "types": "./dist/index.d.ts",
                        "svelte": "./dist/index.js"
                }
        }
}
</code></pre>
<p>这告诉打包工具和工具链,您的包只有一个入口点,即根目录,所有内容应通过以下方式导入:</p>
<pre><code class="language-js">// @errors: 2307
import { Something } from 'your-library';
</code></pre>
<p><code>types</code> 和 <code>svelte</code> 键是导出条件,它们告诉工具在查找 <code>your-library</code> 导入时应引入哪个文件:</p>
<ul>
<li>TypeScript 看到 <code>types</code> 条件,会查找类型定义文件。如果您不发布类型定义,请忽略此条件。</li>
<li>支持 Svelte 的工具会看到 <code>svelte</code> 条件,知道这是一个 Svelte 组件库。如果您发布的库不导出任何 Svelte 组件,并且也可以在非 Svelte 项目中使用(如 Svelte store 库),您可以将此条件替换为 <code>default</code>。</li>
</ul>
<blockquote>
<p>[!注意] 早期版本的 <code>@sveltejs/package</code> 还添加了一个 <code>package.json</code> 导出。这不再是模板的一部分,因为所有工具都可以处理没有明确导出的 <code>package.json</code>。</p>
</blockquote>
<p>您可以根据需要调整 <code>exports</code> 并提供更多入口点。例如,如果您想直接暴露 <code>src/lib/Foo.svelte</code> 组件而不是通过 <code>src/lib/index.js</code> 文件重新导出组件,您可以创建以下导出映射……</p>
<pre><code class="language-json">{
        "exports": {
                "./Foo.svelte": {
                        "types": "./dist/Foo.svelte.d.ts",
                        "svelte": "./dist/Foo.svelte"
                }
        }
}
</code></pre>
<p>……然后您的库的使用者可以用如下方式导入该组件:</p>
<pre><code class="language-js">// @filename: ambient.d.ts
declare module 'your-library/Foo.svelte';

// @filename: index.js
// ---cut---
import Foo from 'your-library/Foo.svelte';
</code></pre>
<blockquote>
<p>[!注意] 请注意,如果您提供类型定义,采用此方式可能需要额外处理。在此处阅读关于此问题的更多详细信息。</p>
</blockquote>
<p>通常,<code>exports</code> 映射的每个键都是用户从您的包中导入某些内容的路径。而值则是将被导入的文件的路径或包含这些文件路径的导出条件映射。</p>
<p>在此处阅读关于 <code>exports</code> 的更多内容。</p>
<h4 id="svelte">svelte</h4>
<p>这是一个遗留字段,用于让工具识别 Svelte 组件库。如果使用 <code>svelte</code> 导出条件,它已不再必要,但为了向尚未了解导出条件的过时工具提供兼容性,建议保留它。它应指向您的根入口点。</p>
<pre><code class="language-json">{
        "svelte": "./dist/index.js"
}
</code></pre>
<h4 id="sideeffects">sideEffects</h4>
<p><code>package.json</code> 中的 <code>sideEffects</code> 字段用于让打包工具判断模块是否可能包含副作用。如果模块在被导入时对其他脚本可见的行为产生变化(例如修改全局变量或内置 JavaScript 对象的原型),则视为有副作用。由于副作用可能会影响应用程序的其他部分,这些文件/模块无论其导出是否在应用程序中使用,都会被包括在最终的打包文件中。</p>
<p>在 <code>sideEffects</code> 字段中指定的模块会帮助打包工具更积极地从最终的打包文件中剔除未使用的导出(即 tree-shaking),从而生成更小更高效的打包文件。不同的打包工具以不同的方式处理 <code>sideEffects</code>。尽管 Vite 不需要此配置,但建议为库声明所有 CSS 文件具有副作用,以保持与 webpack 兼容。新创建的项目中的默认配置如下:</p>
<pre><code class="language-json">/// file: package.json
{
        "sideEffects": ["**/*.css"]
}
</code></pre>
<blockquote>
<p>如果您的库中的脚本存在副作用,请确保更新 <code>sideEffects</code> 字段。在新创建的项目中,所有脚本默认标记为无副作用。如果错误地将包含副作用的文件标记为没有副作用,可能会导致功能异常。</p>
</blockquote>
<p>如果您的包中有副作用的文件,可以通过数组指定这些文件:</p>
<pre><code class="language-json">/// file: package.json
{
        "sideEffects": ["**/*.css", "./dist/sideEffectfulFile.js"]
}
</code></pre>
<p>这样只会将指定的文件视为有副作用的文件。</p>
<h3 id="typescript">TypeScript</h3>
<p>即使您自己不使用 TypeScript,也应为您的库提供类型定义,这样使用您库的人可以获得正确的智能提示。<code>@sveltejs/package</code> 让生成类型的过程对您来说基本上是透明的。默认情况下,在打包您的库时,会为 JavaScript、TypeScript 和 Svelte 文件自动生成类型定义。您只需要确保 exports 映射中的 <code>types</code> 条件指向正确的文件。当通过 <code>npx sv create</code> 初始化库项目时,会自动设置为根导出。</p>
<p>然而,如果您除了根导出还有其他内容,例如提供 <code>your-library/foo</code> 导入,您需要额外注意提供类型定义。不幸的是,默认情况下 TypeScript <em>不会</em> 为这种导出解析 <code>types</code> 条件,比如 <code>{ "./foo": { "types": "./dist/foo.d.ts", ... }}</code>。相反,它会从库的根目录(即 <code>your-library/foo.d.ts</code> 而不是 <code>your-library/dist/foo.d.ts</code>)查找 <code>foo.d.ts</code> 文件。为了解决这个问题,您有两种选择:</p>
<p>第一种选择是要求使用您库的人在其 <code>tsconfig.json</code>(或 <code>jsconfig.json</code>)中将 <code>moduleResolution</code> 选项设置为 <code>bundler</code>(从 TypeScript 5 开始可用,未来是最佳推荐选项)、<code>node16</code> 或 <code>nodenext</code>。这会使 TypeScript 实际查看 exports 映射并正确解析这些类型。</p>
<p>第二种选择是滥用 TypeScript 的 <code>typesVersions</code> 特性连接类型。<code>typesVersions</code> 是 <code>package.json</code> 中的一个字段,TypeScript 根据 TypeScript 版本检查不同类型定义,同时也包含路径映射功能。我们利用该路径映射功能来满足需求。对于上面提到的 <code>foo</code> 导出,相应的 <code>typesVersions</code> 定义如下:</p>
<pre><code class="language-json">{
        "exports": {
                "./foo": {
                        "types": "./dist/foo.d.ts",
                        "svelte": "./dist/foo.js"
                }
        },
        "typesVersions": {
                "&gt;4.0": {
                        "foo": ["./dist/foo.d.ts"]
                }
        }
}
</code></pre>
<p><code>&gt;4.0</code> 表示如果使用的 TypeScript 版本大于 4,则 TypeScript 会检查内部映射。内部映射告诉 TypeScript <code>your-library/foo</code> 的类型定义在 <code>./dist/foo.d.ts</code> 中,这实际上是对 <code>exports</code> 条件的复制。您还可以使用 <code>*</code> 通配符一次性提供多个类型定义而无需重复。如果选择使用 <code>typesVersions</code>,您需要通过它声明所有类型导入,包括根导入(定义为 <code>"index.d.ts": [..]</code>)。</p>
<p>您可以在此处 阅读有关该功能的更多信息。</p>
<h3 id="最佳实践">最佳实践</h3>
<p>除非您计划将包仅供其他 SvelteKit 项目使用,否则应避免在包中使用 SvelteKit 特定模块(如 <code>$app/environment</code>)。例如,与其使用 <code>import { browser } from '$app/environment'</code>,不如使用 <code>import { BROWSER } from 'esm-env'</code>(参见 esm-env 文档)。您可能还希望将当前 URL 或导航操作作为 prop 传入,而不是直接依赖 <code>$app/state</code>、<code>$app/navigation</code> 等。这种更通用的编写方式还会使测试、UI 演示等工具的设置变得更加容易。</p>
<p>在 <code>svelte.config.js</code>(而非 <code>vite.config.js</code> 或 <code>tsconfig.json</code>)中通过 aliases 添加别名,以便它们被 <code>svelte-package</code> 处理。</p>
<p>应仔细考虑对包的更改是错误修复、新功能还是重大更改,并相应地更新包版本。注意,如果从现有库中移除任何 <code>exports</code> 路径或其内的任何 <code>export</code> 条件,应将其视为重大更改。</p>
<pre><code class="language-json">{
"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"
    }+++
}
}
</code></pre>
<h3 id="选项">选项</h3>
<p><code>svelte-package</code> 接受以下选项:</p>
<ul>
<li><code>-w</code>/<code>--watch</code> — 监听 <code>src/lib</code> 的文件更改并重新构建包</li>
<li><code>-i</code>/<code>--input</code> — 包含包所有文件的输入目录。默认为 <code>src/lib</code></li>
<li><code>-o</code>/<code>--output</code> — 处理后的文件写入的输出目录。您的 <code>package.json</code> 的 <code>exports</code> 应指向该文件夹内的文件,<code>files</code> 数组也应包含该文件夹。默认为 <code>dist</code></li>
<li><code>-t</code>/<code>--types</code> — 是否创建类型定义(<code>d.ts</code> 文件)。我们强烈建议这样做,因为它有助于提升生态系统库的质量。默认为 <code>true</code></li>
<li><code>--tsconfig</code> — tsconfig 或 jsconfig 的路径。如果未提供,则会在工作区路径中搜索最近的 tsconfig/jsconfig。</li>
</ul>
<h3 id="发布">发布</h3>
<p>要发布生成的包:</p>
<pre><code class="language-sh">npm publish
</code></pre>
<h3 id="限制">限制</h3>
<p>所有的相对文件导入需要完全指定路径,遵守 Node 的 ESM 算法。这意味着对于像 <code>src/lib/something/index.js</code> 这样的文件,必须包括文件名和扩展名:</p>
<pre><code class="language-js">// @errors: 2307
import { something } from './something+++/index.js+++';
</code></pre>
<p>如果您使用 TypeScript,您需要以同样的方式导入 <code>.ts</code> 文件,但使用 <code>.js</code> 文件后缀而不是 <code>.ts</code> 文件后缀。(这是一个 TypeScript 的设计决策,超出我们的控制范围。)在您的 <code>tsconfig.json</code> 或 <code>jsconfig.json</code> 中设置 <code>"moduleResolution": "NodeNext"</code> 将有助于解决这个问题。</p>
<p>除 Svelte 文件(预处理)和 TypeScript 文件(转换为 JavaScript)外,所有文件都按原样复制。</p>
<h2 id="svelte-中文文档">Svelte 中文文档</h2>
<p>点击查看中文文档:</p>
<ol>
<li>SvelteKit 浅层路由</li>
<li>SvelteKit Packaging</li>
</ol>
<p>系统学习 Svelte,欢迎入手小册《Svelte 开发指南》。语法篇、实战篇、原理篇三大篇章带你系统掌握 Svelte!</p>
<p>此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:https://github.com/mqyqingfeng/Blog</p>
<p>欢迎围观我的“网页版朋友圈”、加入“冴羽·成长陪伴社群”,踏上“前端大佬成长之路”。</p><br><br>
来源:https://www.cnblogs.com/yayujs/p/18815384
頁: [1]
查看完整版本: SvelteKit 最新中文文档教程(18)—— 浅层路由和 Packaging