如何使用 Fly.io 和 Tigris 部署 Next.js 应用

在本教程中,您将学习到应用部署平台 Fly.io 和全球分布式的 S3 兼容对象存储服务 Tigris。

这两个平台密切相关,使它们成为您项目的绝佳选择。您可以从 Fly.io 获得应用部署体验,并从 Tigris 获得对象存储功能。

应用部署相当简单易懂,因此让我们首先快速介绍一下 Tigris 使用的存储桶存储。

(本文视频讲解:java567.com)

先决条件

  • 安装了 IDE/代码编辑器,如 Visual Studio Code
  • 安装了 Node.js 和 npm
  • 安装或设置了 Next.js
  • 在 Fly.io 上有一个免费账户
  • 在 Tigris 上有一个免费账户

目录

  1. 什么是存储桶?
  2. 我们将要构建什么
  3. 如何在 Fly.io 和 Tigris 上创建账户
  4. 如何设置用户数据库项目
  5. 如何构建用户数据库应用程序
  6. 如何创建用户数据库界面
  7. 如何将您的应用程序部署到 Fly.io
  8. 结论

什么是存储桶?

Amazon S3 存储桶是通过亚马逊网络服务(AWS)的简单存储服务(S3)平台访问的公共云存储资源。

全球分布的、与 S3 兼容的对象存储服务 Tigris 使用低延迟存储功能。这意味着您可以在 Tigris 上访问亚马逊的 S3 存储桶来满足您的存储需求。

Tigris 还已经与 Fly.io 完全集成,并且也与运行在 Fly.io 硬件上的 flyctl 完全集成。Fly.io 的命令行界面 flyctl 允许您从账户创建到应用程序部署的所有操作都可以处理。

我们将构建什么

为了学习这些平台的基础知识,我们将构建一个用户数据库应用程序。这相当直观:基本上我们可以执行完整的 CRUD 请求,这意味着可以读取、添加、更新和删除用户数据。

Next.js 将是我们的主要框架,因为它允许我们构建全栈应用,而无需创建单独的服务器。

用户数据库应用用户数据库应用首页

您可以从他们的文档了解更多关于 Fly.io 和 Tigris 的信息。为了这个项目,我们需要在两个平台上创建一个账户,我会在一分钟内为您介绍。

所以现在,理论部分已经讲解完毕,让我们开始吧。

您可以在我的 GitHub 上找到这个项目的代码库。

如何在 Fly.io 和 Tigris 上创建账户

只需按照以下步骤在两个平台上开始运行:

  1. 首先您需要在 Fly.io 上创建一个账户,因为要使用 Tigris 您需要一个 Fly.io 的账户。
  2. 接下来,在您的计算机上安装 flyctl 命令行工具,这对于设置您的账户以部署应用程序至关重要。

好的,让我们继续下一个阶段,我们将在那里设置我们的项目以及我们的 Tigris 存储桶。

如何设置用户数据库项目

首先,导航到您计划创建项目的计算机上的目录。然后创建一个名为 fly-tigris-user-database 的文件夹并进入它。现在在该文件夹内运行以下命令来设置一个 Next.js 项目:

npx create-next-app .

我们正在做的就是设置我们的 Next.js 项目,重要的是对配置选择是为 Tailwind CSS 和 App router 选择是,因为我们在这个项目中需要它们。

现在运行以下命令来安装 AWS SDK:

npm install @aws-sdk/client-s3

我们只需要安装一个包(@aws-sdk/client-s3),这是连接到我们的存储桶所需的。

好的,很好 - 现在是时候为我们刚刚创建的项目创建一个存储桶了。您可以在它们的官方文档中找到此过程的指导。

只需运行以下命令来创建一个存储桶:

fly storage create

现在在设置屏幕上,选择一个名称给您的存储桶。名称应该是唯一的,所以您不能使用其他人选择的名称。

好的,现在是最重要的阶段:您应该有您的 AWS 和存储桶的密钥,就像这个例子中所示:

AWS_ACCESS_KEY_ID: 您的密钥
AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
AWS_REGION: auto
AWS_SECRET_ACCESS_KEY: 您的秘密访问密钥
BUCKET_NAME: 您的存储桶名称

在您的 Next.js 项目根目录下创建一个 .env.local 文件,然后将所有这些秘密环境变量复制粘贴进去。

为了使这些环境变量在我们的 Next.js 应用程序中正常工作,我们需要调整它们的名称使其公开。查看下面的示例,并对您的 .env.local 文件进行更改。

NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID: 您的密钥
NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3: https://fly.storage.tigris.dev
NEXT_PUBLIC_SECRET_AWS_REGION: auto
NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY: 您的秘密访问密钥
NEXT_PUBLIC_SECRET_BUCKET_NAME: 您的存储桶名称

现在在 Tigris 文档页面上,如果您点击仪表板按钮并登录您的账户,您应该会看到您新创建的存储桶,就像我的示例中所示:

Tigris 存储桶Tigris 网站存储桶管理页面

太棒了!第一阶段完成了。我们现在有一个存储桶来在线存储我们的应用数据,所以我们可以开始在下一节中创建我们的应用程序。

如何构建用户数据库应用程序

我将把这一部分分成两个部分。首先,我们将建立并运行服务器,以便测试 CRUD 端点。然后我们将完成前端部分。

如何创建用户数据库服务器

首先,让我们创建我们的后端架构。我们将创建四个端点,一个用于每个 CRUD 请求。我们还需要一个辅助文件,其中包含一些用于从我们的对象存储中获取用户的函数。

如果您尚未这样做,请cd到项目的根目录,并运行下面的命令。它们将快速设置我们的所有文件和文件夹:

cd src/app
mkdir api
mkdir api/deleteuser api/getusers api/postuser api/updateuser
touch api/deleteuser/route.js
touch api/getusers/route.js
touch api/postuser/route.js
touch api/updateuser/route.js
mkdir helpers
touch helpers/getUsers.js

好的,很快就完成了。现在我们只需要将代码添加到我们的五个文件中,我们的后端 API 就准备好测试了。

首先让我们处理辅助文件。这个文件中的代码允许我们收集并访问存储在 Tigris 上我们 S3 存储桶中的用户数据。

把下面的代码放到 helpers/getUsers.js 中:

import {
  S3Client,
  ListObjectsV2Command,
  GetObjectCommand,
} from '@aws-sdk/client-s3';

const streamToString = (stream) =>
  new Promise((resolve, reject) => {
    const chunks = [];
    stream.on('data', (chunk) => chunks.push(chunk));
    stream.on('error', reject);
    stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  });

export async function fetchAllUsersFromS3() {
  try {
    const s3 = new S3Client({
      region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
      endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
      credentials: {
        accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
      },
    });

    const commandDetails = new ListObjectsV2Command({
      Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
      MaxKeys: 10,
    });
    const { Contents } = await s3.send(commandDetails);
    console.log('List Result', Contents);
    if (!Contents) {
      console.log('no users');
    } else {
      const users = await Promise.all(
        Contents.map(async (item) => {
          const getObject = new GetObjectCommand({
            Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
            Key: item.Key,
          });

          const { Body } = await s3.send(getObject);
          const data = await streamToString(Body);
          const userObject = JSON.parse(data);
          console.log('Data', data);
          return userObject;
        })
      );
      return users;
    }
  } catch (e) {
    console.error(e);
    throw e;
  }
}

export async function getUserById(users, userId) {
  if (!users) {
    console.log('no users');
  } else {
    return users.find((user) => user.id === userId);
  }
}

export async function getUserByIdEmail(users, email) {
  if (!users) {
    console.log('no users');
  } else {
    return users.find(
      (user) => user.email.toLowerCase() === email.toLowerCase()
    );
  }
}

主要的函数是 fetchAllUsersFromS3(),它基本上使用我们需要的凭证和配置创建 S3 客户端。它使用 GetObjectCommand 获取对象的内容,然后使用 streamToString 函数将其从流转换为字符串。然后解析 JSON 数据返回用户对象。

另外两个函数调用,getUserById(users, userId)getUserByIdEmail(users, email),是允许我们根据 ID 或电子邮件地址在我们的 S3 存储桶中搜索用户的辅助函数。代码将基本的 AWS 配置参数存储在环境变量中,包括区域、端点 URL、访问密钥和秘密访问密钥,以及 S3 存储桶名称。

好了,现在只剩下路由了。

首先把这段代码放到 getusers/route.js 中:

import {
  S3Client,
  ListObjectsV2Command,
  GetObjectCommand,
} from '@aws-sdk/client-s3';

export async function GET() {
  const streamToString = (stream) =>
    new Promise((resolve, reject) => {
      const chunks = [];
      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('error', reject);
      stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
    });

  try {
    const s3 = new S3Client({
      region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
      endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
      credentials: {
        accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
      },
    });

    const listParams = {
      Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
      MaxKeys: 10,
    };

    const list = new ListObjectsV2Command(listParams);
    const { Contents } = await s3.send(list);

    console.log('List Result', Contents);

    if (!Contents || Contents.length === 0) {
      console.log('No users found');
      return new Response(JSON.stringify({ error: 'No users found' }), {
        status: 404,
      });
    }

    const users = await Promise.all(
      Contents.map(async (item) => {
        const getObjectParams = {
          Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
          Key: item.Key,
        };

        const getObject = new GetObjectCommand(getObjectParams);
        const { Body } = await s3.send(getObject);
        const data = await streamToString(Body);
        console.log('Backend API GET Data:', data);
        return JSON.parse(data);
      })
    );

    return new Response(JSON.stringify(users), { status: 200 });
  } catch (e) {
    console.error('Error:', e);
    return new Response(
      JSON.stringify({ error: e.message || 'Unknown error' }),
      { status: 500 }
    );
  }
}

在这个文件的代码中,我们从 Tigris S3 存储桶中检索用户数据,并通过 Next.js API 路由处理程序函数将其作为 JSON 响应返回。

该函数还导入了与我们的 S3 存储桶通信所需的 AWS SDK 客户端。当请求 API 路由时,将调用主入口点,即 GET 函数。使用环境变量,GET 方法首先建立了一个 S3 客户端,其中包含所需的区域、端点和凭证设置。

之后,创建了一个 ListObjectsV2Command 来从指定的 S3 存储桶中检索用户数据列表的项目。然后方法通过对象列表,使用 GetObjectCommand 获取每个对象的数据。

streamToString 方法用于将每个对象的内容从流转换为字符串。

在解析 JSON 输入后,用户对象作为 JSON 响应发送出去。如果未检测到用户,则返回一个 404 错误响应,并提供一个错误消息。如果在过程中出现任何问题,则提供一个 500 错误响应,其中包含错误消息。

接下来是 POST 路由,将以下代码放入 postuser/route.js 中:

import { fetchAllUsersFromS3, getUserByIdEmail } from '../../helpers/getUsers';

import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';

export async function POST(req) {
  try {
    const { firstname, lastname, email, password } = await req.json();
    const id = crypto.randomUUID();
    const data = { firstname, lastname, email, password, id };
    console.log('Request body data', data);
    const allUsers = await fetchAllUsersFromS3();
    console.log('all users', allUsers);
    const existingUser = await getUserByIdEmail(allUsers, email);
    console.log(existingUser, email);
    if (existingUser) {
      return Response.json({
        error: 'Email address already in use',
      });
    }

    const s3 = new S3Client({
      region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
      endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
      credentials: {
        accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
      },
    });

    const commandDetails = new PutObjectCommand({
      Body: JSON.stringify(data),
      Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
      Key: email,
    });

    await s3.send(commandDetails);

    return Response.json({ message: 'User added' });
  } catch (e) {
    console.error(e);
    return Response.json({ error: 'Failed to create user' });
  }
}

此代码用于管理用户注册,并将用户信息保存在我们的 Tigris S3 存储桶中。fetchAllUsersFromS3getUserByIdEmail 这两个辅助函数被导入,用于与 S3 存储桶通信并收集用户数据。初始入口点,即当通过 API 路由发出 POST 请求时调用的是 POST 函数。

首先,POST 函数从请求体中提取用户信息(名、姓、电子邮件和密码)。然后使用 crypto.randomUUID() 创建一个唯一的用户 ID。为了获取 S3 存储桶中的所有当前用户数据,该方法调用 fetchAllUsersFromS3 辅助函数。

代码使用 getUserByIdEmail 辅助函数来确定请求中提供的电子邮件地址是否已经存在于用户数据中。如果电子邮件地址已经存在,则该方法提供一个包含错误消息的 JSON 响应。如果电子邮件地址是唯一的,则使用环境变量创建具有所需设置的 S3 客户端(区域、端点和凭证)。

然后生成一个 PutObjectCommand,将新的用户数据(作为 JSON 字符串)上传到 S3 存储桶,其中电子邮件地址是键。最后,该方法生成一个 JSON 响应,确认用户已成功添加。如果在过程中发生错误,则该函数提供一个包含错误消息的 JSON 响应。

紧接着是我们的 UPDATE 路由,将以下代码放入 updateuser/route.js 中:

import { getUserById, fetchAllUsersFromS3 } from '../../helpers/getUsers';

import {
  S3Client,
  DeleteObjectCommand,
  PutObjectCommand,
} from '@aws-sdk/client-s3';

export async function PUT(req) {
  try {
    const { firstname, lastname, email, originalEmail, id } = await req.json();
    console.log('request data', firstname, lastname, email, originalEmail, id);
    const allUsers = await fetchAllUsersFromS3();
    console.log('all users', allUsers);
    const userToUpdate = await getUserById(allUsers, id);
    console.log('user to update', userToUpdate);
    const user = allUsers.find((user) => user.id === id);
    const userEmail = user ? user.email : null;
    console.log('User Email', userEmail);
    if (!userToUpdate) {
      return Response.json({ error: 'User not found' });
    }

    if (!originalEmail || !email) {
      return Response.json({
        error: 'Both originalEmail and email are required for update',
      });
    }

    const data = { firstname, lastname, email, id };

    console.log('Updated data', data);

    const s3 = new S3Client({
      region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
      endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
      credentials: {
        accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
      },
    });

    console.log('Original email', originalEmail);
    console.log('New email', email);

    if (userEmail === originalEmail) {
      console.log('The emails are the same so its a match');
      const deleteCommand = new DeleteObjectCommand({
        Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
        Key: originalEmail,
      });

      await s3.send(deleteCommand);
      const putCommand = new PutObjectCommand({
        Body: JSON.stringify(data),
        Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
        Key: email,
      });

      await s3.send(putCommand);

      return Response.json({ message: 'User updated successfully' });
    } else {
      console.log('Error: The emails do not match');
      return Response.json({ error: 'Failed to update user' });
    }
  } catch (e) {
    console.error(e);
  }
}

该代码包括之前的相同辅助函数 getUserByIdfetchAllUsersFromS3,用于连接 S3 存储桶并获取用户数据。主要入口点是 PUT 函数,每当向 API 路由发出 PUT 请求时就会调用它。PUT 函数首先从请求体中提取用户数据(名、姓、电子邮件、原始电子邮件和 ID)。

然后它调用 fetchAllUsersFromS3 辅助函数,从 S3 存储桶中检索所有现有的用户数据。然后代码通过使用指定的用户 ID 调用 getUserById 辅助函数来找到要更新的用户。如果找不到用户,则该方法提供一个包含错误消息的 JSON 响应。

如果原始电子邮件或电子邮件不存在,则该函数提供一个包含错误消息的 JSON 响应。然后方法根据提供的信息构建一个更新后的用户数据对象。此外,代码可以使用环境变量创建具有所需设置(区域、端点和凭证)的 S3 客户端。

假设初始电子邮件地址与当前用户的电子邮件地址匹配,该函数将更新用户信息;但是如果原始电子邮件与当前用户的电子邮件地址不匹配,则方法提供一个包含错误消息的 JSON 响应。像之前一样,如果在过程中检测到错误,则该函数将创建一个日志而不发送响应。

最后是我们的 DELETE 路由。将以下代码添加到 deleteuser/route.js 中:

import { S3Client, DeleteObjectCommand } from '@aws-sdk/client-s3';

import { fetchAllUsersFromS3, getUserById } from '../../helpers/getUsers';

export async function DELETE(req) {
  try {
    const id = await req.json();
    console.log('Id', id.id);
    const allUsers = await fetchAllUsersFromS3();
    console.log('all users', allUsers);
    const userToDelete = await getUserById(allUsers, id.id);
    console.log('user to delete', userToDelete);

    if (!userToDelete) {
      return Response.json({ error: 'User not found' });
    }

    const userEmail = userToDelete.email;
    const s3 = new S3Client({
      region: process.env.NEXT_PUBLIC_SECRET_AWS_REGION,
      endpoint: process.env.NEXT_PUBLIC_SECRET_AWS_ENDPOINT_URL_S3,
      credentials: {
        accessKeyId: process.env.NEXT_PUBLIC_SECRET_AWS_ACCESS_KEY_ID,
        secretAccessKey: process.env.NEXT_PUBLIC_SECRET_AWS_SECRET_ACCESS_KEY,
      },
    });

    const deleteCommand = new DeleteObjectCommand({
      Bucket: process.env.NEXT_PUBLIC_SECRET_BUCKET_NAME,
      Key: userEmail,
    });

    await s3.send(deleteCommand);
    return Response.json({ message: 'User deleted successfully' });
  } catch (e) {
    console.error(e);
    return Response.json({ error: 'Failed to delete user' });
  }
}

这用于从我们的存储桶中删除数据。代码包括与 S3 通信所需的相同的 AWS SDK 客户端,以及用于从 S3 存储桶中获取用户数据的两个辅助函数 fetchAllUsersFromS3getUserById。关键入口点是 DELETE 函数,当向 API 路由发出 DELETE 请求时调用它。

DELETE 函数中,首先从请求体中提取用户 ID。然后它调用 fetchAllUsersFromS3 辅助方法,从 S3 存储桶中检索所有现有的用户数据。然后代码通过使用指定的用户 ID 调用 getUserById 辅助函数来找到要删除的用户。如果客户端找不到,则该方法提供一个包含错误消息的 JSON 响应。

然后生成一个 DeleteObjectCommand,从 S3 存储桶中删除该项,其中用户的电子邮件地址是键。最后,该方法生成一个 JSON 响应,确认用户已成功删除。如果在过程中检测到错误,则该函数将创建一个日志而不发送响应。

前端部分

现在我们已经创建了我们的 API,我们将构建一个简单的前端来测试我们的功能。我们将使用 React 和 Tailwind CSS 来实现。

首先,确保您已经安装了 create-next-app。如果没有,请运行以下命令:

npm install -g create-next-app

接下来,在终端中运行以下命令创建一个新的 Next.js 项目:

npx create-next-app@latest my-next-app

现在,我们将进入项目文件夹并安装一些依赖项:

cd my-next-app
npm install aws-sdk tailwindcss

接下来,我们将配置 Tailwind CSS。在项目根目录下创建一个 tailwind.config.js 文件,内容如下:

module.exports = {
  purge: [],
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
  },
  variants: {
    extend: {},
  },
  plugins: [],
}

然后,在 styles/globals.css 中,添加以下内容:

@tailwind base;
@tailwind components;
@tailwind utilities;

接下来,让我们在项目中创建一个简单的注册表单。在 pages 目录下创建一个名为 index.js 的文件,并将以下代码粘贴到该文件中:

import { useState } from 'react';

export default function Home() {
  const [form, setForm] = useState({
    firstname: '',
    lastname: '',
    email: '',
    password: '',
  });

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prevState) => ({
      ...prevState,
      [name]: value,
    }));
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const response = await fetch('/api/postuser', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(form),
      });

      if (response.ok) {
        const data = await response.json();
        console.log(data);
      }
    } catch (error) {
      console.error('Error:', error);
    }
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
      <div className="max-w-md w-full space-y-8">
        <div>
          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
            Register
          </h2>
        </div>
        <form className="mt-8 space-y-6" onSubmit={handleSubmit}>
          <input type="hidden" name="remember" defaultValue="true" />
          <div className="rounded-md shadow-sm -space-y-px">
            <div>
              <label htmlFor="firstname" className="sr-only">
                First name
              </label>
              <input
                id="firstname"
                name="firstname"
                type="text"
                autoComplete="given-name"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-t-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="First name"
                value={form.firstname}
                onChange={handleChange}
              />
            </div>
            <div>
              <label htmlFor="lastname" className="sr-only">
                Last name
              </label>
              <input
                id="lastname"
                name="lastname"
                type="text"
                autoComplete="family-name"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Last name"
                value={form.lastname}
                onChange={handleChange}
              />
            </div>
            <div>
              <label htmlFor="email" className="sr-only">
                Email address
              </label>
              <input
                id="email"
                name="email"
                type="email"
                autoComplete="email"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Email address"
                value={form.email}
                onChange={handleChange}
              />
            </div>
            <div>
              <label htmlFor="password" className="sr-only">
                Password
              </label>
              <input
                id="password"
                name="password"
                type="password"
                autoComplete="current-password"
                required
                className="appearance-none rounded-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-b-md focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm"
                placeholder="Password"
                value={form.password}
                onChange={handleChange}
              />
            </div>
          </div>

          <div>
            <button
              type="submit"
              className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
            >
              <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                {/* Heroicon name: solid/lock-closed */}
                <svg
                  className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
                  xmlns="http://www.w3.org/2000/svg"
                  viewBox="0 0 20 20"
                  fill="currentColor"
                  aria-hidden="true"
                >
                  <path
                    fillRule="evenodd"
                    d="M10 12a2 2 0 100-4 2 2 0 000 4z"
                  />
                  <path
                    fillRule="evenodd"
                    d="M4 8a6 6 0 1112 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2V8zm2-2v5a2 2 0 002 2h6a2 2 0 002-2V6a2 2 0 00-2-2H8a2 2 0 00-2 2z"
                    clipRule="evenodd"
                  />
                </svg>
              </span>
              Register
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}

这将创建一个简单的注册表单,允许用户输入名、姓、电子邮件和密码。提交表单将通过 API 路由将用户数据发送到我们的 Next.js 应用程序的服务器端。

现在,您可以运行您的 Next.js 应用程序并测试 API 功能。要运行应用程序,请在终端中执行以下命令:

npm run dev

这将启动本地开发服务器。现在,您可以打开浏览器并访问 http://localhost:3000,然后尝试在注册表单中输入数据并提交以查看 API 是否正常工作。

完成这个项目后,您可以进一步扩展它,例如,创建登录表单、更多 API 路由以支持其他操作,或是添加用户数据的验证。

(本文视频讲解:java567.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/576329.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

短视频素材去哪里找,而且不带水印的那种?

为了确保视频创作者能够接触到全球范围内的优质资源&#xff0c;下面列出的视频素材网站各具特色&#xff0c;提供从标准视频到高动态范围&#xff08;HDR&#xff09;的素材&#xff0c;满足你在不同项目中的需求。 1. 蛙学府 (中国) 提供专业级的视频素材&#xff0c;特别适…

【C++】STL-vector的使用

目录 1、什么是vector&#xff1f; 2、vector的使用 2.1 vector的定义 ​编辑 2.2 遍历修改数据 2.3 迭代器 2.4 vector空间增长问题 2.5 vector的增删查改 3、迭代器失效 3.1 会引起其底层空间改变的操作&#xff0c;都有可能是迭代器失效 3.2 指定位置元素的删除操…

【触摸案例-多点触摸的案例 Objective-C语言】

一、我们来做这个多点触摸的案例 1.首先呢,按着这个option键啊,可以模拟多点触摸, 然后呢,再去怎么着去画圈儿, 它这个里边就会产生一个imageView,跟着你去变,会有这么一个效果, 那么,首先啊,我们新建一个项目, Name:03-多点触摸的案例 1)首先,我们把控制器的v…

dwc3控制器是怎么处理otg

概念 在OTG中&#xff0c;初始主机设备称为A设备&#xff0c;外设称为B设备。可用电缆的连接方式来决定初始角色。两用设备使用新型Mini-AB插座&#xff0c;从而使Mini-A插头、Mini-B插头和Mini-AB插座增添了第5个引脚&#xff08;ID&#xff09;&#xff0c;以用于识别不同的…

网御星云防火墙策略配置

网御星云防火墙配置 1. 初始设定2. 网络配置3. 安全规则和策略4. 监控和维护零基础入门学习路线视频配套资料&国内外网安书籍、文档网络安全面试题 1. 初始设定 接入网络&#xff1a; 在开始配置之前&#xff0c;确保你的网御星云防火墙正确连接到网络。这通常涉及将WAN接…

基于Python实现的推箱子小游戏

Python贪吃蛇小游戏实现: 推箱子曾经在我们的童年给我们带来了很多乐趣。推箱子这款游戏现在基本上没人玩了&#xff0c;甚至在新一代人的印象中都已毫无记忆了。。。但是&#xff0c;这款游戏可以在一定程度上锻炼自己的编程能力。 运行效果如图所示&#xff1a; 游戏关卡有点…

Ubuntu系统强制用户设置复杂密码

1、安装cracklib模块 安装PAM的cracklib模块&#xff0c;cracklib能提供额外的密码检查能力 sudo apt-get install libpam-cracklib2、可用vim打开配置文件&#xff08;或其它方式&#xff09; sudo vim /etc/pam.d/common-password3、设置密码复杂度 在# here are the per…

滚珠丝杆有哪些应用场景?

在传动领域中滚珠丝杆是自动化设备和智能制造设备相结合的关键装置&#xff0c;在精密制造工艺、精密装配作业及现代物流系统等多元领域中&#xff0c;发挥着不可或缺的核心作用。其优点在于快速、高效、准确可靠和稳定。它能够在较小的转矩下产生很大的推力&#xff0c;所以被…

win11 安装qt5.14.2 、qtcreator、vs编译器 。用最小安装进行 c++开发qt界面

系统 &#xff1a;win11 一、安装vs生成工具 &#xff0c;安装编译器 下载visualstudio tools 生成工具&#xff1a; 安装编译器 和 windows sdk&#xff1a; 安装debug 调试器&#xff1a; 二、Qt5.14.2下载 下载链接: Index of /archive/qt/5.14/5.14.2 安装qt 三、配置QT/…

【多态】有关多继承和菱形继承的多态

博主首页&#xff1a; 有趣的中国人 专栏首页&#xff1a; C进阶 其它专栏&#xff1a; C初阶 | 初阶数据结构 | Linux 博主会持续更新 本篇文章主要讲解 多继承和菱形继承的多态 的相关内容 文章目录 1. 回顾多态底层2. 抽象类2.1 概念2.2 接口继承和实现继承 3. 虚表所在…

文件上传漏洞(upload-labs)

目录 一、文件上传漏洞 1.什么是文件上传漏洞 常见的WebShell 2.文件上传产生漏洞的原因 二、文件上传绕过 &#xff08;一&#xff09;客服端绕过-JS验证 1.前端验证 upload-labs第一关 &#xff08;二&#xff09;绕过黑名单验证 黑名单验证 1.特殊解析后缀 upl…

Pandas 2.2 中文官方教程和指南(十一·一)

原文&#xff1a;pandas.pydata.org/docs/ PyArrow 功能 原文&#xff1a;pandas.pydata.org/docs/user_guide/pyarrow.html pandas 可以利用PyArrow来扩展功能并改善各种 API 的性能。这包括&#xff1a; 与 NumPy 相比&#xff0c;拥有更广泛的数据类型 对所有数据类型支持缺…

C# 结合JavaScript实现手写板签名并上传到服务器

应用场景 我们最近开发了一款笔迹测试功能的程序&#xff08;测试版&#xff09;&#xff0c;用户在手写板上手写签名&#xff0c;提交后即可测试出被测试者的心理素质评价分析。类似功能的场景还比如&#xff0c;在银行柜台办理业务&#xff0c;期间可能需要您使用手写设备进…

linux 编译binutil 遇到问题

在centos6.10上编译binutil2.27时遇到问题&#xff1a; as.c&#x1f4af;31: error: ‘DEFAULT_GENERATE_ELF_STT_COMMON’ undeclared here (not in a function) 搜到解决方法是这个&#xff1a; 1、https://github.com/riscv-software-src/riscv-tools/issues/66 &#xf…

十七、Java网络编程(一)

1、Java网络编程的基本概念 1)网络编程的概念 Java作为一种与平台无关的语言,从一出现就与网络有关及其密切的关系,因为Java写的程序可以在网络上直接运行,使用Java,只需编写简单的代码就能实现强大的网络功能。下面将介绍几个与Java网络编程有关的概念。 2)TCP/IP协议概…

内置对象部分

一&#xff0c;内置对象 二&#xff0c;math对象 不是构造函数&#xff0c;不需要new来调用&#xff0c;而是直接使用里面的属性和方法即可 1.随机方法random 返回一个随机的小数 [0,1&#xff09; 2.日起格式化 返回的月份会小一&#xff0c;记得加一 周一返回1&#xff…

iObit Uninstaller 安装、激活、使用教程

「软件简介」 IObit Uninstaller 一款专业的卸载工具&#xff0c;旨在彻底移除不需要的软件、插件以及 Windows 应用&#xff0c;同时提供安全、快速和轻量化的 PC 使用体验。 〖下载安装软件〗 版本&#xff1a;V 13.4.0 | 26.9 MB 支持系统&#xff1a;Windows 11/10/8.1/8/…

传媒论坛编辑部传媒论坛杂志社传媒论坛杂志2024年第7期目录

专题│场景传播研究 场景传播&#xff1a;一场遮盖自我与寻找自我的博弈 胡沈明; 3 基于CiteSpace的中国场景传播研究热点分析 管倩;粟银慧; 4-610《传媒论坛》投稿&#xff1a;cnqikantg126.com 数字世界的美与危&#xff1a;场景传播的失范与应对之举 王依晗;章洁…

Centos/linux根目录扩容、分区、挂载。LVM、物理卷、逻辑卷

前言    &#xff08;空格&#xff09; &#xff1a;分区挂载和扩容是两码事 每个Linux使用者在安装Linux时都会遇到这样的困境&#xff1a;在为系统分区时&#xff0c;如何精确评估和分配各个硬盘分区的容量&#xff0c;因为系统管理员不但要考虑到当前某个分区需要的容量&a…

在Linux系统内搭建DNS本地服务器

文章目录 Linux的本地DNS服务一、什么是DNS1.1、域名1.2、DNS服务器、DNS客户端和DNS中继1.3、DNS域名解析 二、搭建DNS服务2.1、正反向解析2.1.1.安装bind软件包2.1.2.修改主配置文件2.1.3.修改区域配置文件2.1.4.配置区域数据文件2.1.5.启动服务、关闭防火墙2.1.6.本地解析测…
最新文章