原文链接: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编译两种方式。
# 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文件:
src/client.js
src/server.js
-
src/service-worker.js
(这个是可选的)
client.js
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等等,但是有一些规则还是要遵循的:
-
服务器必须提供/static文件夹的内容。Sapper不在乎你用来做什么。但是必须要有那个文件夹!
-
采用的服务器框架必须支持middlewares,并且必须使用从@sapper/server里面使用sapper.middleware()导入。
-
采用的服务器必须监听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/<any_url>。但是它是如何让页面的内容呈现的呢?
我们其实可以从静态文件获取内容,也可以调用API来检索数据。无论哪种方式,您都需要向一个路由(或服务端,前提是如果我们只需要使用API)发出请求来检索数据。这就是Server routes的主场。
来自官方文档:“服务器路由是用‘.js’文件编写的模块,导出与HTTP方法对应的函数。”
这只是意味着服务器路由是您可以调用来执行特定操作(如保存数据、获取数据、删除数据等)的服务端。它可以理解为我们应用程序的后端,所以你在一个项目中可以拥有你需要的一切(当然,如果你想的话,你也可以分开它们)。
现在回到我们的启动项目。如何获取[slug].svelte中的每一篇博客文章的内容?打开这个文件,你看到的第一个代码是这样的:
<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,然后在浏览器中打开博客文章,我们会得到如下提示:
{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文件。
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允许您动态导入模块,因此不必在顶层导入模块。我们需要这么做:
<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是如何工作的。
网友评论