用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.svelte
到src/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.svelte
和about.svelte
中添加类似的日志语句。启动开发服务器,看看浏览器中的控制台;首先出现布局信息,然后是索引信息。
现在,当我们导航到关于页面时,日志显示添加的about component
行
由于$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中有三个钩子,我们将在下一节中使用getContext
和getSession
。
在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.svelte
的load
函数检查用户是否通过了认证,如果没有,就把他们重定向到登录页面。我们只在服务器端执行这个检查,因为我们的应用程序的结构是不提供任何方式来导航到/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的框架仍处于测试阶段,如果你在使用它时遇到任何问题,可能很难找到答案。然而,如果它适合你的项目要求,它可能会有令人难以置信的效果。