使用 Next.js 和 Supabase 构建全栈应用程序

在为您的下一个全栈应用程序构建和选择框架时,在我看来,将 Next.js 与 Supabase 结合使用是最好的选择之一。

Supabase 是一个开源的 Firebase 替代品,具有许多强大的工具,包括无缝身份验证。作为开发人员,这是构建成功的全栈应用程序的关键。

除了身份验证,Supabase 还具有其他功能,例如 Postgres 数据库、实时订阅和对象存储。我相信 Supabase 是最容易上手或集成的后端即服务之一。

在本文中,我们将学习如何使用 Next.js 和 Supabase 构建一个全栈应用程序。我们将讨论如何设置 Supbase 项目、配置 UI 以及实现身份验证和功能。

该应用程序的概念是让用户根据指定的参数跟踪和创建锻炼活动,如果有任何错误或必要的更改,则编辑这些活动,并在需要时将其删除。让我们开始吧!

  • Next.js 和 Supabase 简介

  • 我们为什么要使用 Supabase?

  • 使用 Next.js 启动我们的项目

  • 设置 Supabase 项目并创建数据库表

  • 将 Next.js 与 Supabase 数据库连接

  • 配置我们应用的 UI

  • 实现用户认证

  • 实施锻炼功能

    • 获取所有锻炼

    • 创建新的锻炼

    • 更新锻炼

    • 删除锻炼

  • 部署到 Vercel

Next.js 和 Supabase 简介

Next.js 是构建生产就绪的 React 应用程序的最简单和最流行的方法之一。近年来,Next.js 经历了显着的指数级增长,许多公司都采用它来构建他们的应用程序。

我们为什么要使用 Supabase?

Supabase 是基于 PostgreSQL 数据库构建的Firebase 的无服务器、开源替代品。它提供了创建全栈应用程序所需的所有后端服务。

作为用户,您可以从 Supbase 界面管理您的数据库,范围从创建表和关系到在 PostgreSQL 之上编写 SQL 查询和实时引擎。

Supabase 带有非常酷的功能,使您的全栈应用程序开发更加容易。其中一些功能包括:

  • 行级安全性 (RLS) – Supabase 带有 PostgreSQL RLS 功能,允许您限制数据库表中的行。创建策略时,直接使用 SQL 创建它们

  • 实时数据库——Supabase 在 PostgreSQL 数据库上有一个更新功能,可以用来监听实时变化

  • Supabase UI——Supabase 有一个开源的用户界面组件库,可以快速高效地创建应用程序

  • 用户身份验证 – Supbaseauth.users在您创建数据库后立即创建一个表。当您创建应用程序时,Supabase 也会在您注册应用程序后立即分配一个用户和 ID,该应用程序可以在数据库中引用。对于登录方法,您可以通过多种方式验证用户身份,例如电子邮件、密码、魔术链接、Google、GitHub 等

  • 边缘函数——边缘函数是分布在边缘的 TypeScript 函数,靠近用户。它们可用于执行与第三方集成或监听 WebHooks 等功能

使用 Next.js 启动我们的项目

要使用 Next.js 模板在终端中启动我们的项目,我们将运行以下命令:

npx create-next-app nextjs-supabase

nextjs-supabase是我们应用程序的文件夹名称,我们将在其中包含 Next.js 应用程序模板。

稍后我们需要安装 Supabase 客户端包以连接到我们的 Next.js 应用程序。我们可以通过运行以下任一命令来做到这一点:

yarn add @supabase/supabase-js

或者

npm i @supabase/supabase-js

应用程序完成设置后,在您喜欢的代码编辑器中打开该文件夹。现在,我们可以删除/pages/index.js文件中的基本模板,并将其替换为h1标题为“欢迎使用 Workout App”。


超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →


完成后,在终端中运行命令以在http://localhost:3000yarn dev启动您的应用程序。你应该看到这样的页面:

设置 Supabase 项目并创建数据库表

要设置 Supabase 项目,请访问app.supabase.com以使用您的 GitHub 帐户登录应用仪表板。

登录后,您可以通过单击All Projects创建您的组织并在其中设置一个新项目。

单击新建项目并为您的项目指定名称和数据库密码。单击创建新项目按钮;您的项目需要几分钟才能启动并运行。

创建项目后,您应该会看到如下所示的仪表板:

对于本教程,我已经创建了一个名为workout-next-supabase.

现在,让我们通过单击仪表板上的SQL 编辑器图标并单击New Query创建我们的数据库表。在编辑器中输入下面的 SQL 查询,然后单击RUN执行查询。

CREATE TABLE workouts (
 id bigint generated by default as identity primary key,
 user_id uuid references auth.users not null,
 user_email text,
 title text,
 loads text,
 reps text,
 inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);
​
alter table workouts enable row level security;
​
create policy "Individuals can create workouts." on workouts for
   insert with check (auth.uid() = user_id);
​
create policy "Individuals can update their own workouts." on workouts for
   update using (auth.uid() = user_id);
​
create policy "Individuals can delete their own workouts." on workouts for
   delete using (auth.uid() = user_id);
​
create policy "Workouts are public." on workouts for
   select using (true);

这将创建我们将用于构建 CRUD 应用程序的锻炼表。

除了创建表格外,还将启用行级权限,以确保只有授权用户才能创建、更新或删除其锻炼的详细信息。

要查看锻炼表的外观,我们可以单击仪表板上的表格编辑器图标来查看我们刚刚创建的锻炼表。


来自 LogRocket 的更多精彩文章:

  • 不要错过来自 LogRocket 的精选时事通讯The Replay

  • 了解LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect优化应用程序的性能

  • 在多个 Node 版本之间切换

  • 了解如何使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri,一个用于构建二进制文件的新框架

  • 比较NestJS 与 Express.js


对于这个应用程序,我们将有七列:

  • user_id

  • user_email

  • id

  • title

  • loads

  • reps

  • Date stamp

一旦我们的表和列设置好了,下一步就是将我们的 Supabase 数据库与我们的 Next.js 前端应用程序连接起来!

将 Next.js 与 Supabase 数据库连接

要将 Supabase 与我们的 Next.js 应用程序连接起来,我们将需要我们的项目 URL和Anon Key。这两个都可以在我们的数据库仪表板上找到。要获取这两个密钥,请单击齿轮图标转到设置,然后单击API。您会看到这两个键显示如下:

当然,我们不想在浏览器或我们的存储库上公开这些值,因为它是敏感信息。.env.local对我们来说,Next.js 提供了对环境变量的内置支持,允许我们在项目的根目录中创建文件。这将加载我们的环境变量,并通过前缀将它们暴露给浏览器NEXT_PUBLIC。

.env.local现在,让我们在项目的根目录中创建一个文件,并在文件中包含我们的 URL 和密钥。

.env.local
​
NEXT_PUBLIC_SUPABASE_URL= // paste your project url here
NEXT_PUBLIC_SUPABASE_ANON_KEY= // paste your supabase anon key here

注意,不要忘记包含.env.local在您的gitignore文件中,以防止在部署时将其推送到 GitHub 存储库(并且可供所有人查看)。

supabase.js现在让我们通过在项目的根目录创建一个名为的文件来创建我们的 Supabase 客户端文件。在该supabase.js文件中,我们将编写以下代码:

// supabase.js
import { createClient } from "@supabase/supabase-js";
​
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
​
export const supabase = createClient(supabaseUrl, supabaseKey);

在这里,我们createClient从 Supabase 导入一个函数并创建一个名为supabase. 我们调用该createClient函数,然后传入我们的参数:URL ( supabaseUrl) 和 Anon Key ( supabaseKey)。

现在,我们可以在项目的任何地方调用和使用 Supbase 客户端了!

配置我们应用的 UI

首先,我们需要配置我们的应用程序以使其看起来像我们想要的那样。我们将有一个带有项目名称的导航栏,以及首次加载应用程序时的登录和注册选项。当用户注册并登录时,我们将显示导航栏,其中包含Home、Logout和Create Workout按钮。

网站上的每一页也会有一个页脚。

为此,我们将创建一个component文件夹来存放Navbar.js和Footer.js文件。然后,在内部_app.js,我们将pages使用Navbar和 组件包装我们的组件,Footer以便它们显示在应用程序的每个页面上。

// _app.js
import Footer from "../components/Footer";
import Navbar from "../components/Navbar";
import "../styles/globals.css";
​
function MyApp({ Component, pageProps }) {
 return (
   <div>
     <Navbar/>
     <Component {...pageProps}/>
     <Footer />
   </div>
 );
}
​
export default MyApp;

我在这里创建了一个 GitHub 要点,以查看这两个组件与我使用的样式一起看起来像什么。

现在,我们的主页应该是这样的:

实现用户认证

为了实现用户身份验证,我们将在我们的_app.js文件中初始化用户状态并创建一个validateUser函数来检查和验证用户。然后我们将用户状态设置为返回的会话对象。

// _app.js
​
import { useState, useEffect } from "react";
import Footer from "../components/Footer";
import Navbar from "../components/Navbar";
import "../styles/globals.css";
import { supabase } from "../utils/supabase";
​
function MyApp({ Component, pageProps }) {
 const [session, setSession] = useState(null);
​
 useEffect(() => {
   setSession(supabase.auth.session());
   supabase.auth.onAuthStateChange((_event, session) => {
     setSession(session);
   });
 }, []);
 return (
   <div>
     <Navbar session={session} />
     <Component {...pageProps} session={session} />
     <Footer />
   </div>
 );
}
export default MyApp;

当用户加载我们应用程序的主页时,我们希望显示一个按钮来告诉他们登录或注册。单击登录按钮时,它应该将用户重定向到用户可以输入其电子邮件和密码的页面。如果他们是现有用户并且登录详细信息有效,他们将被重定向到主页。

如果用户的凭据无效,则会显示一条警报消息以告知用户该问题。他们将看到一个注册选项。

当用户注册时,确认电子邮件将发送到他们输入的电子邮件。他们需要通过单击电子邮件正文中的链接来确认他们的电子邮件。

现在,当我们点击登录按钮时,我们应该被重定向到用户页面到这个页面:

现在,我们可以单击“注册”按钮并输入电子邮件。

单击此按钮后,将发送一封电子邮件以确认电子邮件地址。确认后,它将让我们登录,我们应该看到如下页面:

请注意,如果我们尚未登录,我们将无法看到我们的活动仪表板、创建新锻炼的按钮或注销。这是最初提到的由 Supbase 提供给我们的身份验证!

实施锻炼功能

现在,我们将深入探讨创建用户创建、修改和删除锻炼的能力。

获取所有锻炼

我们需要获取我们将要创建的所有锻炼并将它们呈现在主页上。我们将在index.js文件中执行此操作:

// /pages/index.js
import Head from "next/head";
import Link from "next/link";
import { useEffect, useState } from "react";
import styles from "../styles/Home.module.css";
import { supabase } from "../utils/supabase";
import WorkoutCard from "../components/WorkoutCard";

export default function Home({ session }) {
 const [workouts, setWorkouts] = useState([]);
 const [loading, setLoading] = useState(true);

 useEffect(() => {
   fetchWorkouts();
 }, []);

 const fetchWorkouts = async () => {
   const user = supabase.auth.user();
   try {
     setLoading(true);
     const { data, error } = await supabase
       .from("workouts")
       .select("*")
       .eq("user_id", user?.id);

     if (error) throw error;
     setWorkouts(data);
   } catch (error) {
     alert(error.message);
   } finally {
     setLoading(false);
   }
 };

 if (loading) {
   return <div className={styles.loading}>Fetching Workouts...</div>;
 }
 return (
   <div className={styles.container}>
     <Head>
       <title>Nextjs x Supabase</title>
       <meta name="description" content="Generated by create next app" />
       <link rel="icon" href="/favicon.ico" />
     </Head>

     <div className={styles.home}>
       {!session?.user ? (
         <div>
           <p>
             Welcome to Adrenargy. Kindly log in to your account or sign in for
             a demo
           </p>
         </div>
       ) : (
         <div>
           <p className={styles.workoutHeading}>
             Hello <span className={styles.email}>{session.user.email}</span>,
             Welcome to your dashboard
           </p>
           {workouts?.length === 0 ? (
             <div className={styles.noWorkout}>
               <p>You have no workouts yet</p>
               <Link href="/create">
                 <button className={styles.button}>
                   {" "}
                   Create a New Workout
                 </button>
               </Link>
             </div>
           ) : (
             <div>
               <p className={styles.workoutHeading}>Here are your workouts</p>
               <WorkoutCard data={workouts}/>
             </div>
           )}
         </div>
       )}
     </div>
   </div>
 );
}

在这个组件中,我们正在解构从文件中的 propssession传递的对象,并使用它来验证授权用户。如果没有用户,则不会显示仪表板。咚漫漫画App,国内外追番无限制,全网动漫资源一网打尽!如果有用户登录,则会出现锻炼仪表板。如果没有创建锻炼,则会出现一条“您还没有锻炼”的文字和一个创建新锻炼的按钮。page``_app.js

为了渲染我们创建的锻炼,我们有两个状态:workouts一个空数组和一个loading接受布尔值的状态true。我们useEffect用于在页面加载时从数据库中获取锻炼数据。

该fetchWorkouts函数用于调用 Supbase 实例以使用该select方法从我们的数据库中的锻炼表中返回所有数据。这 。eq()filter 方法用于过滤掉并仅返回用户 id 与当前登录用户匹配的数据。然后,setWorkouts设置为从数据库发送的数据,并在我们获取数据setLoading后设置回。false

如果仍在获取数据,则页面应显示“Fetching Workouts...”,并且如果向我们的数据库发出的请求返回我们的锻炼数组,我们希望通过该数组进行映射并渲染WorkoutCard组件。

在该WorkoutCard组件中,我们正在渲染锻炼标题、负荷、次数以及它的创建日期和时间。创建的时间正在使用您可以在此处查看date-fns的库进行格式化。我们将在下一节开始创建卡片时查看卡片的外观。

// Workoutcard.js

import Link from "next/link";
import styles from "../styles/WorkoutCard.module.css";
import { BsTrash } from "react-icons/bs";
import { FiEdit } from "react-icons/fi";
import { formatDistanceToNow } from "date-fns/";

const WorkoutCard = ({ data }) => {
 return (
   <div className={styles.workoutContainer}>
     {data?.map((item) => (
       <div key={item.id} className={styles.container}>
         <p className={styles.title}>
           {" "}
           Title: {""}
           {item.title}
         </p>
         <p className={styles.load}>
           {" "}
           Load(kg): {"  "}
           {item.loads}
         </p>
         <p className={styles.reps}>Reps:{item.reps}</p>
         <p className={styles.time}>
           created:{" "}
           {formatDistanceToNow(new Date(item.inserted_at), {
             addSuffix: true,
           })}
         </p>
       </div>
     ))}
   </div>
 );
};

export default WorkoutCard;

创建新的锻炼

现在我们已经登录,我们的仪表板是新鲜和干净的。为了实现创建新锻炼的能力,我们将分别在和文件夹中添加create.js和文件,并实现一些逻辑和样式。Create.module.csspagesstyles

// /pages/create.js

import { supabase } from "../utils/supabase";
import { useState } from "react";
import styles from "../styles/Create.module.css";
import { useRouter } from "next/router";

const Create = () => {
 const initialState = {
   title: "",
   loads: "",
   reps: "",
 };

 const router = useRouter();
 const [workoutData, setWorkoutData] = useState(initialState);

 const { title, loads, reps } = workoutData;

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

 const createWorkout = async () => {
   try {
     const user = supabase.auth.user();

     const { data, error } = await supabase
       .from("workouts")
       .insert([
         {
           title,
           loads,
           reps,
           user_id: user?.id,
         },
       ])
       .single();
     if (error) throw error;
     alert("Workout created successfully");
     setWorkoutData(initialState);
     router.push("/");
   } catch (error) {
     alert(error.message);
   }
 };

 return (
   <>
     <div className={styles.container}>
       <div className={styles.form}>
         <p className={styles.title}>Create a New Workout</p>
         <label className={styles.label}>Title:</label>
         <input
           type="text"
           name="title"
           value={title}
           onChange={handleChange}
           className={styles.input}
           placeholder="Enter a title"
         />
         <label className={styles.label}>Load (kg):</label>
         <input
           type="text"
           name="loads"
           value={loads}
           onChange={handleChange}
           className={styles.input}
           placeholder="Enter weight load"
         />
         <label className={styles.label}>Reps:</label>
         <input
           type="text"
           name="reps"
           value={reps}
           onChange={handleChange}
           className={styles.input}
           placeholder="Enter number of reps"
         />

         <button className={styles.button} onClick={createWorkout}>
           Create Workout
         </button>
       </div>
     </div>
   </>
 );
};

export default Create;

在这里,基本的 UI 范围是我们将有一个表单来创建一个新的锻炼。该表单将包含我们在创建数据库时指定的三个字段(标题、负载和代表)。

定义了一个初始状态对象来处理所有这些传递给workoutsData状态的字段。该onChange函数用于处理输入字段的更改。

该createWorkout函数使用 Supbase 客户端实例使用我们定义的初始状态字段创建新的锻炼并将其插入到数据库表中。

最后,当我们创建了新的锻炼时,我们有一个警报 toast 通知我们。

然后,一旦我们的锻炼被创建,我们将表单数据设置回初始的空字符串状态。之后,我们使用该router.push方法将用户导航回主页。

更新锻炼

要更新锻炼,我们将edit在我们的文件夹中创建一个名为的文件夹,该文件pages夹将保存我们的[id].js文件。我们将在链接到此页面的锻炼组件卡上创建一个编辑链接图标。当卡片呈现在主页上时,我们可以单击此编辑图标,它将带我们进入该特定卡片的编辑页面。

然后,我们将获取所需锻炼卡的详细信息,以由其id和卡的授权所有者从我们的锻炼表中更新。然后,我们将创建一个updateWorkout函数来更新我们的锻炼卡详细信息:

// /pages/edit/[id].js
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import styles from "../../styles/Edit.module.css";
import { supabase } from "../../utils/supabase";

const Edit = () => {
 const [workout, setWorkout] = useState("");
 const router = useRouter();

 const { id } = router.query;
 useEffect(() => {
   const user = supabase.auth.user();
   const getWorkout = async () => {
     const { data } = await supabase
       .from("workouts")
       .select("*")
       .eq("user_id", user?.id)
       .filter("id", "eq", id)
       .single();
     setWorkout(data);
   };
   getWorkout();
 }, [id]);

 const handleOnChange = (e) => {
   setWorkout({
     ...workout,
     [e.target.name]: e.target.value,
   });
 };

 const { title, loads, reps } = workout;
 const updateWorkout = async () => {
   const user = supabase.auth.user();
   const { data } = await supabase
     .from("workouts")
     .update({
       title,
       loads,
       reps,
     })
     .eq("id", id)
     .eq("user_id", user?.id);

   alert("Workout updated successfully");

   router.push("/");
 };
 return (
   <div className={styles.container}>
     <div className={styles.formContainer}>
       <h1 className={styles.title}>Edit Workout</h1>
       <label className={styles.label}> Title:</label>
       <input
         type="text"
         name="title"
         value={workout.title}
         onChange={handleOnChange}
         className={styles.updateInput}
       />
       <label className={styles.label}> Load (kg):</label>
       <input
         type="text"
         name="loads"
         value={workout.loads}
         onChange={handleOnChange}
         className={styles.updateInput}
       />
       <label className={styles.label}> Reps:</label>
       <input
         type="text"
         name="reps"
         value={workout.reps}
         onChange={handleOnChange}
         className={styles.updateInput}
       />

       <button onClick={updateWorkout} className={styles.updateButton}>
         Update Workout
       </button>
     </div>
   </div>
 );
};

export default Edit;

首先,我们创建一个状态来存储将从我们的表中获取的锻炼卡详细信息。然后,我们使用钩子提取该id卡的。useRouter该getWorkout函数调用 Supabase 客户端实例来过滤该id锻炼卡并返回数据(标题、负荷和次数)。

返回锻炼卡详细信息后,我们可以创建updateWorkout函数以使用该函数修改详细信息.update()。一旦用户更新了锻炼并单击了更新锻炼按钮,就会发送一条警报消息,并且用户将被重定向回主页。

让我们看看它是如何工作的。

点击编辑图标进入编辑页面。我们将把标题从“Dumbell Press”重命名为“Arm Curl”:

删除锻炼

要删除每张卡片上的锻炼,我们将创建handleDelete将id作为参数的函数。我们将调用 Supbase 实例以使用

.delete()功能。这.eq('id', id)指定id要在表中删除的行。

 const handleDelete = async (id) => {
   try {


     const user = supabase.auth.user();
     const { data, error } = await supabase
       .from("workouts")
       .delete()
       .eq("id", id)
       .eq("user_id", user?.id);
     fetchWorkouts();
     alert("Workout deleted successfully");
   } catch (error) {
     alert(error.message);
   }
 };

用于检查正在删除的eq('user_id', user?.id)卡是否属于该特定用户。该函数将被传递给文件中的WorkoutCard组件index.js并解构以供组件本身使用,如下所示:

const WorkoutCard = ({ data, handleDelete }) => {
 return (
   <div className={styles.workoutContainer}>
     {data?.map((item) => (
       <div key={item.id} className={styles.container}>
         <p className={styles.title}>
           {" "}
           Title: {""}
           {item.title}
         </p>
         <p className={styles.load}>
           {" "}
           Load(kg): {"  "}
           {item.loads}
         </p>
         <p className={styles.reps}>Reps:{item.reps}</p>
         <p className={styles.time}>
           created:{" "}
           {formatDistanceToNow(new Date(item.inserted_at), {
             addSuffix: true,
           })}
         </p>

         <div className={styles.buttons}>
           <Link href={`/edit/${item.id}`}>
             <a className={styles.edit}>
               <FiEdit />
             </a>
           </Link>
           <button
             onClick={() => handleDelete(item.id)}
             className={styles.delete}
           >
             <BsTrash />
           </button>
         </div>
       </div>
     ))}
   </div>
 );
};

成功删除卡后,将显示警报 Toast,并将用户重定向到主页。

部署到 Vercel

现在,我们必须将我们的应用程序部署到 Vercel,以便 Internet 上的任何人都可以使用它!

要部署到 Vercel,您必须首先将代码推送到存储库,登录到 Vercel 仪表板,单击Create New Project,然后单击刚刚将代码推送到的存储库。

在Environment Variable字段中输入我们之前创建的环境变量及其值 (NEXT_PUBLIC_SUPABASE_URL和NEXT_PUBLIC_SUPABASE_ANON_KEY),然后单击Deploy以将您的应用程序部署到生产环境。

我们终于得到它了!

结论

感谢您的阅读!我希望本教程为您提供使用 Next.js 和 Supabase 创建全栈应用程序所需的知识。

您可以根据用例自定义样式,因为本教程主要关注创建全栈应用程序的逻辑。

猜你喜欢

转载自blog.csdn.net/weixin_47967031/article/details/127319358#comments_25390177