静态网站生成器因其速度、安全性以及用户体验而广受欢迎。但是,有时您的应用程序需要在站点构建时无法获得的数据。React 是一个用于构建用户界面的库,它可以帮助您在客户端应用程序中检索和存储动态数据。
Fauna 是一个灵活的、无服务器的数据库,以 API 的形式提供,完全消除了容量规划、数据复制和计划维护等运营开销。Fauna 允许您将数据建模为文档,使其成为使用 React 编写的 Web 应用程序的自然选择。虽然您可以通过 JavaScript 驱动程序直接访问 Fauna,但这需要为连接到数据库的每个客户端进行自定义实现。通过将您的 Fauna 数据库置于 API 后面,您可以允许任何授权的客户端连接,而不管编程语言是什么。
Netlify Functions 允许您通过部署用作 API 端点的服务器端代码来构建可扩展的动态应用程序。在本教程中,您将使用 React、Netlify Functions 和 Fauna 构建一个无服务器应用程序。您将学习使用 Fauna 存储和检索数据的基础知识。您将创建和部署 Netlify Functions 以安全地访问 Fauna 中的数据。最后,您将把 React 应用程序部署到 Netlify。
开始使用 Fauna
Fauna 是一个分布式、强一致的 OLTP NoSQL 无服务器数据库,它符合 ACID 规范并提供多模型接口。Fauna 还支持来自单个查询的文档、关系、图和时间数据集合。首先,我们将从在 Fauna 控制台中创建数据库开始,方法是选择“数据库”选项卡,然后单击“创建数据库”按钮。

接下来,您需要创建一个集合。为此,您需要选择一个数据库,并在“集合”选项卡下单击“创建集合”。

Fauna 在持久化数据时使用特定的结构。该设计包含如下例所示的属性。
{
"ref": Ref(Collection("avengers"), "299221087899615749"),
"ts": 1623215668240000,
"data": {
"id": "db7bd11d-29c5-4877-b30d-dfc4dfb2b90e",
"name": "Captain America",
"power": "High Strength",
"description": "Shield"
}
}
请注意,Fauna 保留了一个ref
列,这是一个用于识别特定文档的唯一标识符。ts
属性是确定创建记录时间的时间戳,而data
属性负责数据。
为什么创建索引很重要
接下来,让我们为avengers
集合创建两个索引。这在项目的后面部分将非常有价值。您可以从“索引”选项卡或“Shell”选项卡创建索引,后者提供一个控制台来执行脚本。Fauna 支持两种类型的查询技术:FQL(Fauna 的查询语言)和 GraphQL。FQL 基于 Fauna 的模式运行,该模式包括文档、集合、索引、集合和数据库。
让我们从 shell 创建索引。

此命令将在集合上创建索引,这将通过数据对象内的id
字段创建索引。此索引将返回数据对象的 ref。接下来,让我们为 name 属性创建另一个索引,并将其命名为avenger_by_name
。
创建服务器密钥
要创建服务器密钥,我们需要导航到“安全”选项卡,然后单击“新建密钥”按钮。此部分将提示您为所选数据库和用户的角色创建密钥。

开始使用 Netlify Functions 和 React
在本节中,我们将了解如何使用 React 创建 Netlify 函数。我们将使用create-react-app来创建 React 应用程序。
npx create-react-app avengers-faunadb
创建 React 应用程序后,让我们安装一些依赖项,包括 Fauna 和 Netlify 依赖项。
yarn add axios bootstrap node-sass uuid faunadb react-netlify-identity react-netlify-identity-widget
现在让我们创建我们的第一个 Netlify 函数。要创建函数,首先需要全局安装 Netlify CLI。
npm install netlify-cli -g
现在 CLI 已安装,让我们在项目根目录中创建一个.env
文件,其中包含以下字段。
FAUNADB_SERVER_SECRET= <FaunaDB secret key>
REACT_APP_NETLIFY= <Netlify app url>
接下来,让我们看看如何开始创建 Netlify 函数。为此,我们需要在项目根目录中创建一个名为functions
的目录和一个名为netlify.toml
的文件,该文件将负责维护 Netlify 项目的配置。此文件定义了我们的函数目录、构建目录以及要执行的命令。
[build]
command = "npm run build"
functions = "functions/"
publish = "build"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200
force = true
我们将对 Netlify 配置文件进行一些额外的配置,例如本示例中的重定向部分。请注意,我们将 Netlify 函数的默认路径从/.netlify/**
更改为/api/
。此配置主要是为了改善 API URL 的外观和字段。因此,要触发或调用我们的函数,我们可以使用路径
https://domain.com/api/getPokemons
…而不是
https://domain.com/.netlify/getPokemons
接下来,让我们在functions
目录中创建我们的 Netlify 函数。但是,首先,让我们为 Fauna 创建一个连接文件,名为util/connections.js
,返回一个 Fauna 连接对象。
const faunadb = require('faunadb');
const q = faunadb.query
const clientQuery = new faunadb.Client({
secret: process.env.FAUNADB_SERVER_SECRET,
});
module.exports = { clientQuery, q };
接下来,让我们创建一个辅助函数来检查引用并返回,因为我们需要在整个应用程序中的多个地方解析数据。此文件将是util/helper.js
。
const responseObj = (statusCode, data) => {
return {
statusCode: statusCode,
headers: {
/* Required for CORS support to work */
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
},
body: JSON.stringify(data)
};
};
const requestObj = (data) => {
return JSON.parse(data);
}
module.exports = { responseObj: responseObj, requestObj: requestObj }
请注意,上述辅助函数处理CORS问题、JSON 数据的字符串化和解析。让我们创建我们的第一个函数getAvengers
,它将返回所有数据。
const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');
exports.handler = async (event, context) => {
try {
let avengers = await clientQuery.query(
q.Map(
q.Paginate(q.Documents(q.Collection('avengers'))),
q.Lambda(x => q.Get(x))
)
)
return responseObj(200, avengers)
} catch (error) {
console.log(error)
return responseObj(500, error);
}
};
在上面的代码示例中,您可以看到我们使用了几个 FQL 命令,例如Map、Paginate、Lamda。Map关键字用于迭代数组,它接受两个参数:一个数组和Lambda。我们将Paginate传递给第一个参数,它将检查引用并返回一页结果(一个数组)。接下来,我们使用了一个 Lamda 语句,这是一个匿名函数,与 ES6 中的匿名箭头函数非常相似。
接下来,让我们创建我们的函数AddAvenger
,它负责在集合中创建/插入数据。
const { requestObj, responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');
exports.handler = async (event, context) => {
let data = requestObj(event.body);
try {
let avenger = await clientQuery.query(
q.Create(
q.Collection('avengers'),
{
data: {
id: data.id,
name: data.name,
power: data.power,
description: data.description
}
}
)
);
return responseObj(200, avenger)
} catch (error) {
console.log(error)
return responseObj(500, error);
}
};
要保存特定集合的数据,我们将必须将数据传递到data:{}
对象,如上面的代码示例所示。然后我们需要将其传递给Create函数,并将其指向您想要的集合和数据。因此,让我们运行我们的代码,看看它是如何通过netlify dev
命令工作的。

让我们通过浏览器通过 URL https://#:8888/api/GetAvengers
触发 GetAvengers 函数。

上述函数将通过name
属性从avenger_by_name
索引中搜索来获取复仇者对象。但是,首先,让我们通过 Netlify 函数调用GetAvengerByName
函数。为此,让我们创建一个名为SearchAvenger
的函数。
const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');
exports.handler = async (event, context) => {
const {
queryStringParameters: { name },
} = event;
try {
let avenger = await clientQuery.query(
q.Call(q.Function("GetAvengerByName"), [name])
);
return responseObj(200, avenger)
} catch (error) {
console.log(error)
return responseObj(500, error);
}
};
请注意,Call
函数接受两个参数,其中第一个参数将是我们创建的 FQL 函数的引用,以及我们需要传递给该函数的数据。
通过 React 调用 Netlify 函数
现在几个函数都可用,让我们通过 React 使用这些函数。由于这些函数是 REST API,因此让我们通过Axios使用它们,并使用 React 的 Context API 进行状态管理。让我们从名为AppContext.js
的应用程序上下文开始。
import { createContext, useReducer } from "react";
import AppReducer from "./AppReducer"
const initialState = {
isEditing: false,
avenger: { name: '', description: '', power: '' },
avengers: [],
user: null,
isLoggedIn: false
};
export const AppContext = createContext(initialState);
export const AppContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(AppReducer, initialState);
const login = (data) => { dispatch({ type: 'LOGIN', payload: data }) }
const logout = (data) => { dispatch({ type: 'LOGOUT', payload: data }) }
const getAvenger = (data) => { dispatch({ type: 'GET_AVENGER', payload: data }) }
const updateAvenger = (data) => { dispatch({ type: 'UPDATE_AVENGER', payload: data }) }
const clearAvenger = (data) => { dispatch({ type: 'CLEAR_AVENGER', payload: data }) }
const selectAvenger = (data) => { dispatch({ type: 'SELECT_AVENGER', payload: data }) }
const getAvengers = (data) => { dispatch({ type: 'GET_AVENGERS', payload: data }) }
const createAvenger = (data) => { dispatch({ type: 'CREATE_AVENGER', payload: data }) }
const deleteAvengers = (data) => { dispatch({ type: 'DELETE_AVENGER', payload: data }) }
return <AppContext.Provider value={{
...state,
login,
logout,
selectAvenger,
updateAvenger,
clearAvenger,
getAvenger,
getAvengers,
createAvenger,
deleteAvengers
}}>{children}</AppContext.Provider>
}
export default AppContextProvider;
让我们在AppReducer.js
文件中为这个上下文创建 Reducer,它将包含应用程序上下文中每个操作的 reducer 函数。
const updateItem = (avengers, data) => {
let avenger = avengers.find((avenger) => avenger.id === data.id);
let updatedAvenger = { ...avenger, ...data };
let avengerIndex = avengers.findIndex((avenger) => avenger.id === data.id);
return [
...avengers.slice(0, avengerIndex),
updatedAvenger,
...avengers.slice(++avengerIndex),
];
}
const deleteItem = (avengers, id) => {
return avengers.filter((avenger) => avenger.data.id !== id)
}
const AppReducer = (state, action) => {
switch (action.type) {
case 'SELECT_AVENGER':
return {
...state,
isEditing: true,
avenger: action.payload
}
case 'CLEAR_AVENGER':
return {
...state,
isEditing: false,
avenger: { name: '', description: '', power: '' }
}
case 'UPDATE_AVENGER':
return {
...state,
isEditing: false,
avengers: updateItem(state.avengers, action.payload)
}
case 'GET_AVENGER':
return {
...state,
avenger: action.payload.data
}
case 'GET_AVENGERS':
return {
...state,
avengers: Array.isArray(action.payload && action.payload.data) ? action.payload.data : [{ ...action.payload }]
};
case 'CREATE_AVENGER':
return {
...state,
avengers: [{ data: action.payload }, ...state.avengers]
};
case 'DELETE_AVENGER':
return {
...state,
avengers: deleteItem(state.avengers, action.payload)
};
case 'LOGIN':
return {
...state,
user: action.payload,
isLoggedIn: true
};
case 'LOGOUT':
return {
...state,
user: null,
isLoggedIn: false
};
default:
return state
}
}
export default AppReducer;
由于应用程序上下文现在可用,我们可以从我们创建的 Netlify 函数中获取数据,并将它们持久化到我们的应用程序上下文中。因此,让我们看看如何调用其中一个函数。
const { avengers, getAvengers } = useContext(AppContext);
const GetAvengers = async () => {
let { data } = await axios.get('/api/GetAvengers);
getAvengers(data)
}
要将数据获取到应用程序上下文中,让我们从应用程序上下文中导入函数getAvengers
,并将 get 调用获取的数据传递给它。此函数将调用 reducer 函数,该函数将数据保存在上下文中。要访问上下文,我们可以使用名为avengers
的属性。接下来,让我们看看如何将数据保存到复仇者集合中。
const { createAvenger } = useContext(AppContext);
const CreateAvenger = async (e) => {
e.preventDefault();
let new_avenger = { id: uuid(), ...newAvenger }
await axios.post('/api/AddAvenger', new_avenger);
clear();
createAvenger(new_avenger)
}
上述newAvenger
对象是状态对象,它将保存表单数据。请注意,我们为每个文档传递了一个新的 uuid 类型的 id。因此,当数据保存在 Fauna 中时,我们将使用应用程序上下文中createAvenger
函数将数据保存在上下文中。类似地,我们可以通过 Axios 以这种方式使用 CRUD 操作调用所有 Netlify 函数。
如何将应用程序部署到 Netlify
现在我们已经拥有了一个可运行的应用程序,我们可以将其部署到 Netlify。有几种方法可以部署此应用程序
- 通过 GitHub 连接和部署应用程序
- 通过 Netlify CLI 部署应用程序
使用 CLI 会提示您输入特定的详细信息和选择,CLI 将处理其余操作。但在本例中,我们将通过 Github 部署应用程序。因此,首先,让我们登录 Netlify 仪表板并单击**“从 Git 创建新站点”**按钮。接下来,它会提示您选择需要部署的仓库以及站点的配置,例如构建命令、构建文件夹等。

如何通过 Netlify Identity 进行身份验证和授权函数
Netlify Identity 为您的应用程序提供了一套完整的身份验证功能,这将帮助我们管理整个应用程序中的已认证用户。Netlify Identity 可以轻松集成到应用程序中,无需使用任何其他第三方服务和库。要启用 Netlify Identity,我们需要登录到我们的 Netlify 仪表板,并在我们已部署的站点下,我们需要转到“身份”选项卡并启用身份功能。

启用身份将提供指向您的 Netlify 身份的链接。您需要复制该 URL 并将其添加到应用程序的 .env 文件中,用于**REACT_APP_NETLIFY
**。接下来,我们需要通过**netlify-identity-widget**和 Netlify 函数将 Netlify Identity 添加到我们的 React 应用程序中。但是,首先,让我们在index.js
文件中为身份上下文提供程序组件添加**REACT_APP_NETLIFY
**属性。
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import "react-netlify-identity-widget/styles.css"
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';
import { IdentityContextProvider } from "react-netlify-identity-widget"
const url = process.env.REACT_APP_NETLIFY;
ReactDOM.render(
<IdentityContextProvider url={url}>
<App />
</IdentityContextProvider>,
document.getElementById('root')
);
此组件是我们在本应用程序中使用的导航栏。此组件将位于所有其他组件之上,成为处理身份验证的理想位置。此**react-netlify-identity-widget**将添加另一个组件,该组件将处理用户的登录和注册。

接下来,让我们在我们的 Netlify 函数中使用 Identity。Identity 将对我们的函数进行一些小的修改,例如以下函数**GetAvenger
**。
const { responseObj } = require('./util/helper');
const { q, clientQuery } = require('./util/connection');
exports.handler = async (event, context) => {
if (context.clientContext.user) {
const {
queryStringParameters: { id },
} = event;
try {
const avenger = await clientQuery.query(
q.Get(
q.Match(q.Index('avenger_by_id'), id)
)
);
return responseObj(200, avenger)
} catch (error) {
console.log(error)
return responseObj(500, error);
}
} else {
return responseObj(401, 'Unauthorized');
}
};
每个请求的上下文将包含一个名为**clientContext
**的属性,该属性将包含已认证用户详细信息。在上面的示例中,我们使用一个简单的 if 条件来检查用户上下文。
为了在每个请求中获取**clientContext
**,我们需要通过授权标头传递用户令牌。
const { user } = useIdentityContext();
const GetAvenger = async (id) => {
let { data } = await axios.get('/api/GetAvenger/?id=' + id, user && {
headers: {
Authorization: `Bearer ${user.token.access_token}`
}
});
getAvenger(data)
}
一旦通过 netlify 身份小部件登录到应用程序,此用户令牌将在用户上下文中可用。
如您所见,Netlify 函数和 Fauna 看起来是构建无服务器应用程序的有希望的组合。您可以关注此GitHub仓库以获取完整代码,并参考此URL以获取工作演示。
结论
总之,Fauna和 Netlify 看起来是构建无服务器应用程序的有希望的组合。Netlify 还提供了通过插件扩展其功能的灵活性,以增强用户体验。按需付费的定价计划非常适合开发人员开始使用 Fauna。Fauna 速度极快,并且可以自动扩展,因此开发人员可以比以往任何时候都更有时间专注于开发。Fauna 可以处理您在关系型、文档型、图形型、时间序列数据库中发现的复杂数据库操作。Fauna 驱动程序支持所有主要语言,例如 Android、C#、Go、Java、JavaScript、Python、Ruby、Scala 和 Swift。凭借所有这些出色的功能,Fauna 看起来是最好的无服务器数据库之一。有关更多信息,请参阅Fauna 文档。
很棒的文章!我将在未来的项目中研究 Fauna 和 Netlify Functions!