如何用Vercel和CockroachDB构建一个完整的Next.js应用程序

399 阅读9分钟

在本教程中,我们将创建一个用于协调社会活动的应用程序。在这个过程中,你会看到创建一个由Next.js驱动并托管在Vercel上的Web应用是多么简单。本教程还将展示我们如何快速地在云上创建一个成熟的关系型数据库,而无需在本地安装。

我们简单的Next.js网络应用程序有。

  • 一个输入页面,允许用户添加一个带有日期、时间、标题和描述的事件
  • 一个事件页面,列出数据库中的所有事件,并允许用户回应他们的名字
  • 一个RSVP列表,显示谁对事件页面做出了回应

对于本教程,你应该已经熟悉了JavaScript。如果你对这里使用的其他工具Next.js和CockroachDB不熟悉也没关系,因为本教程一开始就对它们进行了介绍要跟上进度,请查看完整的项目代码

CockroachDB简介

CockroachDB是一个使用SQL的分布式数据库,用于云应用。它被设计用来构建、扩展和管理现代数据密集型应用。它还支持PostgreSQL的线程协议,所以你可以使用任何可用的PostgreSQL客户端驱动,使用各种语言进行连接。而且,有一个免费层 ,你可以用来做实验,而不需要为其使用付费。

让我们简单看看如何创建一个CockroachDB账户、集群和数据库

首先,注册一个CockroachDB账户,选择免费计划,并创建你的集群。

CockroachDB Serverless

创建集群后,你会被提示创建一个SQL用户。点击生成并保存密码。

Create SQL User

请确保将你的密码复制到安全的地方,因为你不会再看到它。点击下一步将带你到一个界面,提供你的连接信息,如下所示。

Connection Information

按照CockroachDB客户端选项中的指示,连接到你的集群。

另外,请确保注意下载CA证书的命令。我们稍后需要在我们项目的目录中运行它。

连接到你的集群后,在终端运行以下SQL语句。

defaultdb> CREATE DATABASE social_events_db;

defaultdb> USE social_events_db;

这些语句创建了一个名为 "social_events_db "的数据库,并选择它作为当前/活动数据库。

接下来,我们需要创建两个表:一个用于存储事件,一个用于存储回复的人的名字。事件表有ID、日期、时间、标题和描述列。人们表有这些ID和名字列。

    social_events_db> CREATE TABLE events (id UUID PRIMARY KEY DEFAULT   
  gen_random_uuid(), event_date DATE, event_time TIME, title STRING, description 
  STRING);

    social_events_db> CREATE TABLE people (id UUID PRIMARY KEY DEFAULT   
  gen_random_uuid(), name STRING, event_id UUID REFERENCES events(id));

你可以运行下面的命令来查看创建的表。

SHOW TABLES;

下面是输出的截图。

你也可以通过运行以下命令看到每个表的列。

\d events;

\d people;

结果会是这样的。

现在我们已经创建了我们的CockroachDB数据库,让我们来看看一步一步的实践演示,建立一个Next.js网络应用。要想看到正在运行的演示应用程序,请查看social-events-app.vercel.app

什么是Next.js?

Next.js是一个用于创建服务器端渲染的React应用程序的框架。我们将在这个模板的基础上创建一个Next.js应用程序,它使用Bootstrap 4进行CSS样式设计。

在一个新的终端,运行以下命令。

npx create-next-app --example with-react-bootstrap social-events-app

生成Next.js应用程序后,进入你的项目文件夹,启动实时加载开发服务器。你将通过运行以下命令来完成这一工作。

cd social-events-app``npm run dev

接下来,用你的网络浏览器进入http://localhost:3000/,看看你的应用程序已经启动并运行了这就是我们的应用程序在这一点上的样子。

接下来,我们将看到如何使用Vercel无服务器功能 连接到CockroachDB

首先,创建一个Vercel账户。创建一个空的GitHub仓库,然后将你的项目代码推送到仓库中。

git remote add origin <YOUR_GITHUB_REPO_URL>
git push -u origin main

接下来,请按照Vercel网站上的说明,将你的仓库导入到你的Vercel账户。

为了让你的应用程序与CockroachDB通信,请安装Node.js pg驱动

npm install pg

Vercel支持部署无服务器功能,只需将JavaScript文件放在我们Next.js应用程序的/pages/api 文件夹内即可。

构建网络应用

让我们从创建负责与数据库通信的无服务器函数开始。

之前,我们复制了下载CA证书的命令。在这里,我们使用这个命令,并做了一点修改。改变命令的输出参数(-o)以下载证书。

curl --create-dirs -o ./root.crt -O 
https://cockroachlabs.cloud/clusters/1f404def-6af8-41fe-b3da-ef1229cd6596/cert

打开证书文件,将证书复制到你的Vercel帐户中名为CERT的环境变量中,如下面的截图所示。

Environment Variables

它将从你代码中的process.env.CERT中获得。

我们还将创建一个名为DATABASE_URL的环境变量。你可以使用连接界面中的通用连接字符串选项来检索它。

General connection string option in the connect interface

它将从你代码中的 process.env.DATABASE_URL 中获得。更多信息请参见Vercel关于环境变量的文档。

接下来,在你本地项目的根文件夹内创建一个config.js文件,并添加以下对象。

const config = {
  connectionString: process.env.DATABASE_URL,
  ssl: {
    rejectUnauthorized: true,
    ca: process.env.CERT
  }
};
exports.config = config;

在这里,我们创建一个配置对象,包含连接到我们的数据库的信息。该对象从不同的无服务器函数中导入,以连接到数据库。

如何添加事件

第一个api路由用于向我们的数据库添加事件。在你的本地项目中,创建一个pages/api/addEvent.js文件并开始添加以下代码。

import { Pool } from "pg/lib";
import { config } from "../../config";
const pool = new Pool(config);

这些行将从pg包中导入Pool,然后导入config对象。然后,通过传递配置对象来创建一个连接池

接下来,定义并导出用于添加新事件的无服务器函数,如下所示。

export default async function handler(request, response) {
  const { title, description, date, time } = request.body;
  const query = `INSERT INTO events (title, description, event_date, event_time)
VALUES ('${title}', '${description}', '${date}', '${time}');`;

  try {
    const client = await pool.connect();
    await client.query(query);
    response.json({
      message: "Success!"
    });
  } catch (err) {
    response.status(500).json({
      message: err.message
    });
  }
}

在我们的无服务器函数的正文中,我们首先对request.body对象进行解构,以检索发布的数据。然后,创建一个SQL查询,将事件插入到数据库中。我们使用connect方法连接到我们的数据库集群,使用query方法运行SQL查询。

如果有错误,我们发送一个带有错误代码500和错误信息的响应。否则,我们发送一个表示成功的响应。

如何响应事件

接下来,让我们实现响应事件的api路由。创建一个pages/api/rsvp.js文件,并开始添加以下代码。

import { Pool } from "pg/lib";
import { config } from "../../config";
const pool = new Pool(config);

接下来,定义并导出该函数,如下所示。

export default async function handler(request, response) {
  const { name, eventId } = request.body;
  const query = `INSERT INTO people (name, event_id) VALUES ('${name}', '${eventId}');`;

  try {
    const client = await pool.connect();
    const res = await client.query(query);
    console.log(res);
    response.json({
      message: "Success!"
    });
  } catch (err) {
    response.status(500).json({
      message: err.message
    });
  }
}

我们首先通过JavaScript析构检索发布的名称和事件ID。接下来,我们创建查询,用于向数据库插入一行。之后,我们连接到我们的数据库集群,并运行查询。

如何创建用户界面

让我们从主页开始,用Bootstrap卡片显示事件的列表。

创建事件页面

打开pages/index.jsx文件,开始一步一步地替换代码,如下所示。

首先,添加以下必要的导入。

import React from "react";
import Head from "next/head";
import Link from "next/link";
import { Container, Row, Card, Button, Form } from "react-bootstrap";
import { Pool } from "pg/lib";
import { config } from "../config";
const pool = new Pool(config);

Head是一个组件,用于将元素附加到页面的头部,其中Link实现了客户端的路由转换。

接下来,定义以下React组件。

const Home = ({ error, events }) => {
  const onRSVP = async (eventId) => {};

  return (
    <Container className="md-container">
      <Head>
        <title>Social Events</title>
        <link rel="icon" href="/favicon-32x32.png" />
      </Head>
      <Container>
        <h1>Social Events</h1>
        <p>
          <Link href="/add-event">Share</Link> and attend events..
        </p>
        <Link href="/add-event">
          <Button variant="primary">Add event &rarr;</Button>
        </Link>
        <Container>
          <!-- ADD MARKUP FOR DISPLAYING EVENTS-->
        </Container>
      </Container>

      <footer className="cntr-footer">
        <p>Social Events (c) 2022</p>
      </footer>
    </Container>
  );
};

export default Home;

接下来,添加以下标记,用于在第二个容器内显示事件。

<Row className="justify-content-md-between">
  {events.map((event) => (
    <Card key={event.id} className="sml-card">
      <Card.Body>
        <Card.Title>{event.title}</Card.Title>
        <Card.Text>{event.description}</Card.Text>
        <Card.Text>Date: {event.event_date}</Card.Text>
        <Card.Text>Time: {event.event_time}</Card.Text>
        <Button variant="primary" onClick={() => onRSVP(event.id)}>
          RSVP &rarr;
        </Button>
        <Link href={\`/${event.id}\`}>
          <Button variant="primary">People who have RSVP'd</Button>
        </Link>
        <Form.Control
          type="text"
          placeholder="Write your name to RSVP.."
          value={name}
          onInput={(e) => setName(e.target.value)}
        />
      </Card.Body>
    </Card>
  ))}
</Row>

我们对事件道具进行迭代,并使用一个卡片来显示每个事件。我们还添加了两个按钮,用名字回应事件,并显示已回应的人。

之后,我们需要使用getServerSideProps来检索事件,并将它们作为事件道具传递给函数。我们将使用下面的代码来做这件事。

export async function getServerSideProps() {
  const events = [];
  const client = await pool.connect();
  const res = await client.query("SELECT * FROM events;");

  if (res.rows.length > 0) {
    res.rows.forEach((row) => {
      console.log(row);
      events.push(row);
    });
  }
  return {
    props: { events: JSON.parse(JSON.stringify(events)) }
  };
}

我们使用与前面相同的客户端和配置来从数据库中检索事件,该函数将事件对象作为道具返回给Home函数。如果出现了错误,该函数将返回错误信息。

接下来,实现响应事件的方法,如下所示。

const onRSVP = async (eventId) => {
  const response = await fetch("/api/rsvp", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      name: name,
      eventId: eventId
    })
  });
  if (response.status == 200) {
    alert("RSVP'd!");
  } else {
    alert("Error!");
  }
};

在这里,我们使用Fetch API向相应的api路由发送一个POST请求,并附上用户的名字和事件ID。如果收到一个成功的响应,我们用 "RSVP'd!"提醒用户。否则,我们会显示 "错误!"

同时,在Home函数中定义name变量。

const \[name, setName] = React.useState('');

接下来,在每个显示事件的卡片中,添加以下控件,用于获取名称。

  <Form.Control  type="text"  placeholder="Write your name to RSVP.."  value={name}\
  onInput={e  =>  setName(e.target.value)}  />

以同样的方式,我们必须创建一个pages/[eventId].jsx页面,在这里我们可以显示已经回复的人。这个页面与事件页面类似,只是我们需要获取对某一特定事件作出回应的人。

import React from "react";
import Head from "next/head";
import Link from "next/link";
import { Card, Container, Row } from "react-bootstrap";
import { Pool } from "pg/lib";
import { config } from "../config";
const pool = new Pool(config);


const PeoplePage = ({ people }) => {
  return (
    <Container className="md-container">
      <Head>
        <title>Social Events</title>
        <link rel="icon" href="/favicon-32x32.png" />
      </Head>
      <Container>
        <Container>
          <h1>Social Events</h1>
          <p>
            <Link href="add-event">Share</Link> and attend{" "}
            <Link href="/">events</Link> ..
          </p>
          <Row className="justify-content-md-between">
            <!-- ADD MARKUP FOR DISPLAYING EVENTS-->
          </Row>
        </Container>
      </Container>
      <footer className="cntr-footer">
        <p>Social Events (c) 2022</p>
      </footer>
    </Container>
  );
};

export async function getServerSideProps(context) {
  const { eventId } = context.params;
  console.log(eventId);
  const query = `SELECT * FROM people WHERE event_id='${eventId}';`;


  const people = [];
  const client = await pool.connect();
  const res = await client.query(query);
  if (res.rows.length > 0) {
    res.rows.forEach((row) => {
      people.push(row);
    });
  }
  return {
    props: { people }
  };
}

export default PeoplePage;

在这里,我们使用getServerSideProps方法,从context.params中获取事件ID。接下来,我们使用我们的客户端和配置从数据库中读取人,并将得到的数组作为道具返回给People 页面函数。

在组件的标记中,我们对人的数组进行迭代,如下所示。

{people.map((p) => (
  <Card key={p.id} className="sml-card">
    <Card.Body>
      <Card.Text>{p.name}</Card.Text>
    </Card.Body>
  </Card>
))}

我们使用Bootstrap卡片来显示每个响应者的名字。

创建事件输入页面

接下来,创建一个pages/add-event.jsx页面,首先添加以下导入。

import React from "react";
import Head from "next/head";
import Link from "next/link";
import { Container, Row, Card, Button, Form } from "react-bootstrap";

接下来,定义以下React refs,用于获取表单的值。

const EventPage = () => {
  const eventTitle = React.useRef();
  const eventDate = React.useRef();
  const eventTime = React.useRef();
  const eventDescription = React.useRef();

接下来,定义handleSubmit 方法。

  const handleSubmit = async (e) => {
    e.preventDefault();
    const response = await fetch("/api/addEvent", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        title: eventTitle.current.value,
        date: eventDate.current.value,
        time: eventTime.current.value,
        description: eventDescription.current.value
      })
    });
    if (response.status == 200) {
      alert("Your event is added!");
      eventTitle.current.value = "";
      eventDate.current.value = "";
      eventTime.current.value = "";
      eventDescription.current.value = "";
    } else {
      alert("Error!");
    }
  };

在这里,我们用React refs向负责在数据库中插入事件的api路由发送一个POST请求,其中包含我们从表单的控件中获取的事件数据。如果我们得到一个200状态的响应,我们用 "您的事件已被添加!"的消息提醒用户,并清除表单。否则,我们就显示 "错误!"信息。

接下来,我们添加页面的标记,如下所示。

  return (
    <Container className="md-container">
      <Head>
        <title>Social Events</title>
        <link rel="icon" href="/favicon-32x32.png" />
      </Head>
      <Container>
        <h1>Social Events</h1>
        <p>
          Share and attend <Link href="/">events</Link> ..
        </p>
        <Container>
          <Row className="justify-content-md-between">
            <!-- ADD FORM HERE -->
          </Row>
        </Container>
      </Container>

      <footer className="cntr-footer">
        <p>Social Events (c) 2022</p>
      </footer>
    </Container>
  );
};
export default EventPage;

我们简单地给我们的页面添加一个标题、页眉和副页眉。然后,我们为我们的表单创建一个容器,并在下面创建一个页脚。

接下来,添加以下表单。

<Card className="sml-card">
  <Card.Body>
    <Form onSubmit={handleSubmit}>
      <Form.Group controlId="form.eventTitle">
        <Form.Label>Title</Form.Label>
        <Form.Control
          type="text"
          placeholder="Enter event title"
          ref={eventTitle}
        />
      </Form.Group>
      <Form.Group controlId="form.eventDate">
        <Form.Label>Event date</Form.Label>
        <Form.Control
          type="date"
          placeholder="Enter event date"
          ref={eventDate}
        />
      </Form.Group>
      <Form.Group controlId="form.eventTime">
        <Form.Label>Event time</Form.Label>
        <Form.Control
          type="time"
          placeholder="Enter event time"
          ref={eventTime}
        />
      </Form.Group>
      <Form.Group controlId="form.eventDescription">
        <Form.Label>Event description</Form.Label>
        <Form.Control
          as="textarea"
          rows={3}
          placeholder="Write something about your event.."
          ref={eventDescription}
        />
      </Form.Group>
      <Form.Group>
        <Button className="btn btn-primary" type="submit">
          Send
        </Button>
      </Form.Group>
    </Form>
  </Card.Body>
</Card>

我们将handleSubmit 方法与表单的onSubmit 事件绑定,以便在提交表单时调用。接下来,我们给每个表单控件附加一个React ref,以便在handleSubmit 方法上访问控件的值。

实现这些步骤后,将代码推送到你的GitHub仓库。

表单看起来像这样。

Event Form

在本教程中,我们已经看到了如何轻松创建一个由Next.js驱动并托管在Vercel上的网络应用。我们还展示了如何快速地在云端创建一个成熟的关系型数据库,而无需在本地安装。这是我的repo ,它部署在:https://social-events-app.vercel.app/

我们使用无服务器函数与我们的数据库通信,使用Node.js的PostgreSQL驱动。

要想自己遵循前面的步骤,请注册一个CockroachDB账户,然后开始构建自己的由CockroachDB驱动的Next.js网络应用。