简介

写在开始之前

Sapper 尚处于早期开发阶段,在发布 1.0 版本之前,某些地方可能会存在改动。本文档在逐步完善过程中,如果你遇到了问题,请大方地在 Discord chatroom 求助。

请阅读 迁移指南 以帮助你升级到新版本。

Sapper 是什么?

Sapper 是一款用于构建超高性能的 Web 应用的框架。其中有两个基本概念:

  • 应用程序的每个页面都由 Svelte 组件组成
  • 你可以通过在项目的 src/routes 目录下添加文件来创建页面。文件的内容都会通过服务器来预渲染,以便确保用户首次访问的速度尽可能快,之后便由客户端应用程序接管。

遵循最新的最佳实践构建一个应用程序(包括代码拆分、离线支持、服务端渲染混合客户端渲染等)是非常复杂的。但是借助 Sapper 可以轻松实现以上这些功能,你只需挥洒自己的创意就行了。

虽然阅读此手册你并不需要懂得如何使用 Svelte,但如果懂的话会更有帮助。简单说,Svelte 是一款 UI 框架,它能将你的组件高度优化为原生 JavaScript 代码。请参考 这篇博文 和此 教程 以了解更多信息。

缘何此名?

在战争中,建造桥梁、修路、扫雷并拆除(在战斗条件下均如此)的士兵被称作 sappers(工兵)

对于 Web 开发者来说,危险性通常低于战斗工兵。但是我们也会面临一些风险环境:性能不足的设备、糟糕的网络以及前端工程固有的复杂度。Sapper(即 Svelte app maker 的缩写)使你无畏和忠实的战友!

与 Next.js 对比

Next.js 是由 Zeit 推出的 React 框架,也是 Sapper 的灵感来源。但两者之间有着明显的区别:

  • Sapper 是基于 Svelte 构建的而非 React, 因此速度更快且你的应用体积更小
  • 不同于常规的路由传参方式,我们将路由参数融入文件名中 (参见 路由 章节)
  • 除了创建 pages(页面文件) 的方式外, 还可以在 src/routes 目录下创建 server routes(服务端路由) 。 这样,你添加 JSON API (如,驱动页面的 JSON API)到程序中将会变得非常容易。 (参阅 /docs.json 章节)
  • 链接就是简单的 <a> 标签,这点区别于框架定制的 <Link> 组件。这就意味着即便是位于 markdown 内容中的链接也能如常访问(你所看到的这篇教程均是由 markdown 编写并动态渲染的)。

入门

构建一个 Sapper 应用程序的最快速方式是使用 degit 克隆 sapper-template 仓库的源码:

npx degit "sveltejs/sapper-template#rollup" my-app
# or: npx degit "sveltejs/sapper-template#webpack" my-app
cd my-app
npm install
npm run dev

你将会获得一个名为 my-app 的文件夹以开始你的新项目,接着安装依赖项,然后启动一个本地服务 localhost:3000。尝试编写代码以了解更多特性,你可能无需费心阅读本指南的其余部分,因为 Sapper 上手极为容易!

Sapper 应用程序结构

此章节是魔术揭秘部分。我们建议您先使用项目模板,然后在对 Sapper 项目有了基本认知之后再回到这里查看这部分的文档。

正如你所看到的 sapper-template 仓库中的内容,Sapper 的目录结构如下:

├ package.json
├ src
│ ├ routes
│ │ ├ # 路由文件放这里
│ │ ├ _error.svelte
│ │ └ index.svelte
│ ├ client.js
│ ├ server.js
│ ├ service-worker.js
│ └ template.html
├ static
│ ├ # 静态文件放这里
└ rollup.config.js / webpack.config.js

首次运行 Sapper 时,他将生成一个额外的名为 __sapper__ 的目录,用于存放生成的文件。

你会注意到还有一些额外的文件以及一个名为 cypress 的目录,此目录中存放的是 测试 相关的文件 — 我们暂且忽略这些文件。

可以 从头创建这些文件,但是最好还是使用模板。参见 入门 了解如何更容易地克隆这些文件

package.json

package.json 文件中包含了你的应用程序的依赖项和定义的若干脚本:

  • npm run dev — 以开发模式运行你的应用程序,并监听源文件变化。
  • npm run build — 以生产模式构建当前应用程序
  • npm run export — 构建当前应用程序的静态版本(如果支持的话)(参见 导出(exporting)
  • npm start — 以生产模式启动构建之后的应用程序
  • npm test — 运行测试脚本(参见 测试

src

该目录包含应用程序的三个 入口文件(entry points)src/client.jssrc/server.js 和(可选的) src/service-worker.js — 以及一个名为 src/template.html 的模板文件。

src/client.js

必须 在生成的 @sapper/app 模块中导入(import)并调用 start 函数:

import * as sapper from '@sapper/app';

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

在很多情况下,这就是入口模块的全部了,尽管你可以根据需要在此处执行任意操作。参见 客户端 API 章节以了解那些函数时你可以导入(import)的。

src/server.js

这个一个普通的 Express (或者 Polka 等) 应用程序,其有三个要求:

  • 应该使用类似 sirv 的工具来为 static 目录提供对外访问服务
  • 应该在最后调用 app.use(sapper.middleware()) 函数,其中 sapper 是从 @sapper/server 导入(import)的
  • 必须监听 process.env.PORT 端口

除此之外,您还可以根据自己的喜好编写服务器。

src/service-worker.js

Service workers 充当的是代理服务器,让你可以精确控制如何响应网络请求。例如,当浏览器请求 /goats.jpg 文件(画有山羊的图片)时,service worker 可以使用先前缓存的文件进行影响,或者可以将请求传递给服务器端,甚至可以使用完全不同的响应(例如返回一匹骆驼的图片)。

除了这些之外,这使得构建脱机工作的应用程序成为可能。

因为每个应用程序都需要一个略有不同的 service worker (有时是适合一直从缓存中获取内容;有时是只在没有连接的情况下才应该采取的最后手段),因此 Sapper 不会尝试控制 service worker。相反的,你可以在 service-worker.js 文件中编写业务逻辑。你可以从 @sapper/service-worker 中导入(import)任何内容:

  • filesstatic 目录下所有文件的列表数组
  • shell — 打包工具(Rollup 或 webpack)生成的客户端 JavaScript 文件
  • routes — 由 { pattern: RegExp } 对象组成的数组,可用于确定是否请求由 Sapper 控制的页面
  • timestamp — service worker 生成的时间(用于生成唯一的缓存名称)

src/template.html

此文件是服务器处理请求时所用的模板文件。Sapper 将用实际内容替换掉此模板中的以下标签:

  • %sapper.base% — 一个 <base> 元素(参见 base URLs
  • %sapper.styles% — 对当前请求的页面关键的 CSS
  • %sapper.head% — 代表页面特定 <head> 内容的 HTML,例如 <title>
  • %sapper.html% — 代表要渲染的页面主体部分的 HTML
  • %sapper.scripts% — 加载客户端应用程序的 script 标签

src/routes

这里包含了你的应用程序的精髓部分 - 页面和服务器端路由。参见 路由(routing) 章节了解详细内容。

static

这里存放了你的应用程序所使用的任何文件 - 字体、图像等。例如,static/favicon.png 文件可以通过 /favicon.png 链接来访问。

Sapper 并不为这些文件提供对外访问服务 — 因此,你通常需要使用 sirvserve-static 来支持这些文件 - 但是 Sapper 会读取 static 目录下的文件内容,以便你可以轻松地生成用于支持离线缓存的清单文件(参见 service-worker.js)。

rollup.config.js / webpack.config.js

Sapper 可以使用 Rollupwebpack 来打包你的应用程序。你一般不需要修改配置,但是如果你想修改的话(例如添加新的加载器或插件),当然没问题。

路由

如我们所见,Sapper 有两种路由类型:页面路由和服务器路由。

Pages

Pages are Svelte components written in .svelte files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel.

The filename determines the route. For example, src/routes/index.svelte is the root of your site:

<!-- src/routes/index.svelte -->
<svelte:head>
	<title>Welcome</title>
</svelte:head>

<h1>Hello and welcome to my site!</h1>

A file called either src/routes/about.svelte or src/routes/about/index.svelte would correspond to the /about route:

<!-- src/routes/about.svelte -->
<svelte:head>
	<title>About</title>
</svelte:head>

<h1>About this site</h1>
<p>TODO...</p>

Dynamic parameters are encoded using [brackets]. For example, here's how you could create a page that renders a blog post:

<!-- src/routes/blog/[slug].svelte -->
<script context="module">
	// the (optional) preload function takes a
	// `{ path, params, query }` object and turns it into
	// the data we need to render the page
	export async function preload(page, session) {
		// the `slug` parameter is available because this file
		// is called [slug].svelte
		const { slug } = page.params;

		// `this.fetch` is a wrapper around `fetch` that allows
		// you to make credentialled requests on both
		// server and client
		const res = await this.fetch(`blog/${slug}.json`);
		const article = await res.json();

		return { article };
	}
</script>

<script>
	export let article;
</script>

<svelte:head>
	<title>{article.title}</title>
</svelte:head>

<h1>{article.title}</h1>

<div class='content'>
	{@html article.html}
</div>

If you want to capture more params, you can create nested folders using the same naming convention: [slug]/[language].

If you don't want to create several folders to capture more than one parameter like [year]/[month]/..., or if the number of parameters is dynamic, you can use a spread route parameter. For example, instead of individually capturing /blog/[slug]/[year]/[month]/[day], you can create a file for /blog/[...slug].svelte and extract the params like so:

<!-- src/routes/blog/[...slug].svelte -->
<script context="module">
	export async function preload({ params }) {
		let [slug, year, month, day] = params.slug;

		return { slug, year, month, day };
	}
</script>

See the section on preloading for more info about preload and this.fetch

服务器路由

Server routes are modules written in .js files that export functions corresponding to HTTP methods. Each function receives HTTP request and response objects as arguments, plus a next function. This is useful for creating a JSON API. For example, here's how you could create an endpoint that served the blog page above:

// routes/blog/[slug].json.js
import db from './_database.js'; // the underscore tells Sapper this isn't a route

export async function get(req, res, next) {
	// the `slug` parameter is available because this file
	// is called [slug].json.js
	const { slug } = req.params;

	const article = await db.get(slug);

	if (article !== null) {
		res.setHeader('Content-Type', 'application/json');
		res.end(JSON.stringify(article));
	} else {
		next();
	}
}

delete is a reserved word in JavaScript. To handle DELETE requests, export a function called del instead.

文件命名规则

为用于定义路由的文件命名有以下三个简单的规则:

  • A file called src/routes/about.svelte corresponds to the /about route. A file called src/routes/blog/[slug].svelte corresponds to the /blog/:slug route, in which case params.slug is available to preload
  • The file src/routes/index.svelte corresponds to the root of your app. src/routes/about/index.svelte is treated the same as src/routes/about.svelte.
  • Files and directories with a leading underscore do not create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called src/routes/_helpers/datetime.js and it would not create a /_helpers/datetime route

错误页面(Error page)

In addition to regular pages, there is a 'special' page that Sapper expects to find — src/routes/_error.svelte. This will be shown when an error occurs while rendering a page.

The error object is made available to the template along with the HTTP status code.

路由中使用正则表达式

通过将正则表达式的子集放在参数名称后的括号中,可以使用正则表达式的子集来限定路由参数。

例如,src/routes/items/[id([0-9]+)].svelte 仅匹配数字 ID,因此,/items/123 是匹配的,而 /items/xyz 不匹配。

由于技术上的限制,不能使用以下字符:/\?:()

客户端 API

Sapper 根据你的应用程序生成的 @sapper/app 模块包含用以编程方式控制 Sapper 和响应事件的功能。

start({ target })

  • target — 将页面渲染到的目标元素

这将配置路由并启动应用程序:监听 <a> 元素上的点击事件、与 history API 交互,并渲染和更新 Svelte 组件。

返回一个 Promise,初始页面被 hydrated(水合)之后表示此 Promise 处于已解决(resolve)状态。

import * as sapper from '@sapper/app';

sapper.start({
	target: document.querySelector('#sapper')
}).then(() => {
	console.log('client-side app has started');
});

goto(href, options?)

  • href — 跳转的页面
  • options — 可以包含一个 replaceState 属性,该属性用于确定使用 history.pushState (默认值)还是 history.replaceState。不是必须的。

以编程的方式导航到给定的 href。如果目的地址是一个 Sapper 路由,则 Sapper 奖处理次导航,否则将使用新的 href 重新加载页面。换句话说,其行为就像用户点击带有此 href 的链接一样。

返回一个 Promise,导航完成时表示此 Promise 处于已解决(resolve)状态。导航完成后,可以使用它来执行某些操作,例如更新数据库、存储等。

import { goto } from '@sapper/app';

const navigateAndSave = async () => {
	await goto('/');
	saveItem();
}

const saveItem = () => {
	// do something with the database
}

prefetch(href)

  • href — 要预取的页面

以编程方式预取给定的页面,这意味着: a) 确保该页面的代码已加载,b) 使用适当的选项调用该页面的 preload 方法。当用户点击或将鼠标移动到带有 rel=prefetch 属性的 <a> 元素时,Sapper 会触发相同的行为。

返回一个 Promise,当预取完成时表示此 Promise 处于已解决(resolve)状态。

prefetchRoutes(routes?)

  • routes — 一个可选的字符串数组,代表需要预取的路由

以编程方式预取尚未获取的路由的代码。通常,你可以在 sapper.start() 完成后调用此函数,以加快后续的导航 (这就是 PRPL 模式 中 'L' 的意义)。省略参数的话将导致预取所有路由,或者你也可以使用任何匹配的路径名来指定路由,例如 /about(匹配 src/routes/about.svelte)或者 /blog/* (匹配 src/routes/blog/[slug].svelte)。与 prefetch 不同,prefetchRoutes 不会为单个页面调用 preload

返回一个 Promise,当所有路由都预取完成后表示此 Promise 处于已解决(resolve)状态。

预加载

路由 章节所讲的那样,页面组件可以有一个可选的 preload 函数用于加载页面依赖的某些数据,这与 Next.js 中的 getInitialProps 或 Nuxt.js 中的 asyncData 类似。

<script context="module">
	export async function preload(page, session) {
		const { slug } = page.params;

		const res = await this.fetch(`blog/${slug}.json`);
		const article = await res.json();

		return { article };
	}
</script>

<script>
	export let article;
</script>

<h1>{article.title}</h1>

It lives in a context="module" script — see the tutorial — because it's not part of the component instance itself; instead, it runs before the component is created, allowing you to avoid flashes while data is fetched.

参数

The preload function receives two arguments — page and session.

page is a { host, path, params, query } object where host is the URL's host, path is its pathname, params is derived from path and the route filename, and query is an object of values in the query string.

So if the example above was src/routes/blog/[slug].svelte and the URL was /blog/some-post?foo=bar&baz, the following would be true:

  • page.path === '/blog/some-post'
  • page.params.slug === 'some-post'
  • page.query.foo === 'bar'
  • page.query.baz === true

session is generated on the server by the session option passed to sapper.middleware. For example:

sapper.middleware({
	session: (req, res) => ({
		user: req.user
	})
})

返回值

If you return a Promise from preload, the page will delay rendering until the promise resolves. You can also return a plain object. In both cases, the values in the object will be passed into the components as props.

When Sapper renders a page on the server, it will attempt to serialize the resolved value (using devalue) and include it on the page, so that the client doesn't also need to call preload upon initialization. Serialization will fail if the value includes functions or custom classes (cyclical and repeated references are fine, as are built-ins like Date, Map, Set and RegExp).

上下文

Inside preload, you have access to three methods:

  • this.fetch(url, options)
  • this.error(statusCode, error)
  • this.redirect(statusCode, location)

this.fetch

In browsers, you can use fetch to make AJAX requests, for getting data from your server routes (among other things). On the server it's a little trickier — you can make HTTP requests, but you must specify an origin, and you don't have access to cookies. This means that it's impossible to request data based on the user's session, such as data that requires you to be logged in.

To fix this, Sapper provides this.fetch, which works on the server as well as in the client:

<script context="module">
	export async function preload() {
		const res = await this.fetch(`secret-data.json`, {
			credentials: 'include'
		});

		// ...
	}
</script>

Note that you will need to use session middleware such as express-session in your app/server.js in order to maintain user sessions or do anything involving authentication.

this.error

If the user navigated to /blog/some-invalid-slug, we would want to render a 404 Not Found page. We can do that with this.error:

<script context="module">
	export async function preload({ params, query }) {
		const { slug } = params;

		const res = await this.fetch(`blog/${slug}.json`);

		if (res.status === 200) {
			const article = await res.json();
			return { article };
		}

		this.error(404, 'Not found');
	}
</script>

The same applies to other error codes you might encounter.

this.redirect

You can abort rendering and redirect to a different location with this.redirect:

<script context="module">
	export async function preload(page, session) {
		const { user } = session;

		if (!user) {
			return this.redirect(302, 'login');
		}

		return { user };
	}
</script>

布局

到目前为止,我们已经将页面视为完全独立的组件,在导航过程中,现有组件将被销毁,而新组件将取代它。

但是在许多应用中,某些元素应该在 每个 页面上可见,例如顶级导航或页脚。我们可以使用 layout 组件,从而无需在每个页面中重复它们。

要创建适用于每个页面的布局组件,请创建一个名为 src/routes/_layout.svelte 的文件。默认的布局组件(如果你没有提供新的,则 Sapper 使用自带的)如下所示

<slot></slot>

因此我们可以添加所需的任何标记、样式和行为。例如,我们来添加一个导航栏:

<!-- src/routes/_layout.svelte -->
<nav>
	<a href=".">Home</a>
	<a href="about">About</a>
	<a href="settings">Settings</a>
</nav>

<slot></slot>

如果我们为 //about/settings 分别创建了页面:

<!-- src/routes/index.svelte -->
<h1>Home</h1>
<!-- src/routes/about.svelte -->
<h1>About</h1>
<!-- src/routes/settings.svelte -->
<h1>Settings</h1>

导航栏将始终可见,并且在三个页面之间切换时只会导致 <h1> 标签被替换。

嵌套路由

假设我们不仅有一个独立的 /settings 页面,还有共用同一个菜单的嵌套子页面,例如 /settings/profile/settings/notifications (参见 github.com/settings 所提供的一个实例)。

我们可以创建仅适用于 /settings 子页面的布局(同时继承带有顶级导航的根布局组件):

<!-- src/routes/settings/_layout.svelte -->
<h1>Settings</h1>

<div class="submenu">
	<a href="settings/profile">Profile</a>
	<a href="settings/notifications">Notifications</a>
</div>

<slot></slot>

布局组件接受一个 segment 属性,该属性对于样式之类的东西很有用:

+<script>
+    export let segment;
+</script>
+
<div class="submenu">
-    <a href="settings/profile">Profile</a>
-    <a href="settings/notifications">Notifications</a>
+    <a
+        class:selected={segment === "profile"}
+        href="settings/profile"
+    >Profile</a>
+
+    <a
+        class:selected={segment === "notifications"}
+        href="settings/notifications"
+    >Notifications</a>
</div>

服务端渲染

Sapper 默认开启服务端渲染 (SSR),然后再客户端重新挂载所有动态元素。Svelte 框架本身 对其具有很好的支持。这在页面性能和搜索引擎收录等方面都有好处,但也有其固有的复杂性存在。

让组件与 SSR 兼容

Sapper 在大多数情况下都能与第三方库很好配合。 然而有时第三方库以某种形式打包在一起,使其可以与多个不同模块加载器一起使用。这种情况下可能会创建对 window 对象的依赖,例如检查, window.global 是否存在。

由于像 sapper 这样的服务端渲染环境中不存在 window 对象,因此常规的导入此类模块可能不会生效,会报错并终止 sapper 服务程序:

ReferenceError: window is not defined

解决此问题的方法是将其放在 onMount 函数内部(仅在客户端上调用)去动态导入,这样就不会在服务端调用导入代码。

<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"/>

Stores

传递给 preload 函数的 pagesession 值与 preloading 一起作为 stores 供组件使用。

在组件内部,获取 stores 的引用如下:

<script>
	import { stores } from '@sapper/app';
	const { preloading, page, session } = stores();
</script>
  • preloading 包含一个只读的布尔(boolean)值,用于指示导航是否处于挂起(pending)状态
  • page 包含一个只读的 { host, path, params, query } 对象,与传递给 preload 函数的对象相同
  • session 包含服务器端的所有 session 数据。这是一个 可写的 store,这意味着你可以使用新的数据进行更新(例如,用户登录后),并且你的应用程序也将被刷新。

填充 session 数据

在服务器端,你可以通过向 sapper.middleware 传递一个参数来填充 session

// src/server.js
express() // or Polka, or a similar framework
	.use(
		serve('static'),
		authenticationMiddleware(),
		sapper.middleware({
			session: (req, res) => ({
				user: req.user
			})
		})
	)
	.listen(process.env.PORT);

Session 数据必须是(使用 devalue)可序列化的 — 不支持函数或自定义类(class),只支持 JavaScript 内置的数据类型。

预取

Sapper 使用代码分割将你的应用程序分成小块(每条路由一个),保证启动时间足够快速。

对于 动态 路由,例如 src/routes/blog/[slug].svelte 示例,这显然还不够。为了渲染该博客文章,我们需要为其获取数据,并且在 slug 对应的参数未知时是不能这样做的。在这种糟糕的情况下,这可能会导致延迟,因为浏览器会一直等待数据从服务端返回。

rel=prefetch

我们可以通过 prefetching(预取) 数据的方式来解决该问题。 为链接添加一个 rel=prefetch 属性:

<a rel=prefetch href='blog/what-is-sapper'>What is Sapper?</a>

Sapper 会在用户将鼠标悬停在链接上(在 PC 上)或触摸链接(在移动终端上)时立即运行页面的 preload 函数,而不是等待 click 事件发生了才触发导航。通常,这多出来的几百毫秒的差异,让用户界面响应速度将会无比快速丝滑。

rel=prefetch 是 Sapper 的特定属性,并不是 <a> 标签的固有属性。

构建

到目前为止,我们一直使用 sapper dev 来构建应用并运行开发服务器。但在生产环境中,我们需要构建出一个独立的优化版本。

sapper build

此命令将你的应用程序打包到 __sapper__/build 目录下。 (可以将其更改为自定义目录,也可以控制其他各种参数 — 执行 sapper build --help 命令以了解更多信息)。

输出的即是一个 Node 应用程序,可以从根目录下运行此应用程序:

node __sapper__/build

Exporting

Many sites are effectively static, which is to say they don't actually need an Express server backing them. Instead, they can be hosted and served as static files, which allows them to be deployed to more hosting environments (such as Netlify or GitHub Pages). Static sites are generally cheaper to operate and have better performance characteristics.

Sapper allows you to export a static site with a single zero-config sapper export command. In fact, you're looking at an exported site right now!

Static doesn't mean non-interactive — your Svelte components work exactly as they do normally, and you still get all the benefits of client-side routing and prefetching.

sapper export

Inside your Sapper project, try this:

# npx allows you to use locally-installed dependencies
npx sapper export

This will create a __sapper__/export folder with a production-ready build of your site. You can launch it like so:

npx serve __sapper__/export

Navigate to localhost:5000 (or whatever port serve picked), and verify that your site works as expected.

You can also add a script to your package.json...

{
	"scripts": {
		...
		"export": "sapper export"
	}
}

...allowing you to npm run export your app.

How it works

When you run sapper export, Sapper first builds a production version of your app, as though you had run sapper build, and copies the contents of your static folder to the destination. It then starts the server, and navigates to the root of your app. From there, it follows any <a> elements it finds, and captures any data served by the app.

Because of this, any pages you want to be included in the exported site must either be reachable by <a> elements or added to the --entry option of the sapper export command. Additionally, any non-page routes should be requested in preload, not in onMount or elsewhere.

When not to export

The basic rule is this: for an app to be exportable, any two users hitting the same page of your app must get the same content from the server. In other words, any app that involves user sessions or authentication is not a candidate for sapper export.

Note that you can still export apps with dynamic routes, like our src/routes/blog/[slug].svelte example from earlier. sapper export will intercept fetch requests made inside preload, so the data served from src/routes/blog/[slug].json.js will also be captured.

Route conflicts

Because sapper export writes to the filesystem, it isn't possible to have two server routes that would cause a directory and a file to have the same name. For example, src/routes/foo/index.js and src/routes/foo/bar.js would try to create export/foo and export/foo/bar, which is impossible.

The solution is to rename one of the routes to avoid conflict — for example, src/routes/foo-bar.js. (Note that you would also need to update any code that fetches data from /foo/bar to reference /foo-bar instead.)

For pages, we skirt around this problem by writing export/foo/index.html instead of export/foo.

部署

Sapper 应用程序可在支持 Node 8 或更高版本的任何环境运行。

部署到 Now

This section relates to Now 1, not Now 2

We can very easily deploy our apps to Now:

npm install -g now
now

This will upload the source code to Now, whereupon it will do npm run build and npm start and give you a URL for the deployed app.

For other hosting environments, you may need to do npm run build yourself.

部署为 service workers

Sapper makes the Service Worker file (service-worker.js) unique by including a timestamp in the source code (calculated using Date.now()).

In environments where the app is deployed to multiple servers (such as Now), it is advisable to use a consistent timestamp for all deployments. Otherwise, users may run into issues where the Service Worker updates unexpectedly because the app hits server 1, then server 2, and they have slightly different timestamps.

To override Sapper's timestamp, you can use an environment variable (e.g. SAPPER_TIMESTAMP) and then modify the service-worker.js:

const timestamp = process.env.SAPPER_TIMESTAMP; // instead of `import { timestamp }`

const ASSETS = `cache${timestamp}`;

export default {
	/* ... */
	plugins: [
		/* ... */
		replace({
			/* ... */
			'process.env.SAPPER_TIMESTAMP': process.env.SAPPER_TIMESTAMP || Date.now()
		})
	]
}

Then you can set it using the environment variable, e.g.:

SAPPER_TIMESTAMP=$(date +%s%3N) npm run build

When deploying to Now, you can pass the environment variable into Now itself:

now -e SAPPER_TIMESTAMP=$(date +%s%3N)

安全性

默认情况下,Sapper 是不会为你的应用程序添加安全标头(security headers)的,但是你哦可以使用 Helmet 等中间件自己添加安全标头。

内容安全策略 (CSP)

Sapper 生成内联(inline)的 <script> 标签,如果 内容安全策略 (CSP) 标头禁止执行任意脚本的话(unsafe-inline),则内联脚本会失败。

要解决此问题,Sapper 可以注入一个 nonce,从而通过中间件配置并返回适当的 CSP 标头。以下是一个使用 ExpressHelmet 的示例:

// server.js
import uuidv4 from 'uuid/v4';
import helmet from 'helmet';

app.use((req, res, next) => {
	res.locals.nonce = uuidv4();
	next();
});
app.use(helmet({
	contentSecurityPolicy: {
		directives: {
			scriptSrc: [
				"'self'",
				(req, res) => `'nonce-${res.locals.nonce}'`
			]
		}
	}
}));
app.use(sapper.middleware());

以这种方式使用 res.locals.nonce 应遵循 Helmet 的 CSP 文档 中的约定。

根路径

一般来说,你的 Sapper 应用程序的根目录位于 / 下。但是在某些情况下,你可能需要修改这个根路径,例如,你仅需要 Sapper 控制你的域名的一部分,或者你有多个 Sapper 应用同时运行。

你可以这么做:

// app/server.js

express() // or Polka, or a similar framework
	.use(
		'/my-base-path', // <!-- add this line
		compression({ threshold: 0 }),
		serve('static'),
		sapper.middleware()
	)
	.listen(process.env.PORT);

Sapper 将检测根路径,并配置相应的服务端和客户端路由。

如果你要 导出 你的应用程序,则需要告诉导出程序从哪里开始爬取:

sapper export --basepath my-base-path

测试

在 sapper 中你可以使用你所喜欢的任何测试框架和库。sapper-template 默认使用 Cypress 进行测试。

运行测试

npm test

这将启动服务器并打开 Cypress。你可以(应当!)在 cypress/integration/spec.js 中添加自己的测试用例。有关 Cyperss 的更多信息请参考其 文档

调试

使用 ndb 来调试你的服务端代码会很容易。首先全局安装 ndb:

npm install -g ndb

然后运行 sapper:

ndb npm run dev

上述示例假定 npm run dev 运行的是 sapper dev。你也可以使用 npx 来运行 Sapper,即 ndb npx sapper dev

需要注意的是,ndb 启动时可能在前几秒不会有任何输出提示。