使用 Netlify Functions 和 React 访问数据

静态网站生成器因其速度、安全性以及用户体验而广受欢迎。但是,有时您的应用程序需要在站点构建时无法获得的数据。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 命令,例如MapPaginateLamdaMap关键字用于迭代数组,它接受两个参数:一个数组和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。有几种方法可以部署此应用程序

  1. 通过 GitHub 连接和部署应用程序
  2. 通过 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 文档