最新基于Svelte的框架探索——SvelteKit(sapper的继任者)

3,991 阅读10分钟

Svelte创建网络应用的最新框架来了。SvelteKit。这个框架即使对经验不足的开发者来说也很容易使用。

SvelteKit是Sapper的继任者,Sapper是一个由Svelte驱动的紧凑而强大的JavaScript框架。新发布的SvelteKit是对Sapper所提供内容的升级,目前正处于公开测试阶段。

亲自探索SvelteKit给我留下了深刻的印象,因为它相当容易理解;与React等其他流行的框架相比,它需要学习的概念更少。

让我们深入了解一下Svelte和SvelteKit的基础知识,并最终探索一个SvelteKit的例子。

什么是Svelte和SvelteKit?

Svelte是一个类似React的组件库,而SvelteKit是类似Next.js的应用框架。虽然相似,但Svelte之所以能与React区分开来,是因为它提供了一种不同的方式来思考网络应用。

React使用虚拟DOM差异来决定更新UI所需的变化,但Svelte是一个编译器,它编译你的代码并将Svelte组件转换为JavaScript来渲染和更新它们,使其更快、更轻。

然后,SvelteKit就会像Next.js一样,用服务器端的渲染、路由等来完成设置应用程序的所有繁重工作。然而,SvelteKit还使用了一个适配器,可以将你的应用导出到特定的平台,并能很好地适应无服务器架构。由于无服务器架构正变得越来越突出,这也是尝试SvelteKit的一个好理由。

你可以为Netlify和Vercel等平台使用官方的SvelteKit适配器。

通过同时提供包括服务器端渲染、代码拆分等功能,SvelteKit对初创期特别有用。

有了这些,让我们看看如何用SvelteKit创建一个新项目。

设置SvelteKit

在我们对一个示例应用程序进行编码之前,我们先来玩玩当你用SvelteKit创建一个新项目时得到的演示应用程序,并回顾一些关键的概念,让你熟悉这个框架。

安装

首先,在终端输入以下代码片段。这将在当前目录下建立一个应用程序。

npm init svelte@next

然后输入下面的代码来安装所有的依赖项,我们就可以开始了。

npm install

另外,如果你使用的是Visual Studio Code,请安装官方的Svelte扩展,以获得语法高亮和其他编写Svelte组件的功能,如应用程序页面。

SvelteKit设置了一个路由系统,你的src/routes 中的文件决定了你的应用程序中的路由。这个目录可以通过编辑svelte.config.cjs 来改变。

请注意,src/routes/index.svelte 是主页。

通过输入npm run dev ,你开始了一个开发服务器。SvelteKit在幕后使用Vite,使得更新速度非常快。

在这一点上,通过安装静态适配器来构建整个应用程序的预渲染版本。

npm i -D @sveltejs/adapter-static@next

现在,让我们探索一些代码,做一些改变,看看结果。

路由

我们将在SvelteKit为我们引导的计数器应用程序中添加另一个路由,方法是将about.sveltesrc/routes/ 目录中。

<!-- about page -->
<svelte:head>
    <title>About</title>
</svelte:head>
<h1>About Page</h1>
<p>This is the about page. Click <a href="/">here</a> to go to the index page.</p>

正如你可能猜到的那样,这将为我们设置另一个路由:/about 。为了导航到这个页面,我们也将在索引页上添加一个链接。

索引页已经有了下面这一行。

<p>Visit <a href="https://svelte.dev">svelte.dev</a> to learn how to build Svelte apps.</p>

我们只要把它改成下面的代码就可以了。

<p>Visit the <a href="/about">about</a> page</p>

当我们点击该链接时,内部路由器就会启动并处理导航问题。事实上,SvelteKit默认会处理导航。最初的加载是在服务器端处理的,然后SvelteKit的内置路由器在客户端处理后续的导航,除非我们另有指定。

SvelteKit允许你通过修改Svelte配置文件svelte.config.cjs ,来禁用这个路由器。将router 属性设置为 "false "可以禁用整个应用程序的路由器。这将导致应用程序为每个页面发送新的请求,这意味着导航将在服务器端处理。

如果需要的话,你也可以在每个页面的基础上禁用路由器。我们将继续前进,通过在about.svelte 的顶部添加以下内容来看到它的作用。

<script context="module" lang="ts">
    export const router=false;
</script>

我将在稍后谈论context="module"lang="ts" 。现在,让我们通过npm run``dev 来运行这个应用程序。我们应该期望所有来自 "关于 "页面的路由将由服务器处理,这意味着当从 "关于 "页面导航时,将向服务器发出新的请求。这是SvelteKit为我们提供的一个很好的小功能,完全开箱即用。

脚本和样式

看一下我们刚才的脚本,包含context="module" 的脚本被直接添加到模块中。这意味着每当组件被初始化时,它们就会运行一次,而不是像其他没有context="module" 的脚本那样,成为实例--组件--的一部分,并在实例被创建和初始化时运行。

因此,<script context="module"></script> 中的变量在模块默认导出的实例中共享,也就是组件本身。

lang="ts" 告诉编译器,使用的语言是TypeScript。如果你在设置时选择了TypeScript作为语言,你就需要使用这个。如果你使用的是JavaScript,那么就不需要在这里做什么。

作为一个小实验,我们将这个添加到src/lib/Counter.svelte

<script context="module">
    console.log("module code");
</script>

然后,在已经存在的实例级脚本的顶部添加这一行。

console.log("component code");

我们还将在index.svelte 中加入另一个计数器组件,即<Counter/>

那么,当我们运行这个时,我们会看到什么?由于这两个计数器是相互独立的,日志显示 "模块代码 "首先运行,然后出现两个 "组件代码 "的信息。

现在,让我们把这个添加到about.svelte 的底部。

<style>
    p {
        color:blue;
        max-width: 14rem;
        margin: 2rem auto;
        line-height: 1.35;
    }
</style>

在Svelte中,应用于组件的样式是以组件为范围的。这个样式将只应用于 "关于 "页面。

你还会注意到$layout.svelte 组件在routes/ ;它可以显示和样式那些在不同路线上持续存在的东西,比如说,页脚。

布局

让我们深入了解布局组件如何将每一个组件都包裹在自己的内部,使其成为执行提供商店和设置上下文等功能的理想场所。

首先,让我们将其添加到$layout.svelte 文件中。

<script>
  console.log("layout component");
</script>  

然后在路由index.svelteabout.svelte 中添加类似的日志语句。启动开发服务器,看看浏览器中的控制台;首先出现布局信息,然后是索引信息。

Console In Your Browser Shows The Layout Message First Then The Index Message.

现在,当我们导航到关于页面时,日志显示添加的about component

The About Page Logs Show the Added About Component Line

由于$layout 组件首先被渲染,当路由器需要时,页面就会从布局中添加和删除。

你也可以使用Svelte提供的生命周期方法onDestroy ,来验证布局组件只渲染一次,并且在导航到不同页面时不会被卸载。通过在$layout.svelte ,你会发现控制台中没有出现日志。

import { onDestroy } from 'svelte';
onDestroy(() => console.log("$layout unmounted")); 

onDestroy ,即使我们在不同的页面之间导航,也不会被调用。

我们可以通过获取一些许多页面需要的数据,或者建立一个集中的存储(我们将在后面看到),让其他页面可以用来互相传递数据,从而利用这种行为。

如果你熟悉Svelte或React,在代码中添加上下文可以让我们免于钻研道具。在我们的例子中,我们可以在$layout.svelte 中为数据添加上下文,供所有页面及其组件接收。

服务器端

我们知道,SvelteKit默认在第一次加载时在服务器端渲染应用程序。但是,如果我们想在SSR期间用数据填充我们的应用程序,而不向用户显示一个加载旋钮呢?或者,我们如何将数据从服务器上传到客户端?

那么,SvelteKit提供了只在服务器上运行的钩子,帮助我们实现这些目标。但在我们探索钩子之前,我想先谈谈端点,以便更好地理解服务器端的情况。

端点是服务器端的,其创建方式与页面和路由类似。然而,作为端点的文件将以.js.ts 的扩展名结尾,位于routes 目录中。

// src/routes/dogs.ts
import type { RequestHandler, Response } from "@sveltejs/kit";

interface dog{
name: string
}
const dogs:dog[]=[{name:"German Shepherd"},{name:"BullDog"},{name:"Poodle"}]
export const get:RequestHandler= async () =>{
    const res:Response={
        body:{
            dogs
        }
     }
    return res;
}

方法名称get 对应于HTTP方法GET。这个端点可以在/dogs 。如果你在浏览器中导航到/dogs ,你会发现一个包含狗的列表的JSON响应。

有了钩子,你就可以对服务器端进行更精细的控制,创造一个理想的地方来执行认证等功能,因为它们也会从客户端接收HTTP请求对象。SvelteKit中有三个钩子,我们将在下一节中使用getContextgetSession

在SvelteKit中构建

了解了SvelteKit生态系统的基本情况后,我们可以构建一个非常简单的玩具应用程序,它将从我们将要设置的源头获取数据,执行一些简单的认证,并设置一个中央存储。

我们的应用程序将包含以下路由:/counter1,/counter2,/about, 和/login 。计数器页面将被保护,而关于页面将不会。

因此,让我们先关注一下认证逻辑。

认证

由于钩子在每次请求运行之前都会在服务器上运行,而且它们可以访问请求参数,所以src/hooks.ts 是提取cookie并为用户创建会话的理想场所。

请注意,这个会话不是典型意义上的会话;服务器端不会保留任何会话记录。我们在这里使用的会话将只是帮助我们向客户端传递数据,并提供初始状态。

getContext 钩子接收请求头,它可能包含也可能不包含cookies,这取决于请求的认证。当我们提取认证令牌并返回时,下一个钩子将接收这个上下文作为一个参数。

getSession 钩子返回的任何东西都可以作为一个会话变量提供给每个页面。

// src/hooks.ts
import {defaultState} from '$lib/store';
import * as cookie from 'cookie';
const auth_token='demo_token_for_example';
const userDetails={name:"Deb",age:45}

export const getContext:GetContext=({ headers })=>{
    const cookies = cookie.parse(headers.cookie || '');
    return {
        token:cookies['token']
    };
}
export const getSession:GetSession=async ({context})=>{
    let initialState={...defaultState};
    if (context['token']===auth_token){
        console.log("tokens match");
        initialState.authenticated=true
        initialState.user=userDetails;
    }
    console.log(initialState)
    return initialState
}

为了简洁起见,我们将把认证令牌和用户细节存储在文件本身。在一个真实的项目中,你可能会使用一个数据库或一个认证后端来做这个。

我们的想法是在getContext ,从头文件中提取一个cookie,然后检查它是否有正确的令牌。如果它包含正确的令牌,我们就返回 "已认证 "的初始状态。不要担心initialState ,我们将在本篇文章的后面看一下$lib/store

现在我们将设置一个端点,它将接受一个GET请求并返回一个包含令牌的cookie。这在登录组件中会很有用。

// src/routes/auth.ts
const auth_token='demo_token_for_example';
const cookie=`token=${auth_token};HttpOnly;Secure`
const header:Headers={'set-cookie':cookie}
export const get:RequestHandler=()=>{
    return{
        headers:header,
        body:{
            token:auth_token,
            success:true,
            user:{
                name:"Deb",
                age:45
            }
        }
    }

}

同样,用户的详细信息通常会从数据库中获取。但在这里,为了简单起见,我们对它们进行硬编码。

构建商店

如果你不熟悉Svelte的可写存储,它们可以被写入或来自应用程序的任何地方,并且是反应式的。这是一个建立可写存储的简单方法,它将存储我们应用程序的全局状态。

// src/lib/store.ts
import {Writable, writable} from 'svelte/store';
export type User={
    name:string|null,
    age?:number
}
export interface stateType{
    authenticated:boolean,
    user:User,
    counter:number
}
export const defaultState:stateType={
    authenticated:false,
    user:{
        name:null,
    },
    counter:0
}
export default class Store{
    state:Writable<stateType>;
    constructor(initialState:stateType=defaultState){
        this.state=writable({...initialState})
    }
    changeAuthenticationState=(user:User)=>{
        this.state.update((obj)=>{
            console.log("old state")
            console.log(obj)
            return {
                ...obj,
                authenticated:!obj.authenticated,
                user:user
            }
        })
    }
    updateCounter=(val:number)=>{
        this.state.update((obj)=>{
            return {
                ...obj,
                counter:val
            }
        })
    }
}

接下来,我们将在$layout.svelte 根目录下建立一个上下文,并向所有的子目录提供我们的存储空间,使所有的页面都能访问存储空间。

<!-- src/routes/$layout.svelte -->
<script context="module" lang="ts">
    import Store from '$lib/store';
    import {setContext} from 'svelte';
</script>
<script lang="ts">
    import '../app.css';
    import {session} from '$app/stores';
    const store=new Store($session)
    setContext<Store>('store',store);
</script>
<slot />

请注意我们是如何使用从会话中得到的初始状态创建一个新的存储,并将其传递给setContext 。现在可以在任何页面中通过键'store' 来访问这个存储空间。

load 函数

我们的页面也可以导出一个特殊的函数,叫做load 函数。这个函数可以在组件渲染之前获取数据或写入会话,首先在服务器端运行,然后在客户端运行。这在服务器端渲染时特别有用,因为我们可能需要用必须事先获取的数据来填充我们的页面。

<!-- src/routes/login.svelte -->
<script context="module" lang="ts">
    import type { Load } from '@sveltejs/kit';
    export const load:Load=async ({session})=>{

                if(session.authenticated){
                    return{  
                        redirect:'/counter1',
                        status:302
                    }
                }   
            return {}
    }
</script>
<script lang="ts">
    import type Store from '$lib/store';
    import {goto} from '$app/navigation';
    import {setContext,getContext} from 'svelte';
    const store=getContext<Store>('store');
    const login=async ()=> {
        let res= await fetch('/auth');
        let data=await res.json();
        if(data.success){
            store.changeAuthenticationState(data.user);
            goto('/counter1');
        }
    }
</script>
<h1>Login Page</h1>
<button on:click={login}>Login</button>

在登录页面的load 函数中,我们可以检查用户是否经过认证,因为我们不想向经过认证的用户显示登录页面。

如果他们通过了认证,我们就把他们重定向到/counter1 页面。如果没有,我们获取令牌并更新状态。一旦通过认证,我们就可以导航到受保护的路由,如/counter1

计数器

counter1.svelteload 函数检查用户是否通过了认证,如果没有,就把他们重定向到登录页面。我们只在服务器端执行这个检查,因为我们的应用程序的结构是不提供任何方式来导航到/counter1 页面,而不对服务器执行完整的请求。

<script context="module" lang="ts">
    import {browser} from '$app/env';
    export const load:Load=async ({session})=>{
        if(!browser)
            {
                if(!session.authenticated){
                    return{ 
                        redirect:'login',
                        status:302
                    }
                }
                else{
                    session.counter=1; //set counter to 1 during ssr
                }
            }
            return {}
    }
</script>
<script lang="ts">
    import type Store from '$lib/store';
    import Counter from '$lib/Counter.svelte';
    import {setContext,getContext} from 'svelte';
    const store=getContext<Store>('store');
    const state=store.state;
</script>
<svelte:head>
    <title>Counter 1</title>
</svelte:head>
<main>
    <h1>Hello {$state.user.name}</h1>
    <Counter update={store.updateCounter} count={$state.counter}/>
    <p>Visit <a href="/counter2"> Counter2</a> </p>
</main>

然而,我们在任何不受保护的页面中都不包括指向受保护页面的链接,所以没有办法在不进行全面加载的情况下导航到这些页面。这意味着将向服务器发出请求。

当对/counter1 的请求发出后,getSession 运行并分配初始状态,将计数器设置为0。然后load 函数运行并将计数器的值更新为1,将更新的会话发送到布局组件,以便用更新的状态设置存储。

注意,如果我们在$layout.svelte 中有一个加载函数,它将在counter1.svelte 的加载函数之前运行。

/counter2 页面与/counter1 相同,只是我们将计数器初始化为2,促使第13行成为session.counter=2

在下面的代码中,我们可以在/counter1/counter2 页面中都使用计数器组件。

<!-- Counter.svelte -->
<script lang="ts">
    export let count:number;
    export let update:Function;
    const increment = () => {
        update(count+1)
    };
</script>
<button on:click={increment}>
    Clicks: {count}
</button>

结束

为了完成这个应用程序,我们必须添加about.svelte 页面。

<!-About.svelte -->
<h1> About page </h1>

创建一个生产构建

npm run build 将为我们创建一个生产构建。由于我们使用的是默认的节点适配器,我们在/build 中获得一个节点服务器,并使用node build 为应用程序服务。

结论

通过使用SvelteKit,我们能够在短短几分钟内创建一个包含SSR、认证和商店的应用程序!这就是SvelteKit。

由于SvelteKit的框架仍处于测试阶段,如果你在使用它时遇到任何问题,可能很难找到答案。然而,如果它适合你的项目要求,它可能会有令人难以置信的效果。

探索SvelteKit,最新的基于Svelte的框架》一文出现在LogRocket博客上。