用CockroachDB和Netlify函数构建一个完整的Jamstack应用程序

166 阅读15分钟

为了帮助人们进行户外活动并改善他们的身体健康状况,我们将创建一个户外活动追踪器。虽然我们不会涉及用户管理等方面,但该应用程序将有一个输入页面,用户可以输入诸如徒步旅行、滑冰、游泳和足球等活动,并提供诸如日期、持续时间、距离和描述等细节。该应用程序的活动页面将列出数据库中的活动,并让用户按日期、距离和持续时间等字段进行排序。

Jamstack的网络应用架构包括JavaScript、API和标记。静态网站生成器,如React-Static、Hugo或Gatsby,为前端应用程序提供静态标记。

让我们来探讨如何使用React-Static来生成我们的静态Jamstack应用程序。这个应用程序将使用Netlify函数在CockroachDB中存储数据,并使用Netlify为终端用户提供服务。

要遵循这个教程,你只需要知道一些JavaScript和基本的Git操作,剩下的我们会指导你完成。

前提条件

要学习本教程,你应该具备以下条件。

准备应用资源

本教程在Jamstack应用程序中的CockroachDB上存储数据。CockroachDB是一个高可用的分布式SQL数据库。你可以在你的电脑上使用它,也可以通过CockroachDB无服务器平台使用。

在本节中,我们将向免费的CockroachDB无服务器测试版集群中的数据库编写SQL查询,以存储来自客户端应用程序的数据。我们使用Netlify Functions来管理连接应用程序和CockroachDB集群的无服务器函数。

在下一节中,我们将在CockroachDB Serverless上创建一个免费集群,然后安装必要的工具,将我们的系统连接到集群上。在第三部分,我们将在集群内为我们的应用程序创建一个数据库。

在CockroachDB Serverless上创建一个数据库集群

**注意:**如果你已经在CockroachDB Serverless上有一个集群,你可以跳过这一节,进入下一节。

要在CockroachDB Serverless上创建一个集群,如果我们是新用户,必须先在CockroachDB Serverless上创建一个免费账户。注册完账户后,我们按照提示在测试模式下创建一个新的免费集群

我们可以保留默认值,或者在创建集群页面上指定集群名称、区域和云提供商。

 specify a cluster name, region, and cloud provider on the Create your cluster page

接下来,我们点击创建你的免费数据库。我们的集群在5到10秒内就准备好了。

一旦我们创建了我们的集群,连接信息模式就会打开,显示集群的连接细节。我们确保注意集群的密码,因为关闭模态后我们就看不到了。要做到这一点,我们把鼠标悬停在或点击REVEAL_PASSWORD来显示集群密码,然后把密码复制到记事本中。

hover over or click REVEAL_PASSWORD to show the cluster password

现在我们看到新创建的群集在群集页面上列出。

newly-created cluster listed on the <strong>Clusters</strong> page

如上图所示,我们是该集群的默认根用户。在下一节,我们将从我们的计算机连接到集群,并在集群中创建一个数据库。

将Cockroach Shell连接到集群上

为了从我们的机器上连接到CockroachDB无服务器集群,我们可以使用任何Postgres客户端驱动,因为CockroachDB支持Postgres Wire协议CockroachDB文档中的PostgreSQL兼容性部分解释了它与PostgreSQL的兼容性,包括PostgreSQL不支持的功能。 如果我们使用Linux操作系统,我们执行下面的命令,用curl下载CockroachDB CLI,并解压到我们系统的bin目录中进行安装。

 curl https://binaries.cockroachdb.com/cockroach-v21.1.5.linux-amd64.tgz | tar -xz
  && sudo cp -i cockroach-v21.1.5.linux-amd64/cockroach /usr/local/bin/

或者,如果我们使用Mac电脑,我们执行下面的命令,使用curl下载CockroachDB CLI ,并解压到我们系统的bin目录下。

 curl https://binaries.cockroachdb.com/cockroach-v21.1.5.darwin-10.9-amd64.tgz |
  tar -xJ && cp -i cockroach-v21.1.5.darwin-10.9-amd64/cockroach /usr/local/bin/

接下来,我们将下面的命令粘贴到我们的终端,将CLUSTER_ID 占位符替换为我们先前创建的集群的ID。然后,我们执行下面的命令来下载证书授权(CA)证书文件。

 curl --create-dirs -o ~/.postgresql/root.crt -O
  https://cockroachlabs.cloud/clusters/<CLUSTER_ID>/cert

注意:通过CockroachDB无服务器控制台在连接信息模式中找到你的集群ID。点击 "连接"打开模态,然后点击 "**连接字符串 "**标签,显示下载我们集群的CA文件的步骤,其中包含我们的集群ID。

Find your cluster ID in the Connection Info modal

我们执行下面的命令,使用Cockroach CLI的curl连接方法连接到云端集群。我们用之前显示的连接信息模版的集群证书替换占位符的值。

cockroach sql --url='postgresql://<USERNAME>@<HOST>:26257/defaultdb?sslmode=verify-full&sslrootcert='$HOME'/.postgresql/root.crt&options=--cluster=<CLUSTER_NAME>-<cluster-id>'

上面的命令中传递给-url参数的连接字符串包括集群的USERNAME,DATABASE,HOST, 和PASSWORD 凭证。它还包括我们在上一步中保存在电脑上的集群的CA证书文件的路径。

在认证之后,我们有一个Cockroach shell,可以访问我们的云集群。我们现在可以通过执行SQL查询对集群进行操作。例如,SHOW DATABASES 命令显示集群内的默认数据库,像这样。

Default databases within a cluster

注意:你可以尝试使用CockroachDB的SQL Playground来安全地操作CockroachDB的样本数据库。

应用数据的建模

建立数据库模式的模型是开发我们的活动追踪器应用程序的下一步。这个数据库的结构很简单,用一个表来存储活动,与另一个表没有关系。

在活跃的Cockroach shell中,我们执行下面的SQL查询,为Jamstack应用程序创建一个数据库。

CREATE DATABASE jamstack;

现在,我们从默认的defaultdb 数据库切换到新的Jamstack数据库。这个动作使我们能够在不指定数据库的情况下执行查询,因为Jamstack是默认的。

接下来,我们通过在活跃的Cockroach shell中执行下面的SQL查询,在Jamstack数据库中创建一个活动表。

CREATE TABLE activities (ID varchar(125) PRIMARY KEY, Name varchar(75) NOT NULL,
  Description varchar(255) NOT NULL, Activity_Type varchar(55) NOT NULL, Duration
  varchar(55) NOT NULL, Distance varchar(55) NOT NULL, Date_Created date NOT NULL
  DEFAULT CURRENT_DATE);

在执行了上面的查询后,我们可以通过获取新的结构来确认我们创建了这个表。

接下来,我们执行下面的SQL命令来获取Activities表的表结构。\d activities;

上面的命令应该打印出一个类似下面的表结构。

 table structure

在上面的图片中,我们可以看到Activities表内每一列的属性。

通过在CockroachDB Serverless上配置云数据库,我们已经准备好了Jamstack应用程序的数据层,并使用CockroachDB配置了一个表和相应的列。现在我们可以通过\q command ,退出Cockroach shell。

我们的下一步是在Netlify函数中使用数据库来存储来自网络应用的用户活动。

创建Netlify函数

Netlify Functions是事件驱动的无服务器函数,我们在Netlify上进行部署和管理。每个函数都是一个JavaScript文件,它作为一个端点,我们通过向端点的URL发出HTTP请求来调用。

Netlify函数的JavaScript文件与React-Static网络应用程序在同一目录下。当我们把它部署到Netlify时,它们也在同一个项目中。

为了开始工作,我们使用npm在新的终端标签中执行这个命令,在我们的电脑上全局安装React-StaticNetlifyCLI。npm install -g react-static netlify-cli

安装完React-Static CLI后,我们通过执行这个命令引导一个新的项目。

react-static create

该命令启动了一个交互式安装程序,引导我们创建一个新的项目。在安装程序的模板部分,我们选择基本选项,用一个基本的博客模板和一些定义的路由来引导一个应用程序。

bootstrap an application with a basic blog template

注意:在本教程中,我们把这个应用程序称为Jamstack。请随意使用你喜欢的名字。

现在,我们用这个命令将目录改为新创建的项目目录:cd Jamstack接下来,我们安装六个新的软件包,在网络应用和Netlify功能中使用。

yarn add dotenv pg pg-format uuid react-helmet react-icons

我们也执行下面的init命令,直接从我们的命令行上创建一个Netlify的项目。创建一个项目将使我们能够在Netlify项目中远程存储环境变量。

注意:如果这是你第一次使用Netlify CLI创建一个项目,你的默认浏览器将启动,让你用你的Netlify帐户来验证Netlify CLI。

netlify init

如下图所示,屏幕提示我们为我们的Netlify项目提供一个独特的名字,或者使用一个随机生成的名字。

provide a unique name for our Netlify project

创建项目后,我们使用Netlify CLI中的env命令来设置所需的环境变量。

我们从CockroachDB无服务器仪表盘的连接信息模式中获得了下面使用的凭证。我们以下面的格式存储凭证,将角括号中的占位符替换成我们相应的凭证。

注意:我们从连接信息模式中复制的数据库包括CLUSTER_NAME.TENANT_ID.DATABASE.数据库的名称是defaultdb

在通过下面的命令设置DATABASE 变量时,用“jamstack” 替换“defaultdb” 。例如,“fire-camel-841.defaultdb” 应该改为“fire-camel-841.jamstack”jamstack 是我们在上一节中创建的数据库的名称。

DB_CERT 环境变量将读取root.crt文件的内容,该文件是我们在设置本地Cockroach shell到CockroachDB集群时下载的。

netlify env:set DATABASE <DATABASE>
netlify env:set HOST <HOST>
netlify env:set DB_CERT "$(cat $HOME/.postgresql/root.crt)"
netlify env:set USERNAME <USER>
netlify env:set PASSWORD <PASSWORD>
netlify env:set PORT <PORT>

Netlify函数使用上面的凭证,通过dotenv包与我们的CockroachDB建立连接。

接下来我们在我们喜欢的代码编辑器或集成开发环境(IDE)中打开新的React-Static项目,为Netlify函数创建新的文件。

使用我们的代码编辑器或IDE,我们现在在项目(根)目录下创建一个netlify.toml 文件来存储这个项目的Netlify配置。我们将下面的代码块粘贴到netlify.toml 文件中。

[dev]
port=5050

[build]
publish = "dist"
command = "npm run build"

[[headers]]
for = "/*"

[headers.values]
Access-Control-Allow-Origin = "*"

我们在上面以TOML格式指定的以下配置配置了dev,它建立了一个Netlify功能的环境,如下所示。

  • 头部配置块指定了Netlify函数每次收到请求时应用到的请求头。

接下来,我们执行下面的mkdir 命令,在项目目录中分别创建两个目录嵌套,以存储Netlify Functions的JavaScript文件。mkdir -p netlify/functions

注意如果你使用的是Windows操作系统,上述命令中的-p标志将不起作用,因此你需要手动创建嵌套目录。

现在,函数目录是空的。在下一步,我们将在设置Netlify dev后创建并实现函数端点,以便在我们的计算机上本地运行这些函数。

使用我们的代码编辑器,我们在package.json 文件中的scripts对象中添加start:function 命令,如下图所示,以启动Netlify dev服务器。

"scripts": {
  "start:functions": "netlify dev",
   "start": "react-static start",
   "stage": "react-static build --staging",
   "build": "react-static build",
   "analyze": "react-static build --analyze",
   "serve": "serve dist -p 3000"
 }

当它匹配并执行时,上面的start:functions 字段会使用netlify.toml 文件中指定的配置启动Netlify dev服务器。

现在,我们执行下面的命令,使用package.json 脚本对象中匹配的命令键来启动Netlify dev服务器。

yarn start:functions

Netlify dev server在终端启动,打印出项目的功能指标,就像下面的图片。

The Netlify dev server

创建新的活动Netlify功能

现在,我们在netlify/functions 目录内制作一个create-activity.js 文件,并添加下面的代码块来实现函数内的应用逻辑。

const { Pool } = require("pg");
const format = require("pg-format");
const { v4 } = require("uuid");

const client = new Pool({
  user: process.env.USERNAME,
  host: process.env.HOST,
  database: process.env.DATABASE,
  password: process.env.PASSWORD,
  port: process.env.PORT,
  ssl : {
    ca: process.env.DB_CERT
  }
});

exports.handler = async ({ body }, context, callback) => {
 const { duration, activity_type, distance, description, name } =
   JSON.parse(body);

 try {
   const clientPool = await client.connect();

   const uuid = v4();
   const sqlStatement = format(
     "INSERT INTO activities(id, name, description , activity_type, duration, distance, date_created) VALUES(%L, %L, %L, %L, %L, %L, %L)",
     uuid,
     name,
     description,
     activity_type,
     duration,
     distance,
     new Date()
   );


	try {
  	await clientPool.query(sqlStatement);

  	return {
    	statusCode: 200,
    	body: JSON.stringify({ response: `${activity_type} activity created` }),
  	};
	} catch (e) {
  	return {
    	statusCode: 422,
    	body: JSON.stringify({ response: "Error inserting activity" }),
  	};
	}
 } catch (error) {
   clientPool.release();

   return {
     statusCode: 500,
     body: JSON.stringify({
      error,
       message: "An internal error occurred. Try again later",
     }),
   };
 }
};

上面的整个代码块处理了一个POST 请求,使用请求体中的数据向CockroachDB集群插入一个新的活动记录。下面的步骤实现了这个任务。

首先,我们通过使用节点pg驱动创建一个连接池来连接到Cockroach集群,并将集群凭证和自签证书文件作为参数传递给池类

注意:我们使用的是node-pg提供的数据库连接池的默认配置。如果你正在建立一个更复杂的应用程序,建议你通过配置,并根据你的需要进行配置。

代码从函数请求的事件参数中解构请求主体中的JSON数据。它解析了请求主体,然后进一步从JSON数据中分解出持续时间、activity_typedistancename 、和description 的值。

我们创建一个SQL查询,使用Nodepg-format包插入新的活动记录,以在查询中包括去结构的活动细节。

注意:我们没有使用ES6模板字面直接将活动细节插入到SQL查询语句中,而是使用pg-format来减少SQL注入攻击风险。为了进一步降低SQL注入攻击的风险,我们可以使用ValidatorJoi等验证库来验证每个活动细节。

pg驱动将SQL语句作为参数传递给客户端类中的查询方法。它包括一个回调,从执行的查询中返回一个错误和一个对象。

为了测试这个功能,我们从一个新的终端窗口执行下面的命令。我们可以使用curl向/create-activity函数发出一个POST 请求,请求体中有一个样本活动的细节。

注意:我们也可以使用我们喜欢的API测试工具,如PostmanInsomnia,来测试功能端点。

curl -X POST -H "Content-Type: application/json"  http://localhost:5050/.netlify/functions/create-activity -d '{"activity_type":"Hiking", "distance":"2KM", "name":"Trip to the mountains", "description":"My personal trip to the high mountains","duration":"2 Hours"}'

在控制台执行上述请求后,下面的JSON格式的响应表明,该函数成功运行并创建了一个新的活动。

$ -> { "response" : "Hiking activity was created"}

在这一点上,我们已经创建了第一个Netlify函数来插入一个新的用户活动记录到我们的CockroachDB。下一步是创建另一个函数来接收数据库中的所有用户活动记录。

创建用户活动记录的检索函数

接下来,我们创建Netlify函数来从数据库中检索用户活动。首先,我们用下面的代码创建一个 activities.js 文件。

const { Pool } = require("pg");
const format = require("pg-format");

  const client = new Pool({
  host: process.env.HOST,
  database: process.env.DATABASE,
  password: process.env.PASSWORD,
  port: process.env.PORT,
  user: process.env.USERNAME,
  ssl: {
    ca : process.env.DB_CERT
   },
  });

  exports.handler = async ({ queryStringParameters }, context, callback) => {
   const { order } = queryStringParameters;

   try {
    const clientPool = await client.connect();

     const sqlStatement = format(
       "SELECT * FROM activities ORDER BY date_created %s",
       order || "ASC"
     );

     const { rows } = await clientPool.query(sqlStatement);

     return {
       statusCode: 200,
       body: JSON.stringify({ data: rows }),
     };
   } catch (e) {
     clientPool.release();

     return {
       statusCode: 500,
       body: JSON.stringify({
         response: "An internal server error occurred"
       }),
     };
   }
  };

上面的activity函数代码块执行了一个类似于POST /create-activity 端点中的过程。但是,这一次,该操作检索了活动表中的所有行。

首先,代码建立了一个与集群的连接。然后,它构建了一个包含过滤请求参数的SQL查询,使用pg格式以避免SQL注入攻击。该SQL语句由一个ORDER过滤器。

之后,它执行了一个异步操作,通过Node pg驱动使用创建的SQL语句查询数据库。在解决了查询承诺后,它解构了返回的数据,并将其作为一个响应发送回来。

为了测试上述函数,我们执行下面的命令,该命令使用curl向运行中的Netlify dev服务器内的函数端点发出GET 请求。

curl -X GET http://localhost:5050/.netlify/functions/activities

下面的JSON数据是数据域中的活动数组。这个数组作为上述GET请求的响应出现在我们的控制台。

 $ -> { data: [ { "activity_type":"Hiking", "distance":"2KM", "name":"Trip to the
  mountains", "description":"My personal trip to the high mountains","duration":"2
  Hours" } ] }

有了这两个API,我们现在可以使用React-Static来构建客户端应用程序,它在部署过程中会生成静态标记。

使用React-Static构建客户端应用程序

React-Static是React生态系统中最快、最轻的静态网站生成器之一。我们为这个项目选择React-Static的原因之一是它与React的核心API紧密兼容。

这里的Web应用程序有两个页面。默认或索引页面在用户打开应用程序后立即出现。它显示所有用户创建的活动,第二个页面,create-activity ,供用户创建一个新的活动。

应用程序的样式

首先,使用我们喜欢的代码编辑器,我们修改src/App.js 文件中的App组件以使用React-helmet包。使用react-helmet,我们采用了一个HTML链接元素,引用Bootstrap的CDN来设计整个应用程序的风格。

import React from 'react'
import { Root, Routes, addPrefetchExcludes } from 'react-static'
import { Helmet } from 'react-helmet'
import { Router } from 'components/Router'
import Dynamic from 'containers/Dynamic'

import './app.css'
// Any routes that start with 'dynamic' will be treated as non-static routes
addPrefetchExcludes(['dynamic'])
function App() {
 return (
   <Root>
     <Helmet>
         <link
           rel="stylesheet"       href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"         integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
           crossorigin="anonymous"
         />
         <link
           rel="stylesheet"    href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css"
         />
     </Helmet>
     <div className="content">
       <React.Suspense fallback={<em>Loading...</em>}>
         <Router>
           <Dynamic path="dynamic" />
           <Routes path="*" />
         </Router>
       </React.Suspense>
     </div>
   </Root>
 )
}
export default App

接下来,我们打开模板src/app.css 文件,添加自定义CSS样式,我们接下来创建的组件将使用这些样式。

/* ACTIVITIES STYLING */
.title {
 font-weight: normal;
 font-size: 1.4rem;
}
.icon-ctn {
 padding: 0 .2rem;
}
.input-element {
 height: 45px;
 width: 98%;
 border-radius: 5px;
 padding: 0 1rem;
 border: 1px solid #c0c0c0;
}
.activity-header {
 width: 100%;
 background: #6933ff;
 color: #fff;
 height: 55px;
 display: flex;
 justify-content: center;
 align-items: center;
}

.title {
 font-size: 1.4rem;
}
.title-sm {
 font-size: 1.2rem;
 text-transform: capitalize;
}
.cards-list {
 display: flex;
 flex-direction: column;
 list-style: none;
 padding: 0;
 row-gap: 50px;
}
.activity-type {
 width: 7rem;
 height: 45px;
 list-style: none;
 padding: 0.5rem 1rem;
 border: 1px solid #c0c0c0;
 text-align: center;
 border-radius: 50px;
 transition: all 300ms;
}
.activity-btn {
 background: #6933ff;
 color: #fff;
 border: 1px solid #6933ff;
 border-radius: 5px;
 height: 42px;
 font-size: .9rem;
 padding: .5rem 2rem;
 transition: all 300ms;
}

.activity-btn:hover {
 color: #6933ff;
 cursor: pointer;
 background: transparent;
}

/* CREATE ACTIVITY PAGE STYLING */

.type-list {
 display: grid;
 place-content: center;
 grid-gap: 0.5rem 1rem;
 grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
}

/* ACTIVITY CARD STYLING */
.card {
 border: 0;
 box-shadow: 0 2px 3px #c0c0c0;
 padding: 0.5rem 1rem;
 border-radius: 7px;
}

.activity-type:hover {
 cursor: pointer;
 background: #6933ff;
 border: 0;
 color: #fff;
}

label {
 font-size: 1.1rem;
 margin-bottom: 0.5rem;
}

.activities-items {
 max-width: 700px;
 margin: 0 auto;
}

.activities {
 background: #ebf4fd;
 height: 100%;
}

.align-center {
 display: flex;
 justify-content: center;
 align-content: center;
}

.flex {
 display: flex;
}

在这一点上,应用程序的样式已经到位了。我们现在可以定义通往使用自定义样式的页面的路由。

创建应用程序的路由

接下来,我们打开项目根目录下的static-config.js 文件,为应用程序中的两个页面创建路由。

我们修改下面的static-config.js 文件代码块,使用getRoutesAPI创建一个默认路由和一个创建-活动路由。

import path from "path";

export default {
 getRoutes: async () => {
   return [
     {
       path: "/",
       template: "src/pages/activities",
     },
     {
       path: "/create-activity",
       template: "src/pages/createActivity",
     },
   ];
 },
 plugins: [
   [
     require.resolve("react-static-plugin-source-filesystem"),
     {
       location: path.resolve("./src/pages"),
     },
   ],
   require.resolve("react-static-plugin-reach-router"),
   require.resolve("react-static-plugin-sitemap"),
 ],
};

通过修改我们的static-config.js 文件代码块,我们在应用程序中添加了一个默认页面和一个create-activity 页面。然而,我们还没有创建在这些页面上渲染的组件。我们将在下一节做这个。

创建活动页面

使用我们的代码编辑器,我们在src/pages目录下制作一个createActivity.js 文件,并添加下面的代码块内容。这个文件作为一个包含输入字段的页面,供用户输入活动的详细信息。

import React, { useState } from "react";
import { navigate } from "@reach/router";
import "../app.css";

const activities = ["Hiking", "Skating", "Swimming", "Soccer"];

const CreateActivity = () => {
 const [isLoading, setLoading] = useState(false);
 const [selectedActivity, selectActivity] = useState(null);
 const [activityName, setActivityName] = useState("");
 const [description, setDescription] = useState("");
 const [distance, setDistance] = useState("");
 const [duration, setDuration] = useState("");

 const createAnActivity = async () => {
   setLoading(true);

   try {
     await fetch(`/.netlify/functions/create-activity`, {
       method: "POST",
       body: JSON.stringify({
         duration,
         distance,
         description,
         activity_type: selectedActivity,
         name: activityName,
       })
     });
   } catch (e) {
     console.log(e);
   } finally {
     setLoading(false);
   }
 };

 return (
   <div className="activities" style={{ height: "100vh" }}>
     <header className="activity-header">
       <h2 className="title"> Outdoor Activity Tracker </h2>
     </header>
     <br />

     <div className={"activities-items"}>
       <div className="flex" style={{ justifyContent: "space-between" }}>
         <h2 className="title"> Create New Activity </h2>

         <div className="align-center">
           <button onClick={() => navigate("/")} className="activity-btn">View Previous Activities</button>
         </div>
       </div>
       <hr />
       <div>
         <label id={"newActivity"}> Activity Name: </label>
         <br />

         <div className="align-center">
           <input
             value={activityName}
             onChange={(e) => {
               setActivityName(e.target.value);
             }}
             className={"input-element"}
             type={"text"}
             placeholder={"What is your activity name?"}
           />
         </div>

         <br />
         <label id={"newActivity"}> Activity Description: </label>
         <br />

         <div className="align-center">
           <input
             value={description}
             onChange={(e) => setDescription(e.target.value)}
             className={"input-element"}
             type={"text"}
             placeholder={"What is your activity about?"}
           />
         </div>
         <br />
         <div className="flex" style={{ justifyContent: "space-between" }}>
           <div className="flex" style={{ flexDirection: "column" }}>
             <label>Distance Covered</label>
             <input
               value={distance}
               onChange={({ target }) => setDistance(target.value)}
               className="input-element"
               placeholder={"Distance Covered"}
             />
           </div>

           <div className="flex" style={{ flexDirection: "column" }}>
             <label>Total Time Spent</label>
             <input
               value={duration}
               onChange={({ target }) => setDuration(target.value)}
               className="input-element"
               placeholder={"Total Time Spent"}
             />
           </div>
         </div>
         <br />
         <label> Activity Type: </label>
         <ul className={"type-list"}>
           {activities.map((i) => (
             <li
               key={i}
               className={"activity-type"}
               onClick={() => selectActivity(i)}
               style={{
                 background: i === selectedActivity && "#6933ff",
                 color: i === selectedActivity && "#fff",
                 border: i === selectedActivity && 0,
               }}
             >
               <p>{i}</p>
             </li>
           ))}
         </ul>
         <br />

         <div className="align-center">
           <button
             className="activity-btn"
             style={{ width: "100%" }}
             onClick={() => createAnActivity()}
           >
             {isLoading ? "Creating" : "Create"} Activity
           </button>
         </div>
       </div>
       <br />
       <br />
     </div>
   </div>
 );
};

export default CreateActivity;

上面的代码块包含一个React组件。它的状态变量存储了来自输入字段的数据,用户在这里输入活动名称、描述、距离和持续时间。该页面还包含一个位于中心位置的按钮,供用户在提交活动时点击。

在CreateActivity组件中,有一个异步的createAnActivity函数。这个函数使用浏览器的fetch API来进行POST请求。它的请求体包含要提交给Netlify功能的活动细节。

接下来,在src/pages 目录中创建一个activities.js 文件,并将下面的代码块内容添加到该文件中。这是为了确保我们在static-config.js 文件中引用了activities.js 文件,因为我们以后会回来实现活动页面。

import React from "react";

const Activities = () => (
	<p> Activities page to retrieve all activities </p>
)

export default Activities;

要查看CreateActivity 页面,我们要确保应用服务器在我们的终端中运行,然后使用我们的网络浏览器的地址栏导航到/create-activity 页面。我们可以通过为一个假想的活动在输入字段中填入数值来测试这个页面,如下图所示。

fill the input fields with values for an imaginary activity

当用户在填写完表单字段后点击创建活动,它就会调用createAnActivity 函数来提交细节。

我们将在下一节实现默认页面,使用GET /activities Netlify函数来获取所有创建的活动。

创建默认的活动页面

现在,我们创建我们应用程序的第二个页面,即默认的activities 页面。首先,使用我们的代码编辑器,我们在项目中的src/components 文件夹中制作一个activityCard.js 文件,并添加下面的代码块内容。这个文件将包含一个组件,用来渲染一个父级组件作为道具传入的数据卡片。

import React from "react";
import { FiCalendar, FiClock } from 'react-icons/fi'
import { BsConeStriped } from 'react-icons/bs'

const ActivityCard = ({
   distance,
   duration,
   date_created,
   name,
   description,
   activity_type
}) => {
   return (
       <div className="card" >
           <div className="flex" style={{ justifyContent: 'space-between' }} >
               <h3 className="title-sm" > {name} </h3>

               <div className="activity-type" >
                   {activity_type}
               </div>
           </div>

           <div className="flex" >
               <div className="icon-ctn" >
                   <FiCalendar size={19} />
               </div>
               <p>{new Date(date_created).toDateString()}</p>
           </div>

           <hr />

           <div className="flex" style={{ justifyContent: 'space-between' }} >
               <div className="flex" >
                   <div className="icon-ctn" >
                       <FiClock size={19} />
                   </div>
                   {duration} Spent
               </div>
              <div className="flex" >
                   <div className="icon-ctn" >
                       <BsConeStriped size={19} />
                   </div>
                   {distance} Covered
               </div>
           </div>
           <br />
           <br />
           <p> {description} </p>
       </div>
   )
}

export default ActivityCard;

接下来,我们用下面的代码块内容替换src/pages/activities.js 文件中的模板代码。这段新代码将在默认路径中显示一个用户创建的活动列表。

import React, { useState, useEffect } from "react";
import "../app.css";
import { navigate } from "@reach/router";
import ActivityCard from "../components/activityCard";

const Activities = () => {
 const [activitiesData, setActivitiesData] = useState([]);
 const [isLoading, setLoading] = useState(true);
 const [sortMode, setSortMode] = useState("ASC");

 useEffect(() => {
   fetchActivities();

   return () => fetchActivities();
 }, [sortMode]);

 const fetchActivities = async () => {
   setLoading(true);

   try {
     const body = await fetch(
       `/.netlify/functions/activities?order=${sortMode}`
     );
     const { data } = await body.json();


     if (data) {
        setActivitiesData(data);
     }
   } catch (e) {
     console.log("error fetching data", e);
   } finally {
     setLoading(false);
   }
 };

 return (
   <div className="activities">
     <header className="activity-header">
       <h2 className="title"> Outdoor Activity Tracker </h2>
     </header>
     <br />

     <section className={"activities-items"}>
       {isLoading ? (
         <div className="align-center" style={{ height: "100vh" }}>
           <h4> Fetching Your Activities .... </h4>
         </div>
       ) : activitiesData.length === 0 ? (
         <div style={{ height: "100vh" }}>
           <div className="align-center" style={{display: 'flex', justifyContent : 'center'}}>
             <button
               className={"activity-btn"}
               onClick={() => navigate("/create-activity")}
             >
               Create New Activity
             </button>
           </div>
           <h4 style={{ textAlign: "center" }}>
             You currently have no past activity. <br /> Click the button above
             to create your first activity
           </h4>
         </div>
       ) : (
         <div>
           <div className={"flex"} style={{ justifyContent: "space-between" }}>
             <div className={"align-center"}>
               <h2 className="title"> All Outdoor Activities </h2>
             </div>

             <div>
               <button
                 onClick={() => navigate("/create-activity")}
                 className={"activity-btn"}
               >
                 Create New Activity
               </button>
             </div>
           </div>
           <hr />

           <div className={"flex"}>
             <p style={{ margin: "0 .5rem" }}> Sort By: </p>
             <div>
               <select
                 onChange={(e) => {
                   setSortMode(e.target.value);
                 }}
                 value={sortMode}
                 className={"align-center"}
               >
                 <option value="ASC"> Recently Created </option>
                 <option value="DESC"> First Created </option>
               </select>
             </div>
           </div>

           <ul className={"cards-list"}>
             {activitiesData.map(
               ({
                 activity_type,
                 duration,
                 distance,
                 date_created,
                 name,
                 id,
                 description,
               }) => {
                 return (
                   <li key={id}>
                     <ActivityCard
                       distance={distance}
                       duration={duration}
                       date_created={date_created}
                       name={name}
                       description={description}
                       activity_type={activity_type}
                     />
                   </li>
                 );
               }
             )}
           </ul>
         </div>
       )}
     </section>
   </div>
 );
};

export default Activities;

上面代码块中的组件获取并显示/create-activity Netlify函数使用useEffect钩子从数据库中获取的所有活动,以执行fetchActivities 函数。该函数在组件安装后立即向/create-activity Netlify Function发出了一个GET 请求。该GET请求包含一个订单请求参数,其值来自组件状态。

请注意,我们在create-activity 组件中使用的useEffect 钩子在其依赖数组中接受一个 sortMode 本地状态,并观察 sortMode 的变化。一个下拉元素设置了sortMode值,我们可以使用这个元素来过滤显示的活动。当用户选择一个新的过滤器选项时,sortMode状态会发生变化,从而触发或重新触发useEffect 钩子,该钩子执行fetchAcitivities 函数以获取带有新过滤器的新数据。

activitiesData 本地状态存储了包含网络请求返回的活动的数组。代码进一步映射数组以显示ActivityCard,它将所有的活动细节作为一个属性。

测试我们的应用程序

为了查看新的默认页面,我们使用网络浏览器的地址栏(http://locahost:5050)。我们应该看到两张活动卡,其中包含我们先前创建的活动的细节。

 two activity cards containing the details of the activity we created

上面的图片显示了带有两个活动的默认页面。我们在使用curl进行POST 请求时创建了第一个活动,我们通过填写和提交创建活动页面上的输入字段创建了第二个活动。

在这一点上,应用程序在我们的计算机上工作。但是,我们的应用程序必须经过一个构建过程,其中React组件生成静态标记。在这之后,我们可以使用云服务托管静态页面,以便通过互联网使用其他设备访问。

现在我们可以进一步清理,删除我们启动应用程序时创建的src/pages/blog.jssrc/pages/about.jssrc/pages/index.js 文件。

在下一节中,我们将重点讨论将整个应用程序部署到Netlify。

部署应用程序

我们可以将Jamstack应用程序部署到不同的托管服务提供商,如NetlifyVercel,以便在互联网上提供服务。然而,我们只能将Netlify功能部署到Netlify。由于这个应用程序使用了Netlify功能,最好只将整个项目部署到Netlify。

netlify CLI中的deploy命令可以直接从我们的终端部署整个项目,而不需要使用Netlify控制台。

我们执行下面的命令,在dist目录下生成一个JAMstack应用程序的生产包。

npm run build

production bundle of the JAMstack application

接下来,我们执行下面的命令,将生成的生产包部署到我们在Netlify中创建的项目。

netlify deploy --prod --dir dist

deploy the generated production bundle

我们的Jamstack应用程序现在已经完成并部署到Netlify。我们可以通过deploy命令的输出中的网站URL来查看已部署的应用程序。

接下来的步骤

通过本教程的学习,你已经使用JavaScript作为主要语言建立了一个Jamstack应用程序。你使用Netlify Functions来管理REST API 层,该层将数据存储在免费测试集群的CockroachDB中。最后,你使用React-Static为Netlify的内容交付网络(CDN)生成标记,为终端用户服务。

你可以在这个GitHub资源库中找到包含这个应用程序源代码的目录。欢迎克隆和重用Netlify的功能,用更多的数据库字段来扩展这个应用程序,如地形类型、天气状况或其他功能。或者,注册你的CockroachDB无服务器账户,开始免费构建你自己的由CockroachDB驱动的Jamstack网络应用。