在本文中,我们将解释单页应用程序 (SPA) 和静态网站之间的区别,以及如何使用 GatsbyJS 和 FaunaDB 在动态 JAMstack 应用中将两者的优势结合起来。我们将构建一个应用程序,该应用程序在构建时从 FaunaDB 中提取一些数据,预渲染 HTML 以快速交付给客户端,然后在用户与页面交互时加载其他数据。这种技术的组合为我们提供了静态生成网站和 SPA 的最佳属性。
简而言之……
重量级后端、单页应用程序、静态网站
在过去,当 JavaScript 还很新的时候,它主要用于提供效果和改进交互。这里有一些动画,那里有一个下拉菜单,仅此而已。繁重的工作由后端的 Perl、Java 或 PHP 执行。
随着时间的推移,这种情况发生了变化:客户端代码变得越来越重,JavaScript 承担了越来越多的前端工作,直到我们最终发送了几乎为空的 HTML 并渲染了浏览器中的整个 UI,让后端为我们提供 JSON 数据。
这导致了关注点的清晰分离,并使我们能够使用 JavaScript 构建整个应用程序,称为单页应用程序 (SPA)。SPA 最重要的优势是无需重新加载。您可以点击链接更改显示内容,而无需触发页面的完全重新加载。这本身就提供了一种 卓越的用户体验。但是,SPA 显著增加了客户端代码的大小;客户端现在必须等待多个延迟的总和
- 服务延迟:从服务器检索 HTML 和 JavaScript,其中 JavaScript 比以前更大
- 数据加载延迟:加载客户端请求的附加数据
- 前端框架渲染延迟:一旦接收到数据,React、Vue 或 Angular 等前端框架仍然需要执行大量工作来构建最终的 HTML
一个皇室隐喻

我们可以将加载 SPA 比作玩具城堡的建造和交付。客户端需要检索 HTML 和 JavaScript,然后检索数据,然后仍然需要组装页面。构建块已交付,但它们在交付后仍需要组装在一起。
如果有一种方法可以提前建造城堡就好了……
进入 JAMstack
JAMstack 应用程序由JavaScript、API 和Markup 组成。借助当今的静态网站生成器,例如 Next.js 和 GatsbyJS,JavaScript 和 Markup 部分可以捆绑到一个静态包中,并通过内容交付网络 (CDN) 部署,该网络将文件交付到浏览器。CDN 将捆绑包和其他资产地理分布到多个位置。当用户的浏览器获取捆绑包和资产时,它可以从网络上最接近的位置接收它们,从而减少服务延迟。
继续我们的玩具城堡类比,JAMstack 应用与 SPA 的不同之处在于页面(或城堡)是预先组装交付的。由于我们一次性接收完整的城堡,不再需要构建它,因此我们的延迟更低。
使用水合使静态 JAMstack 应用变得动态
在 JAMstack 方法中,我们从动态应用程序开始,并预渲染静态 HTML 页面以便通过快速的 CDN 传递。但是,如果完全静态的网站不够用,并且我们需要在用户与各个组件交互时支持一些动态内容,而无需重新加载整个页面怎么办?这就是客户端水合发挥作用的地方。
水合是客户端进程,其中服务器端渲染的 HTML (DOM) 通过我们的前端框架“注入”事件处理程序和/或动态组件,使其更具交互性。这可能很棘手,因为它取决于将原始 DOM 与用户与页面交互时内存中保留的新虚拟 DOM (VDOM) 进行协调。如果 DOM 和 VDOM 树不匹配,则可能会出现错误,导致元素以错误的顺序显示,或者需要重建页面。
幸运的是,像 GatsbyJS 和 NextJS 这样的库旨在最大程度地减少此类与水合相关的错误的可能性,开箱即用地为您处理所有操作,只需几行代码即可。结果是一个动态 JAMstack Web 应用程序,它同时比等效的 SPA 更快且更具动态性。
还有一个技术细节需要说明:动态数据将来自哪里?
分布式前端友好型数据库!
JAMstack 应用通常依赖于 API(即 JAM 中的“A”),但如果我们需要加载任何类型的自定义数据,则需要一个数据库。而传统的数据库对于其他通过 CDN 传递的全球分布式站点来说仍然是性能瓶颈,因为传统数据库仅位于一个区域。与其使用传统数据库,我们希望我们的数据库位于分布式网络上,就像 CDN 一样,从尽可能靠近客户端的位置提供数据。这种类型的数据库称为分布式数据库。
在此示例中,我们将选择 FaunaDB,因为它也具有强一致性,这意味着无论我的客户端从哪里访问数据,数据都将相同,并且数据不会丢失。与 JAMstack 应用程序配合特别好的其他功能是 (a) 数据库作为 API(GraphQL 或 FQL)访问,不需要打开连接,以及 (b) 数据库具有一个安全层,可以从前端安全地访问公共和私有数据。这意味着我们可以在不扩展后端的情况下保持 JAMstack 的低延迟,所有这些都无需任何配置。
让我们比较一下加载水合静态网站与构建玩具城堡的过程。由于 CDN,我们仍然具有较低的延迟,但数据量也更少,因为大部分站点是静态生成的,因此需要较少的渲染。在城堡(或页面动态部分)交付后,只需要组装一小部分。
使用 GatsbyJS 和 FaunaDB 的示例应用程序
让我们构建一个示例应用程序,该应用程序在构建时从 FaunaDB 加载数据并将其渲染为静态 HTML,然后在运行时在客户端浏览器中加载其他动态数据。对于此示例,我们使用 GatsbyJS,这是一个基于 React 的 JAMstack 框架,它预渲染静态 HTML。由于我们使用 GatsbyJS,因此我们可以完全使用 React 编写我们的网站代码,生成和传递静态页面,然后在运行时动态加载其他数据。我们将使用 FaunaDB 作为我们的 完全托管的无服务器数据库解决方案。我们将构建一个应用程序,在该应用程序中,我们可以列出产品和评论。
让我们看一下使示例应用程序启动并运行需要执行的操作概述,然后详细介绍每个步骤。
- 设置新的数据库
- 向数据库添加 GraphQL 模式
- 使用模拟数据填充数据库
- 创建一个新的 GatsbyJS 项目
- 安装 NPM 包
- 为数据库创建服务器密钥
- 使用服务器密钥和新的只读密钥更新 GatsbyJS 配置文件
- 在构建时加载预渲染的产品数据
- 在运行时加载评论
1. 设置新的数据库
在开始之前,请在 dashboard.fauna.com 上创建一个帐户。拥有帐户后,让我们设置一个新的数据库。它应该包含产品及其评论,以便我们可以在构建时加载产品,并在浏览器中加载评论。
2. 向数据库添加 GraphQL 模式
接下来,我们使用服务器密钥将 GraphQL 模式上传到我们的数据库。为此,我们创建一个名为 schema.gql 的新文件,其内容如下所示
type Product {
title: String!
description: String
reviews: [Review] @relation
}
type Review {
username: String!
text: String!
product: Product!
}
type Query {
allProducts: [Product]
}
您可以通过 FaunaDB 控制台上传您的 schema.gql 文件,方法是点击左侧边栏上的“GraphQL”,然后点击“导入模式”按钮。
在向 FaunaDB 提供 GraphQL 模式后,它会自动为我们模式中的实体(产品和评论)创建所需的集合。此外,它还会创建与这些集合进行有意义且高效交互所需的索引。您现在应该会看到一个 GraphQL Playground,您可以在其中进行测试
3. 使用模拟数据填充数据库
要使用产品和评论填充我们的数据库,我们可以使用 dashboard.fauna.com 上的 Shell:
要创建一些数据,我们将使用 Fauna 查询语言 (FQL),之后我们将继续使用 GraphQL 来构建我们的示例应用程序。将以下 FQL 查询粘贴到 Shell 中以创建三个产品文档
Map(
[
{ title: "Screwdriver", description: "Drives screws." },
{ title: "Hair dryer", description: "Dries your hair." },
{ title: "Rocket", description: "Flies you to the moon and back." }
],
Lambda("product",
Create(Collection("Product"), { data: Var("product") })
)
);
然后,我们可以编写一个查询,检索我们刚刚创建的产品并为每个产品文档创建一个评论文档
Map(
Paginate(Match(Index("allProducts"))),
Lambda("ref", Create(Collection("Review"), {
data: {
username: "Tina",
text: "Good product!",
product: Var("ref")
}
}))
);
这两种类型的文档都将通过 GraphQL 加载。但是,产品和评论之间存在显着差异。前者不会发生很大变化并且相对静态,而后者则由用户驱动。GatsbyJS 允许我们以两种方式加载数据
- 在构建时加载的数据,将用于生成静态站点。
- 在客户端访问并与您的网站交互时实时加载的数据。
在此示例中,我们选择在构建时加载产品,并在浏览器中按需加载评论。因此,我们获得了由 CDN 提供服务的静态 HTML 产品页面,用户可以立即看到。然后,当用户与产品页面交互时,我们加载评论的数据。
4. 创建一个新的 GatsbyJS 项目
以下命令将根据启动模板创建一个 GatsbyJS 项目
$ npx gatsby-cli new hello-world-gatsby-faunadb
$ cd hello-world-gatsby-faunadb
5. 安装 npm 包
为了使用 Gatsby 和 Apollo 构建我们的新项目,我们需要一些额外的包。我们可以使用以下命令安装这些包:
$ npm i gatsby-source-graphql apollo-boost react-apollo
我们将使用 **gatsby-source-graphql** 作为将 GraphQL API 链接到构建过程的一种方式。使用此库,您可以进行 GraphQL 调用,其结果将自动作为 React 组件的属性提供。这样,您可以使用动态数据静态生成您的应用程序。**apollo-boost** 包是一个易于配置的 GraphQL 库,将用于在客户端获取数据。最后,Apollo 和 React 之间的链接将由 **react-apollo** 库处理。
6. 为数据库创建服务器密钥
我们将创建一个服务器密钥,Gatsby 将使用它来预渲染页面。请记住将密钥复制到某个位置,因为我们稍后会使用它。请仔细保管服务器密钥,因为它们可用于创建、销毁或管理分配给它们的数据库。要创建密钥,我们可以转到 Fauna 仪表板并在安全选项卡中创建密钥。
7. 使用服务器和新的只读密钥更新 GatsbyJS 配置文件
要将 GraphQL 支持添加到我们的构建过程中,我们需要将以下代码添加到我们 plugins 部分中的 graphql-config.js 中,我们将在其中插入我们刚才生成的 FaunaDB 服务器密钥。
{
resolve: "gatsby-source-graphql",
options: {
typeName: "Fauna",
fieldName: "fauna",
url: "https://graphql.fauna.com/graphql",
headers: {
Authorization: "Bearer <SERVER KEY>",
},
},
}
为了使 GraphQL 访问在浏览器中工作,我们必须创建一个仅具有从集合读取数据的权限的密钥。FaunaDB 具有一个广泛的安全层,您可以在其中定义它。最简单的方法是转到 dashboard.fauna.com 上的 FaunaDB 控制台,然后通过点击左侧边栏中的“安全”,然后“管理角色”,然后“新建自定义角色”为您的数据库创建新角色
将新的自定义角色命名为“ClientRead”,并确保添加所有集合和索引(这些是通过导入 GraphQL 模式创建的集合)。然后,为每个集合和索引选择“读取”。您的屏幕应如下所示
您可能已经注意到此页面上的“成员资格”选项卡。尽管在本教程中我们没有使用它,但它非常有趣,值得解释一下,因为它是一种获取安全令牌的替代方法。在“成员资格”选项卡中,可以指定 FaunaDb 中的集合实体(假设我们有一个“用户”集合)是特定角色的成员。这意味着,如果您模拟该集合中的其中一个实体,则角色权限将适用。您可以通过将凭据与实体关联并使用 登录 函数来模拟数据库实体(例如,用户),该函数将返回一个令牌。这样,您也可以在 FaunaDb 中实现基于密码的身份验证。在本教程中我们不会使用它,但如果您对此感兴趣,请查看 FaunaDB 的 身份验证教程。
现在让我们暂时忽略“成员资格”,创建角色后,我们可以使用新角色创建一个新密钥。与之前一样,点击“安全”,然后“新建密钥”,但这次从“角色”下拉列表中选择“ClientRead”
现在,让我们将此只读密钥插入 gatsby-browser.js 配置文件中,以便能够从浏览器调用 GraphQL API
import React from "react"
import ApolloClient from "apollo-boost"
import { ApolloProvider } from "react-apollo"
const client = new ApolloClient({
uri: "https://graphql.fauna.com/graphql",
request: operation => {
operation.setContext({
headers: {
Authorization: "Bearer <CLIENT_KEY>",
},
})
},
})
export const wrapRootElement = ({ element }) => (
<ApolloProvider client={client}>{element}</ApolloProvider>
)
GatsbyJS 将渲染其 Router 组件作为根元素。如果我们想在客户端的应用程序中的任何地方使用 ApolloClient
,我们需要使用 ApolloProvider
组件包装此根元素。
8. 在构建时加载预渲染的产品数据
现在一切都已设置好,我们终于可以编写实际代码来加载我们的数据了。让我们从将在构建时加载的产品开始。
为此,我们需要修改 src/pages/index.js 文件使其如下所示
import React from "react"
import { graphql } from "gatsby"
Import Layout from "../components/Layout"
const IndexPage = ({ data }) => (
<Layout>
<ul>
{data.fauna.allProducts.data.map(product => (
<li>{product.title} - {product.description}</li>
))}
</ul>
</Layout>
)
export const query = graphql`
{
fauna {
allProducts {
data { _id title description }
}
}
}
`
export default IndexPage
导出的查询将自动被 GatsbyJS 捕获并在渲染 IndexPage 组件之前执行。该查询的结果将作为 data 属性传递到 IndexPage 组件中。如果我们现在运行开发脚本,我们可以在开发服务器上的 http://localhost:8000/
上看到预渲染的文档。
$ npm run develop
9. 在运行时加载评论
要在客户端加载产品的评论,我们必须对 src/pages/index.js 进行一些更改:
import { gql } from "apollo-boost"
import { useQuery } from "@apollo/react-hooks"
import { graphql } from "gatsby"
import React, { useState } from "react"
import Layout from "../components/layout"
// Query for fetching at build-time
export const query = graphql
`
{
fauna {
allProducts {
data {
_id title description
}
}
}
}
`
// Query for fetching on the client
const GET_REVIEWS = gql
`
query GetReviews($productId: ID!) {
findProductByID(id: $productId) {
reviews {
data {
_id username text
}
}
}
}
`
const IndexPage = props => {
const [productId, setProductId] = useState(null)
const { loading, data } = useQuery(GET_REVIEWS, {
variables: {
productId
},
skip: !productId,
})
}
export default IndexPage
让我们一步一步地完成此步骤。
首先,我们需要导入 apollo-boost 和 apollo-react 包的部分内容,以便我们可以使用之前在 gatsby-browser.js 文件中设置的 GraphQL 客户端。
然后,我们需要实现我们的 GET_REVIEWS
查询。它尝试按 ID 查找产品,然后加载该产品的关联评论。该查询采用一个变量,即 productId。
在组件函数中,我们使用了两个钩子:useState
和 useQuery
useState 钩子跟踪我们想要加载评论的 productId。如果用户点击一个按钮,状态将设置为与该按钮对应的 productId。
然后,useQuery
钩子将此 productId
应用于从 FaunaDB 加载该产品的评论。钩子的 skip 参数阻止在首次渲染页面时执行查询,因为 productId 将为 null。
如果我们现在再次运行开发服务器并点击按钮,我们的应用程序应该按预期使用不同的 productId 执行查询。
$ npm run develop
结论
服务器端数据获取和客户端水合的组合使 JAMstack 应用程序非常强大。这些方法使我们能够灵活地与数据交互,以便我们能够满足不同的业务需求。
通常来说,在构建时加载尽可能多的数据来提升页面性能是一个好主意。但是,如果数据不是所有客户端都需要,或者太大而无法一次性发送给客户端,我们可以将其拆分并在客户端上切换到按需加载。对于用户特定数据、分页或任何变化频率较高且可能在到达用户之前就过时的数据,都是这种情况。
在这篇文章中,我们实现了一种方法,在构建时加载一部分数据,然后在用户与页面交互时在前端加载其余数据。
当然,我们还没有实现登录或表单来创建新的评论。我们该如何解决这个问题?这将是另一个教程的主题,在那个教程中我们可以使用 FaunaDB 的 基于属性的访问控制 来指定客户端密钥可以从前端读取和写入什么内容。
本教程的代码可以在 此仓库 中找到。