在之前的文章中,我们展示了如何使用 FaunaDB 构建 GraphQL API。我们还写了一系列文章,解释了传统数据库为了实现全球可扩展性,必须采用最终一致性(而不是强一致性),或者在关系和索引可能性方面做出妥协。FaunaDB 不同,因为它没有做出这些妥协。它是为可扩展性而构建的,因此可以安全地为您的未来初创公司提供服务,无论它变得多么庞大,都不会牺牲关系和一致性数据。
在本文中,我们很高兴使用 React hook、FaunaDB 和 Cloudinary,在一个现实世界的应用程序中,以无服务器的方式将所有这些整合在一起,该应用程序具有高度动态的数据。我们将使用 Fauna 查询语言 (FQL) 而不是 GraphQL,并从一个仅包含前端的方法开始,该方法直接访问无服务器数据库 FaunaDB,用于数据存储、身份验证和授权。

对于使用特定技术的示例应用程序,黄金标准是待办事项应用程序,主要是因为它们很简单。任何数据库都可以为一个非常简单的应用程序提供服务并发光。
而这正是这个应用程序与众不同的地方!如果我们真的想展示 FaunaDB 如何在现实世界应用程序中脱颖而出,那么我们需要构建一些更高级的东西。
介绍 Fwitter
当我们最初在 Twitter 工作时,数据库很糟糕。当我们离开时,它们仍然很糟糕
Evan Weaver
由于 FaunaDB 是由前 Twitter 工程师开发的,他们亲身经历了这些限制,因此一个类似 Twitter 的应用程序感觉是一个合适的怀旧选择。而且,由于我们使用 FaunaDB 构建它,让我们称这个无服务器宝贝为“Fwitter”。
下面是一个简短的视频,展示了它的外观,完整源代码可以在 GitHub 上获取。
当您克隆仓库并开始深入研究时,您可能会注意到大量的注释良好的示例查询,这些查询在本篇文章中没有涵盖。那是因为我们将使用 Fwitter 作为我们未来文章中的首选示例应用程序,并随着时间的推移在其中构建更多功能。
但是,现在,以下是我们将在此处介绍的内容的简要概述
- 数据建模
- 设置项目
- 创建前端
- FaunaDB JavaScript 驱动程序
- 创建数据
- 使用 UDF 和 ABAC 角色保护您的数据
- 如何实现身份验证
- 添加 Cloudinary 用于媒体
- 检索数据
- 代码库中的更多内容
我们构建这些功能无需配置操作或为您的数据库设置服务器。由于 Cloudinary 和 FaunaDB 都是开箱即用的可扩展和分布式的,因此我们永远不必担心在多个区域设置服务器以实现对其他国家用户的低延迟。
让我们深入了解!
数据建模
在我们展示 FaunaDB 如何在关系方面脱颖而出之前,我们需要介绍应用程序数据模型中关系的类型。FaunaDB 的数据实体存储在文档中,然后存储在集合中,就像表中的行一样。例如,每个用户的详细信息将由存储在 Users 集合中的 User 文档表示。最终,我们计划支持单点登录和基于密码的登录方法,用于单个用户,每个方法将由 Accounts 集合中的 Account 文档表示。
此时,一个用户对应一个帐户,因此哪个实体存储引用(即用户 ID)并不重要。我们可以在 Account 或 User 文档中存储用户 ID,形成一对一关系。

但是,由于一个 User 最终将拥有多个 Accounts(或身份验证方法),因此我们将拥有一个一对多的模型。

在 Users 和 Accounts 之间的一对多关系中,每个 Account 仅指向一个用户,因此在 Account 上存储 User 引用是有意义的。

我们还有多对多关系,例如 Fweets 和 Users 之间的关系,因为用户可以通过点赞、评论和转发以复杂的方式相互交互。

此外,我们将使用第三个集合 Fweetstats 来存储有关用户和 Fweet 之间交互的信息。

Fweetstats 的数据将帮助我们确定,例如,是否将图标的颜色更改为指示用户是否已经点赞、评论或转发了某个 Fweet。它还有助于我们确定点击心形图标的含义:取消点赞或点赞。

应用程序的最终模型将如下所示:

Fweets 是模型的核心,因为它们包含 Fweet 最重要的数据,例如有关消息的信息、点赞次数、转发次数、评论次数以及附加的 Cloudinary 媒体。FaunaDB 以 JSON 格式存储此数据,如下所示:

如模型和此示例 JSON 所示,主题标签存储为引用列表。如果需要,我们可以将完整的主题标签 JSON 存储在这里,这是在缺乏关系的更有限的文档型数据库中的首选解决方案。但是,这意味着我们的主题标签将被复制到任何地方(就像在更有限的数据库中一样),并且查找主题标签或检索特定主题标签的 Fweets 也会变得更加困难,如下所示。

请注意,Fweet 不包含到 Comments 的链接,但 Comments 集合包含对 Fweet 的引用。那是因为一条 Comment 属于一条 Fweet,但一条 Fweet 可以有多条评论,类似于 Users 和 Accounts 之间的一对多关系。
最后,有一个 FollowerStats 集合,它基本上保存有关用户如何相互交互的信息,以便个性化各自的提要。我们在这篇文章中不会过多介绍,但您可以尝试使用源代码中的查询,并关注我们有关高级索引的未来文章。
希望您已经开始了解为什么我们选择了比待办事项应用程序更复杂的东西。尽管 Fwitter 远没有基于它的真实 Twitter 应用程序那么复杂,但已经很明显,在没有关系的情况下实现这样一个应用程序将是一件非常困难的事情。
现在,如果您还没有从 GitHub 仓库中进行操作,那么终于可以将我们的项目在本地运行了!
设置项目
要设置项目,请访问 FaunaDB 仪表板并注册。进入仪表板后,单击“新建数据库”,填写名称,然后单击“保存”。您现在应该位于新数据库的“概述”页面上。
接下来,我们需要一个密钥,我们将在设置脚本中使用它。单击左侧边栏中的“安全”选项卡,然后单击“新建密钥”按钮。
在“新建密钥”表单中,当前数据库应该已经被选中。对于“角色”,保留为“管理员”。可以选择添加一个密钥名称。接下来,单击“保存”并复制下一页显示的密钥密码。它将不再显示。

现在您已经拥有数据库密码,请克隆 git 仓库并按照自述文件进行操作。我们已经准备了一些脚本,因此您只需运行以下命令即可初始化应用程序、创建所有集合并填充数据库。脚本将提供更多说明。
// install node modules
npm install
// run setup, this will create all the resources in your database
// provide the admin key when the script asks for it.
// !!! the setup script will give you another key, this is a key
// with almost no permissions that you need to place in your .env.local as the
// script suggestions
npm run setup
npm run populate
// start the frontend
脚本运行后,您的 .env.local 文件应该包含脚本为您提供的引导密钥(不是管理员密钥)。
REACT_APP_LOCAL___BOOTSTRAP_FAUNADB_KEY=<bootstrap key>
您可以选择创建一个 Cloudinary 帐户,并将您的云名称和公共模板(有一个名为“ml_default”的默认模板,您可以将其设为公共)添加到环境中,以在推文中包含图像和视频。
REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME=<cloudinary cloudname>
REACT_APP_LOCAL___CLOUDINARY_TEMPLATE=<cloudinary template>
如果没有这些变量,包含媒体按钮将无法工作,但应用程序的其余部分应该可以正常运行。

创建前端

对于前端,我们使用 Create React App 生成一个应用程序,然后将应用程序划分为页面和组件。页面是具有自身 URL 的顶级组件。登录和注册页面不言自明。首页是来自我们关注的作者的标准推文提要;这是我们登录帐户后看到的页面。用户和标签页面按时间顺序倒序显示特定用户或标签的推文。
我们使用 React Router 根据 URL 将这些页面重定向,如 src/app.js 文件所示。
<Router>
<SessionProvider value={{ state, dispatch }}>
<Layout>
<Switch>
<Route exact path="/accounts/login">
<Login />
</Route>
<Route exact path="/accounts/register">
<Register />
</Route>
<Route path="/users/:authorHandle" component={User} />
<Route path="/tags/:tag" component={Tag} />
<Route path="/">
<Home />
</Route>
</Switch>
</Layout>
</SessionProvider>
</Router>
上面代码段中唯一需要注意的是 SessionProvider,它是一个 React 上下文,用于在登录后存储用户信息。我们将在身份验证部分重新审视它。现在,知道它可以让我们从每个组件访问 Account(以及 User)信息就足够了。
快速浏览一下主页 (src/pages/home.js
),看看我们如何使用钩子组合来管理数据。我们应用程序的大部分逻辑都在 FaunaDB 查询中实现,这些查询位于 src/fauna/querie
s 文件夹中。所有对数据库的调用都将通过 query-manager,在以后的文章中,我们将把它重构为无服务器函数调用。但目前这些调用来自前端,我们将使用 FaunaDB 的 ABAC 安全规则和用户定义函数 (UDF) 来保护其中的敏感部分。由于 FaunaDB 充当一个令牌安全的 API,我们无需担心连接数量的限制,就像在传统数据库中那样。
FaunaDB JavaScript 驱动程序
接下来,看看 src/fauna/query-manager.js
文件,了解我们如何使用 FaunaDB 的 JavaScript 驱动程序将 FaunaDB 连接到我们的应用程序,该驱动程序只是一个 节点模块,我们使用 `npm install` 安装了它。与任何节点模块一样,我们将它导入到我们的应用程序中,如下所示
import faunadb from 'faunadb'
并通过提供令牌来创建客户端。
this.client = new faunadb.Client({
secret: token || this.bootstrapToken
})
我们将在身份验证部分进一步介绍令牌。现在,让我们创建一些数据!
创建数据
在 src/fauna/queries/fweets.js
文件中可以找到创建新 Fweet 文档的逻辑。FaunaDB 文档就像 JSON 一样,每个 Fweet 都遵循相同的基本结构:
const data = {
data: {
message: message,
likes: 0,
refweets: 0,
comments: 0,
created: Now()
}
}
Now()
函数用于插入查询时间,以便用户提要中的 Fweet 可以按时间顺序排序。请注意,FaunaDB 会自动在每个数据库实体上放置时间戳 以进行时间查询。但是,FaunaDB 时间戳表示文档最后更新的时间,而不是创建的时间,并且每次 Fweet 被喜欢时,文档都会更新;为了我们预期的排序顺序,我们需要创建时间。
接下来,我们使用 Create() 函数将此数据发送到 FaunaDB。通过使用 Collection(‘fweets’)
向 Create()
提供对 Fweets 集合的引用,我们指定了数据需要去往何处。
const query = Create(Collection('fweets'), data )
我们现在可以将此查询包装在一个接受消息参数并使用 client.query()
执行它的函数中,该函数会将查询发送到数据库。只有在我们调用 client.query()
时,查询才会被发送到数据库并执行。在此之前,我们将尽可能多的 FQL 函数组合起来构建我们的查询。
function createFweet(message, hashtags) {
const data = …
const query = …
return client.query(query)
}
请注意,我们使用的是普通 JavaScript 变量来组成此查询,本质上只是调用函数。编写 FQL 就是关于函数组合;通过将小的函数组合成更大的表达式来构建查询。这种函数方法具有非常强大的优势。它允许我们使用原生语言特性(如 JavaScript 变量)来组成查询,同时还编写受保护的更高阶 FQL 函数,不受注入的影响。
例如,在下面的查询中,我们使用在其他地方使用 FQL 定义的 CreateHashtags()
函数向文档添加哈希标签。
const data = {
data: {
// ...
hashtags: CreateHashtags(tags),
likes: 0,
// ...
}
FQL 从驱动程序的主机语言(在本例中为 JavaScript)内部的工作方式是使 FQL 成为 eDSL(嵌入式领域特定语言)。CreateHashtags()
等函数就像原生 FQL 函数一样,它们都只是接收输入的函数。这意味着我们可以使用我们自己的函数轻松地扩展语言,就像 Fauna 社区的这个开源 FQL 库 一样。
同样重要的是要注意,我们在两个不同的集合中创建了两个实体,在一个事务中。因此,如果/当事情出错时,没有风险会创建 Fweet 而没有创建 Hashtags。从更专业的角度来说,FaunaDB 是事务性的且一致的,无论您是在多个集合上运行查询还是不是,这是一种 在可扩展的分布式数据库中很少见 的属性。
接下来,我们需要将作者添加到查询中。首先,我们可以使用 Identity()
FQL 函数来返回对当前登录文档的引用。如数据建模部分所述,该文档类型为 Account,与 Users 分开,以便在以后阶段支持 SSO。

然后,我们需要将 Identity()
包装在 Get()
中,以访问完整的 Account 文档,而不仅仅是它的引用。
Get(Identity())
最后,我们将所有这些内容包装在 Select()
中,以从帐户文档中选择 data.user
字段并将其添加到 data JSON 中。
const data = {
data: {
// ...
hashtags: CreateHashtags(tags),
author: Select(['data', 'user'], Get(Identity())),
likes: 0,
// ...
}
}
现在我们已经构建了查询,让我们将所有内容整合在一起并调用 client.query(query)
来执行它。
function createFweet(message, hashtags) {
const data = {
data: {
message: message,
likes: 0,
refweets: 0,
comments: 0,
author: Select(['data', 'user'], Get(Identity())),
hashtags: CreateHashtags(tags),
created: Now()
}
}
const query = Create(Collection('fweets'), data )
return client.query(query)
}
通过使用函数组合,您可以轻松地将所有高级逻辑组合在一个查询中,该查询将在一个事务中执行。查看 src/fauna/queries/fweets.js
文件以查看最终结果,该结果将进一步利用函数组合来添加速率限制等功能。
使用 UDF 和 ABAC 角色保护数据
细心的读者现在可能会有一些关于安全性的想法。我们本质上是在 JavaScript 中创建查询,并从前端调用这些查询。是什么阻止了恶意用户更改这些查询?
FaunaDB 提供了两个功能,使我们能够保护数据:基于属性的访问控制 (ABAC) 和用户定义函数 (UDF)。使用 ABAC,我们可以通过编写角色来控制特定密钥或令牌可以访问哪些集合或实体。
使用 UDF,我们可以使用 CreateFunction()
将 FQL 语句推送到数据库。
CreateFunction({
name: 'create_fweet',
body: <your FQL statement>,
})
一旦函数作为 UDF 存在于数据库中,应用程序就无法再更改它,然后我们从前端调用此 UDF。
client.query(
Call(Function('create_fweet'), message, hashTags)
)
由于查询现在保存在数据库中(就像存储过程一样),用户就无法再操作它了。
UDF 用于保护调用的一个示例是,我们不传入 Fweet 的作者。Fweet 的作者是从 Identity() 函数派生的,这使得用户无法以他人的名义写 Fweet。
当然,我们仍然需要定义用户是否有权调用 UDF。为此,我们将使用一个非常简单的 ABAC 角色,该角色定义了一组角色成员及其权限。该角色将命名为 logged_in_role
,其成员身份将包括 Accounts 集合中的所有文档,所有这些成员将被授予调用 create_fweet
UDF 的权限。
CreateRole(
name: 'logged_in_role',
privileges: [
{
resource: q.Function('create_fweet'),
actions: {
call: true
}
}
],
membership: [{ resource: Collection('accounts') }],
)
我们现在知道这些权限被授予了一个帐户,但是我们如何“成为”一个帐户?通过使用 FaunaDB Login() 函数对用户进行身份验证,如下一节所述。
如何在 FaunaDB 中实现身份验证

我们刚刚展示了一个角色,该角色授予 Accounts 调用 create_fweets
函数的权限。但是我们如何“成为”一个帐户呢?
首先,我们创建一个新的 Account 文档,将凭据与 Account 相关的任何其他数据(在本例中为电子邮件地址和对 User 的引用)一起存储。
return Create(Collection('accounts'), {
credentials: { password: password },
data: {
email: email,
user: Select(['ref'], Var('user'))
}
})
}
然后,我们可以对 Account 引用调用 Login()
,它将检索一个令牌。
Login(
Match( < Account reference > ,
{ password: password }
)
)
我们在客户端中使用此令牌来模拟 Account。由于所有 Accounts 都是 Account 集合的成员,因此此令牌满足 logged_in_role
的成员资格要求,并被授予调用 create_fweet
UDF 的权限。
为了启动整个过程,我们有两个非常重要的角色。
bootstrap_role
:只能调用login
和register
UDFlogged_in_role
:可以调用其他函数,例如create_fweet
您在运行设置脚本时收到的令牌本质上是用 bootstrap_role
创建的密钥。在 src/fauna/query-manager.js
中使用该令牌创建了一个客户端,该客户端只能注册或登录。登录后,我们使用从 Login()
返回的新令牌创建一个新的 FaunaDB 客户端,该客户端现在授予访问其他 UDF 函数(如 create_fweet
)的权限。注销意味着我们只是恢复到引导令牌。您可以在 src/fauna/query-manager.js
中看到此过程,以及 src/fauna/setup/roles.js
文件中更复杂的角色示例。
如何在 React 中实现会话
之前,在“创建前端”部分,我们提到了SessionProvider
组件。在 React 中,Provider 属于 React 上下文,它是一个方便不同组件之间共享数据的概念。这对于诸如用户的信息之类的需要在应用程序中无处不在的数据来说是理想的。通过在 HTML 中尽早插入SessionProvider
,我们确保了每个组件都可以访问它。现在,组件要访问用户详细信息,唯一需要做的就是导入上下文并使用 React 的“useContext”钩子。
import SessionContext from '../context/session'
import React, { useContext } from 'react'
// In your component
const sessionContext = useContext(SessionContext)
const { user } = sessionContext.state
但是用户是如何进入上下文的呢?当我们包含 SessionProvider 时,我们传入了一个包含当前状态和分发函数的值。
const [state, dispatch] = React.useReducer(sessionReducer, { user: null })
// ...
<SessionProvider value={{ state, dispatch }}>
状态只是当前状态,分发函数用于修改上下文。这个分发函数实际上是上下文的核心,因为创建上下文只涉及调用React.createContext()
,它会让你访问Provider
和Consumer
。
const SessionContext = React.createContext({})
export const SessionProvider = SessionContext.Provider
export const SessionConsumer = SessionContext.Consumer
export default SessionContext
我们可以看到,状态和分发是从 React 所谓的 reducer(使用React.useReducer
)中提取出来的,所以让我们写一个 reducer。
export const sessionReducer = (state, action) => {
switch (action.type) {
case 'login': {
return { user: action.data.user }
}
case 'register': {
return { user: action.data.user }
}
case 'logout': {
return { user: null }
}
default: {
throw new Error(`Unhandled action type: ${action.type}`)
}
}
}
这就是让你更改上下文的逻辑。本质上,它接收一个动作并根据该动作决定如何修改上下文。在我的情况下,动作只是一个带有字符串的类型。我们使用这个上下文来保存用户信息,这意味着我们在成功登录后使用它:
sessionContext.dispatch({ type: 'login', data: e })
添加 Cloudinary 用于媒体
当我们创建 Fweet 时,我们还没有考虑到资产。FaunaDB 旨在存储应用程序数据,而不是图像 blob 或视频数据。但是,我们可以轻松地将媒体存储在 Cloudinary 上,并在 FaunaDB 中保留一个链接。以下是在app.js
中插入 Cloudinary 脚本
loadScript('https://widget.cloudinary.com/v2.0/global/all.js')
然后我们在src/components/uploader.js
中创建一个 Cloudinary 上传小部件
window.cloudinary.createUploadWidget(
{
cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME,
uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE,
},
(error, result) => {
// ...
}
)
如前所述,你需要在环境变量(.env.local
文件)中提供 Cloudinary 云名称和模板才能使用此功能。创建一个 Cloudinary 帐户是免费的,一旦你拥有了一个帐户,你就可以从dashboard
中获取云名称。

你也可以选择使用 API 密钥来保护上传。在这种情况下,我们直接从前端上传,因此上传使用公共模板。要添加模板或修改它以使其公开,请单击顶部菜单中的齿轮图标,转到上传选项卡,然后单击添加上传预设。
你也可以编辑 ml_default 模板,并将其设为公开。

现在,当我们的媒体按钮被点击时,我们只需调用widget.open()
。
const handleUploadClick = () => {
widget.open()
}
return (
<div>
<FontAwesomeIcon icon={faImage} onClick={handleUploadClick}></FontAwesomeIcon>
</div>
)
这为我们提供了一个小的媒体按钮,当它被点击时会打开 Cloudinary 上传小部件。

当我们创建小部件时,我们也可以提供样式和字体,让它与我们自己的应用程序的外观和感觉一致,就像我们在上面(在src/components/uploader.js
)中所做的那样:
const widget = window.cloudinary.createUploadWidget(
{
cloudName: process.env.REACT_APP_LOCAL___CLOUDINARY_CLOUDNAME,
uploadPreset: process.env.REACT_APP_LOCAL___CLOUDINARY_TEMPLATE,
styles: {
palette: {
window: '#E5E8EB',
windowBorder: '#4A4A4A',
tabIcon: '#000000',
// ...
},
fonts: {
一旦我们把媒体上传到 Cloudinary,我们就会收到有关上传媒体的大量信息,然后在创建 Fweet 时将其添加到数据中。

然后,我们只需使用存储的id
(Cloudinary 称之为 publicId)以及 Cloudinary React 库(在src/components/asset.js
)中
import { Image, Video, Transformation } from 'cloudinary-react'
来在我们的 feed 中显示图像。
<div className="fweet-asset">
<Image publicId={asset.id}
cloudName={cloudName} fetchFormat="auto" quality="auto" secure="true" />
</div>
当你使用 id 而不是直接 URL 时,Cloudinary 会进行一系列优化,以尽可能最佳的格式提供媒体。例如,当你添加以下视频图像时
<div className="fweet-asset">
<Video playsInline autoPlay loop={true} controls={true} cloudName={cloudName} publicId={publicId}>
<Transformation width="600" fetchFormat="auto" crop="scale" />
</Video>
</div>
Cloudinary 会自动将视频缩放到 600 像素的宽度,并将其作为 WebM(VP9)传递给 Chrome 浏览器(482 KB),作为 MP4(HEVC)传递给 Safari 浏览器(520 KB),或者作为 MP4(H.264)传递给不支持这两种格式的浏览器(821 KB)。Cloudinary 在服务器端执行这些优化,从而显著提高页面加载时间和整体用户体验。
检索数据
我们已经展示了如何添加数据。现在我们还需要检索数据。获取 Fwitter feed 的数据有很多挑战。我们需要:
- 按特定顺序(考虑时间和受欢迎程度)获取你关注的人的 fweet
- 获取 fweet 的作者以显示他的个人资料图像和用户名
- 获取统计数据以显示有多少赞、转推和评论
- 获取评论以在 fweet 下面列出这些评论。
- 获取关于你是否已经喜欢、转推或评论过这个特定 fweet 的信息。
- 如果它是转推,则获取原始 fweet。
这种类型的查询从许多不同的集合中获取数据,需要高级索引/排序,但让我们从简单的地方开始。我们如何获取 Fweets?我们首先使用Collection()
函数获取对 Fweets 集合的引用。
Collection('fweets')
然后我们将其包装在Documents()
函数中以获取所有集合的文档引用。
Documents(Collection('fweets'))
然后我们遍历这些引用。
Paginate(Documents(Collection('fweets')))
Paginate()
需要一些解释。在调用Paginate()
之前,我们有一个查询返回了一组假设的数据。Paginate()
实际上将这些数据物化为我们可以读取的实体页面。FaunaDB 要求我们使用这个Paginate()
函数来保护我们免于编写检索集合中每个文档的低效查询,因为在为大规模构建的数据库中,该集合可能包含数百万个文档。如果没有Paginate()
的保护,这将变得非常昂贵!
让我们将这个部分查询保存在一个普通的 JavaScript 变量references
中,以便我们可以在其上继续构建。
const references = Paginate(Documents(Collection('fweets')))
到目前为止,我们的查询只返回一个指向我们的 Fweets 的引用列表。要获取实际的文档,我们只需执行我们在 JavaScript 中所做的操作:使用匿名函数遍历列表。在 FQL 中,Lambda 只是一个匿名函数。
const fweets = Map(
references,
Lambda(['ref'], Get(Var('ref')))
)
如果你习惯于像 SQL 这样的声明式查询语言,它声明你想要什么并让数据库找出如何获取它,这可能看起来很冗长。相反,FQL 声明你想要什么以及你想要它如何,这使得它更过程化。由于你是定义如何获取数据的人,而不是查询引擎,因此查询的价格和性能影响是可预测的。你可以在不执行查询的情况下准确地确定此查询的读取次数,如果你的数据库包含大量数据并且是按使用付费的,这是一个巨大的优势。所以可能会有一个学习曲线,但它值得你花时间和精力去学习。一旦你了解了 FQL 的工作原理,你就会发现查询就像普通的代码一样易读。
让我们准备我们的查询,通过引入 Let 使其易于扩展。Let 将允许我们绑定变量并立即在下一个变量绑定中重用它们,这使你可以更优雅地构建查询。
const fweets = Map(
references,
Lambda(
['ref'],
Let(
{
fweet: Get(Var('ref'))
},
// Just return the fweet for now
Var('fweet')
)
)
)
现在我们有了这个结构,获取额外的数据很容易。所以让我们获取作者。
const fweets = Map(
references,
Lambda(
['ref'],
Let(
{
fweet: Get(Var('ref')),
author: Get(Select(['data', 'author'], Var('fweet')))
},
{ fweet: Var('fweet'), author: Var('author') }
)
)
)
虽然我们没有写一个联接,但我们已经将 Users(作者)与 Fweets 联接在一起。我们将在后续文章中进一步扩展这些构建块。同时,浏览src/fauna/queries/fweets.js
以查看最终查询和更多示例。
代码库中的更多内容
如果你还没有,请打开代码库,这是一个 Fwitter 示例应用程序。你会发现很多我们没有在这里探索的带注释的示例,但将在以后的文章中介绍。本节涉及几个我们认为你应该查看的文件。
首先,查看src/fauna/queries/fweets.js
文件,了解如何使用 FaunaDB 的索引进行复杂匹配和排序(索引是在src/fauna/setup/fweets.js
中创建的)。我们实现了三种不同的访问模式,以便按受欢迎程度和时间、按用户名和按标签获取 Fweets。

按受欢迎程度和时间获取 Fweets 是一种特别有趣的访问模式,因为它实际上是根据用户之间的互动对 Fweets 进行排序,这是一种衰减的受欢迎程度。
此外,请查看src/fauna/queries/search.js
,我们在这里实现了基于 FaunaDB 索引和索引绑定的自动完成,以搜索作者和标签。由于 FaunaDB 可以跨多个集合索引,所以我们可以编写一个索引来支持对 Users 和 Tags 的自动完成类型搜索。

我们实现了这些示例,因为灵活而强大的索引与关系的结合在可扩展的分布式数据库中是罕见的。缺少关系和灵活索引的数据库要求你提前知道如何访问你的数据,当你需要更改业务逻辑以适应客户不断变化的使用案例时,你将遇到问题。
在 FaunaDB 中,如果你没有预见你想访问数据的特定方式,不用担心 - 只需添加一个索引!我们有范围索引、术语索引和复合索引,这些索引可以在你想要的时候指定,而无需围绕最终一致性进行编码。
即将发布的预览
如引言中所述,我们推出这款 Fwitter 应用程序是为了展示复杂、真实的用例。也就是说,一些功能仍在开发中,将在以后的文章中介绍,包括流媒体、分页、基准测试以及更高级的安全性模型,包括短期令牌、JWT 令牌、单点登录(可能使用 Auth0 之类的服务)、基于 IP 的速率限制(使用 Cloudflare worker)、电子邮件验证(使用 SendGrid 之类的服务)和 HttpOnly cookie。

最终结果将是一个依赖于服务和无服务器函数的堆栈,这与动态 JAMstack 应用程序非常相似,只是缺少静态站点生成器。请关注后续文章,并确保订阅 Fauna 博客,并关注 CSS-Tricks 上更多与 FaunaDB 相关的文章。
我认为有一个错字“user”
谢谢!已经更新了
这简直太棒了!谢谢,期待后续内容 :)
谢谢 Brian!
请继续关注,如果您喜欢它,请帮助我们传播!:)
发现这篇文章真的很有趣。我使用 Fauna 在工作中为金融工具玩过很多次,但发现自己经常一头雾水。这篇文章用一个不是待办事项列表的示例解释了很多东西。期待下一篇文章!
非常感谢!:)
如果您再次发现自己一头雾水,请加入我们的社区 Slack:http://community.fauna.com/
您会发现我们的社区非常乐于助人。
做得太好了,无法形容这篇文章对我的帮助有多大。我非常期待后续文章!
谢谢!这种评论让一切都值得!:)
好文章!
期待 SSO 集成。
精彩的帖子。等待承诺的下一篇文章,其中包括 Cloudflare worker。
谢谢,并为此事道歉。本来应该更快完成的!
不过仍然计划中!我正在努力完成其他工作,然后回来继续!:)
嗨 Brecht
谢谢您。我从这里学到了很多关于 Fauna 的知识。我迫不及待地想看后续文章!
感谢您的阅读。后续文章仍在创作中。不过进度缓慢,可能会略有调整,因为还有很多其他工作要做。
难以置信的内容。我一直试图学习 Fauna,以便与我的 React 应用程序结合使用。谢谢。期待更多内容。
太感谢了!:)
非常感谢!
我刚完成一个类似于您的项目(除了使用 Next.js 构建 React,并使用 useSWR+Graphql-request 而不是 FQL)。
感谢分享,并表示敬意
太棒了,我应该深入研究一下。
很棒的文章!
我最大的问题是,您用什么制作了这些很棒的数据库图表