使用 GatsbyJS 和 FaunaDB 构建动态 JAMstack 应用

❥ 赞助

在本文中,我们将解释单页应用程序 (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.jsGatsbyJS,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 作为我们的 完全托管的无服务器数据库解决方案。我们将构建一个应用程序,在该应用程序中,我们可以列出产品和评论。

让我们看一下使示例应用程序启动并运行需要执行的操作概述,然后详细介绍每个步骤。

  1. 设置新的数据库
  2. 向数据库添加 GraphQL 模式
  3. 使用模拟数据填充数据库
  4. 创建一个新的 GatsbyJS 项目
  5. 安装 NPM 包
  6. 为数据库创建服务器密钥
  7. 使用服务器密钥和新的只读密钥更新 GatsbyJS 配置文件
  8. 在构建时加载预渲染的产品数据
  9. 在运行时加载评论

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。

在组件函数中,我们使用了两个钩子:useStateuseQuery

useState 钩子跟踪我们想要加载评论的 productId。如果用户点击一个按钮,状态将设置为与该按钮对应的 productId。

然后,useQuery 钩子将此 productId 应用于从 FaunaDB 加载该产品的评论。钩子的 skip 参数阻止在首次渲染页面时执行查询,因为 productId 将为 null。

如果我们现在再次运行开发服务器并点击按钮,我们的应用程序应该按预期使用不同的 productId 执行查询。

$ npm run develop

结论

服务器端数据获取和客户端水合的组合使 JAMstack 应用程序非常强大。这些方法使我们能够灵活地与数据交互,以便我们能够满足不同的业务需求。

通常来说,在构建时加载尽可能多的数据来提升页面性能是一个好主意。但是,如果数据不是所有客户端都需要,或者太大而无法一次性发送给客户端,我们可以将其拆分并在客户端上切换到按需加载。对于用户特定数据、分页或任何变化频率较高且可能在到达用户之前就过时的数据,都是这种情况。

在这篇文章中,我们实现了一种方法,在构建时加载一部分数据,然后在用户与页面交互时在前端加载其余数据。

当然,我们还没有实现登录或表单来创建新的评论。我们该如何解决这个问题?这将是另一个教程的主题,在那个教程中我们可以使用 FaunaDB 的 基于属性的访问控制 来指定客户端密钥可以从前端读取和写入什么内容。

本教程的代码可以在 此仓库 中找到。