原文链接:https://blog.logrocket.com/exploring-sapper-svelte-a-quick-tutorial/

因为英文水平有限,有问题大家留言批评。本来准备结合作者文章做体验文章,但是觉得感觉作者对于sapper的初始结构介绍很全面,没必要画蛇添足。

在战争中,建造桥梁,修路,清理雷区并进行拆除(在战斗条件下均如此)的士兵被称为**工兵(Sapper)**。

我们可以单独使用Svelte构建更复杂的应用程序,但是随着代码逻辑深入,它可能很快就会变得混乱。那么让我们来看看Sapper!

简介

Sapper是Svelte的配套组件框架,可帮助您以快速有效的方式构建更大,更复杂的应用程序。

在当今时代,构建web应用程序是一项相当复杂的工作,包括代码分解、数据管理、性能优化等。这就是为什么今天有无数的前端工具,但它们都有自己的复杂性和学习曲线。

开发一个应用程序应该不会那么困难,对吧?它能比现在更简单吗?有没有一种方法能让你在保持头脑清醒的同时满足所有的要求呢?这是一个问题。

对于web开发人员来说,风险当然比不上作战的工程师。但是我们面对的也是充满敌意的环境:性能欠佳的开设备,糟糕的网络连接,还有前端工程固有的复杂性。Sapper是Svelte app maker的缩写,是您勇敢而忠诚的盟友。

Sapper的设计目标是轻量级、高性能、易于推广,同时还要提供足够的特性来将您的想法转化为出色的web应用程序。

以下便是Sapper在Svelte中构建Web应用程序时为我们所提供的帮助:

  • Routing

  • SSR

  • 自动代码分割

  • 离线支持(使用Service Workers)

  • 高层次项目结构管理

    我相信大家都知道,自己管理这些可能很快就会成为一件烦人的事情,不得不让我们从实际的业务逻辑中分心。

    但是说了这么多,有啥用?那让我们来看看一个使用Svelte + Sapper的小型服务器渲染的应用程序。

体验

下载安装

Sapper模板提供webpack编译和rollup编译两种方式。

1
2
3
4
5
6
7
8
9
# rollup
npx degit "sveltejs/sapper-template#rollup" my-app
# webpack
npx degit "sveltejs/sapper-template#webpack" my-app

cd my-app

npm install
npm run dev & open http://localhost:3000

通过这个官方模板,其实我们就可以简单的了解到Sapper的路由处理和SSR,不用探索太深。

项目结构

Sapper比较固执,所以某些要求在开发时必须按照规定的形式执行。

入口

每个Sapper项目都有三个入口文件和一个src/template.html文件:

  1. src/client.js
  2. src/server.js
  3. src/service-worker.js (这个是可选的)
client.js
1
2
3
4
5
import * as sapper from '@sapper/app';

sapper.start({
target: document.querySelector('#sapper')
});

这是客户端所要呈现的应用程序的入口文件。它是一个相当简单的文件,这里需要做的就是从@sapper/app导入Sapper模块,并调用start方法。它接受一个对象作为参数,唯一需要的参数就是target。

目标指定应用程序将挂载在哪个DOM节点上。如果熟悉React,可以将其看作是ReactDOM.render。

server.js

我们需要一个服务器来为用户提供我们的应用程序,能理解吧?由于这是一个Node.js环境,所以我们有大量的选择。可以使用Express.js,Koa.js,Polka等等,但是有一些规则还是要遵循的:

  1. 服务器必须提供**/static**文件夹的内容。Sapper不在乎你用来做什么。但是必须要有那个文件夹!

  2. 采用的服务器框架必须支持middlewares,并且必须使用从@sapper/server里面使用sapper.middleware()导入。

  3. 采用的服务器必须监听process.env.PORT。

只有这三条规定。我们可以实际去看看生成的server.js文件与操作。

service-worker.js

如果你需要了解一下什么是service-worker,这篇文章应该不错。现在,使用Sapper构建功能完整的web应用程序不需要service-worker.js文件;它只是为了帮助你访问离线支持、推送通知、后台同步等功能(所以一般用于移动端)。

咱们可以选择完全不使用它,也可以使用它来实现更完整的用户体验。

template.html

这是应用程序的主要入口页面,所有组件、样式参考和脚本都在这里按需注入。除了需要通过从HTML链接到CDN来添加模块的少数情况外,它几乎是固定不动的。

routes

每个Sapper应用程序的重中之重。这是你大部分逻辑和内容的所在。我们将在下一节中进行更深入的研究。

路由

如果启动了项目,那么访问http://localhost:3000就能进入一个简单的web应用程序,其中包含一个主页、一个关于页面和一个博客页面。到目前为止,相当简单。

现在,让我们尝试理解Sapper如何能够协调URL和相应的文件。在Sapper中,有两种类型的路由:页面路由和服务器路由。

让我们进一步分析一下。

Page routes

当我们导航到某个页面例如(/ about)时,Sapper会呈现src/routes文件夹中的about.svelte文件。 这也意味着该文件夹内的任何.svelte文件都可以自动“映射”到相同名称的路由下。 因此,如果src/routes文件夹中有一个名为jump.svelte的文件,则导航/jumping将能把该文件的页面进行渲染。

简而言之,页面路由就是src/routes文件夹下的.svelte文件。这种方法处理的一个非常好的结果是,项目路由的路径是可预测的,并且很容易理解。我们如果想要一条新路由,只需要在src/routes中创建一个新的.svelte文件,我们这就成功了!

但如果我们想要一个嵌套的路由即子路由,比如这样的**/projects/sapper/awesome**。我们只需要做的就是为每个子路由创建一个文件夹。所以,对于上面的例子,你会有一个这样的文件夹结构:src/route/projects/sapper,然后我们在此文件夹下放置awesome.svelte文件就可以了。

知道了这一点,我们再回头看看我们的引导程序,并且导航到‘关于’页面。按照上面的逻辑,它应该是src/routers下的about.svelte文件。我们确认后也确实如此。

请注意,index.svelte文件是一个保留文件,当您导航到子路由时会呈现该文件。 例如,在我们模板中,我们有一个/ blogs路由,在其中可以访问其下的其他子路由,例如/blogs/why-the-name。

但是请注意,当/ blogs本身是文件夹时,在浏览器中导航到/blogs时也会呈现一个文件。 我们如何为这种路线创建文件?

所以,我们要么在/ blogs文件夹之外定义一个blog.svelte文件,要么我们需要在/ blogs文件夹下放置一个index.svelte文件,但不能同时放置两个文件。 当您直接访问/ blogs时,将呈现此index.svelte文件。

接下来我们思索,带有动态标签的URL呢? 在我们的示例中,手动创建每个博客文章并将其存储为.svelte文件是不可行的。 我们需要的是一个模板,该模板用于呈现所有博客文章。

再来看看我们的项目。在src/routes/blogs下面,有一个[slug].svelte的文件。那是什么呢?木有错—它就是一个模板,用于呈现所有的博客文章,而不管它们是什么。这意味着/blogs之后的任何标签都会被这个文件自动处理,我们可以在页面挂载时获取页面内容,然后将其呈现给浏览器。

那么这是否也就意味着/routes下的任何文件或文件夹都自动映射到一个URL呢?答案是肯定的,但是也有一个例外。就是如果在文件或文件夹前面加上下划线,Sapper不会将其转换为URL。这可以让我们很容易地将帮助文件放在routes文件夹中。

假设我们需要一个helper文件夹来存放所有helper文件。我们可以使用/routes/_helpers这样的文件夹,然后放置在/_helpers下的任何文件都不会被视为路由。完美!

Server routes

在前面的小节中,我们看到了有一个[slug].svelte文件,可以帮助我们匹配这样的任何url: /blogs/。但是它是如何让页面的内容呈现的呢?

我们其实可以从静态文件获取内容,也可以调用API来检索数据。无论哪种方式,您都需要向一个路由(或服务端,前提是如果我们只需要使用API)发出请求来检索数据。这就是Server routes的主场。

来自官方文档:“服务器路由是用‘.js’文件编写的模块,导出与HTTP方法对应的函数。”

这只是意味着服务器路由是您可以调用来执行特定操作(如保存数据、获取数据、删除数据等)的服务端。它可以理解为我们应用程序的后端,所以你在一个项目中可以拥有你需要的一切(当然,如果你想的话,你也可以分开它们)。

现在回到我们的启动项目。如何获取[slug].svelte中的每一篇博客文章的内容?打开这个文件,你看到的第一个代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script context="module">
export async function preload({ params, query }) {
// the `slug` parameter is available because
// this file is called [slug].html
const res = await this.fetch(`blog/${params.slug}.json`);
const data = await res.json();
if (res.status === 200) {
return { post: data };
} else {
this.error(res.status, data.message);
}
}
</script>

我们看到一个简单的JS函数,该函数发出GET请求并从该请求返回数据。 它以一个对象作为参数,然后在第2行对其进行结构分解以获取两个变量:params和query。

params和query包含什么? 在函数的开头打个log,然后在浏览器中打开博客文章,我们会得到如下提示:

1
{slug: "why-the-name"}slug: "why-the-name"__proto__: Object {}

嗯。因此,如果我们在第5行打开“why-the-name”帖子,我们的GET请求将是blog/why-the-name.json。然后在第6行将其转换为json对象。

在第7行,我们检查请求是否成功,如果是,则在第8行返回它,否则调用一个名为this.error的特殊方法。带有响应状态和错误消息的错误对象。

很简单。但是实际的服务器路由在哪里呢?它是什么样子的呢?查看src/routes/blog,你会看到一个[slug].json.js文件——这是我们的服务器路由。并注意它的命名方式与[slug] .svelte相同?这就是Sapper将服务器路由映射到页面路由的方式。如果你需要获取一个名为example的文件。Sapper将寻找example.json.js文件来处理请求。

现在我们来分析分析 [slug].json.js文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import posts from './_posts.js';

const lookup = new Map();
posts.forEach(post => {
lookup.set(post.slug, JSON.stringify(post));
});

export function get(req, res, next) {
// the `slug` parameter is available because
// this file is called [slug].json.js
const { slug } = req.params;
if (lookup.has(slug)) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(lookup.get(slug));
} else {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Not found`
}));
}
}

我们真正感兴趣的是从第8行开始。第3-6行只是为要处理的路由准备数据。还记得我们是如何在我们的页面路由中发出GET请求的吗?[slug].svelte是处理该请求的服务器路由。

如果我们熟悉Express.js,那么我们应该很熟悉它。因为这只是一个简单的服务。它所做的就是获取从请求对象传递给它的slug,在我们的数据存储中搜索它(在本例中是查找),然后在响应对象中返回它。

如果使用数据库,第12行可能类似于Posts.find({ where: { slug } })(Sequelize)。

服务器路由是包含接口的文件,可以从页面路由中调用它们。 因此,让我们快速了解一下到目前为止所了解的知识:

  • 页面路由是src/routes文件夹下的.svelte文件,用于将内容呈现给浏览器。

  • 服务器路由是包含API接口的.js文件,并按名称映射到特定的页面路由。

  • 页面路由可以调用服务器路由中定义的接口,以执行特定的操作,例如获取数据。

  • Sapper 是经过深思熟虑的!

Server-side rendering(SSR)

服务器端渲染(SSR)是让Sapper具有吸引力的重要原因。 如果您不知道SSR是什么或不知道为什么需要它,那么我们来简单解释解释。

默认情况下,Sapper首先在服务器端呈现所有应用程序,然后在客户端加载动态元素。这样一来,我们就可以做到两全其美,而不用因为分离做出任何妥协。

不过,这里有一个问题:尽管Sapper在支持第三方模块方面做得近乎完美,但有些模块需要访问window对象,而且如您所知,您不能从服务器端访问window。仅仅导入这样的模块将导致您的编译失败。

不过,不要烦恼;有一个简单的解决方法。Sapper允许您动态导入模块,因此不必在顶层导入模块。我们需要这么做:

1
2
3
4
5
6
7
8
9
10
11
12
<script>
import { onMount } from 'svelte';

let MyComponent;

onMount(async () => {
const module = await import('my-non-ssr-component');
MyComponent = module.default;
});
</script>

<svelte:component this={MyComponent} foo="bar"/>

在第2行,我们导入onMount函数。onMount函数内置于Svelte中,只有在组件挂载到客户端时才调用它(与React的componentDidMount等效)。

这意味着,当只在onMount执行的时候在其中导入有问题的模块,而不会在服务器上调用该模块,也不会出现缺少窗口对象的问题。您的代码编译成功,一切恢复正常。

这种方法还有另一个好处:因为我们正在为这个组件使用动态导入,所以实际上我们初始向客户端提供的代码更少。

结论

我们已经看到和Sapper一起工作是多么直观和简单。路由系统非常容易掌握,即使是初学者,创建一个API来支持你的前端也是相当简单的,SSR也非常容易实现。

这里有很多我们没有涉及的特性,包括预加载、错误处理、regex路由等。真正学习的方法就是用它在造东西。

既然我们已经了解了Sapper的基础知识,现在就可以开始使用它了。创建一个小项目,破坏,修复,四处乱搞,然后才能真正感受到Sapper是如何工作的。