如何用Next.js和Supabase构建全栈应用(附代码示例)

2,229 阅读15分钟

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

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

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

在这篇文章中,我们将学习如何使用Next.js和Supabase构建一个全栈应用**。**我们将讨论如何设置Supabase项目,配置用户界面,并实现认证和功能。

这个应用程序的概念是让用户根据指定的参数跟踪和创建锻炼活动,如果有任何错误或必要的变化,可以编辑这些活动,如果需要的话,可以删除它们。让我们开始吧!

Next.js和Supabase简介

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

我们为什么要使用Supabase?

Supabase是Firebase的一个无服务器、开源的替代品,建立在PostgreSQL数据库之上。它提供了创建一个全栈应用程序所需的所有后台服务。

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

Supabase具有非常酷的功能,使你的全栈应用开发更加容易。其中的一些功能是:

  • 行级安全(RLS) - Supabase带有PostgreSQL的RLS功能,允许你限制数据库表中的行。当你创建策略时,你直接用SQL创建它们
  • 实时数据库 - Supabase在PostgreSQL数据库上有一个更新功能,可以用来监听实时变化。
  • Supabase UI - Supabase有一个开源的用户界面组件库,可以快速有效地创建应用程序
  • 用户认证 - Supabase在你创建数据库时就会创建一个auth.users 表。当你创建一个应用程序时,Supabase也会在你在应用程序上注册后立即分配一个可以在数据库内引用的用户和ID。对于登录方法,有不同的方式可以验证用户,如电子邮件、密码、魔法链接、谷歌、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 ,标题为 "欢迎来到锻炼应用程序"。

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

Welcome Screen

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

要建立一个Supabase项目,请访问app.supabase.com,用你的GitHub账户登录到应用仪表板:

Supabase Dashboard

登录后,你可以创建你的组织,并通过点击所有项目在其中设置一个新项目:

All Projects And Organization Screen

点击新项目,给你的项目一个名称和数据库密码。点击创建新项目按钮;你的项目将需要几分钟的时间来启动和运行。

Create A New Project

一旦项目被创建,你应该看到一个像这样的仪表板:

Workout Next Supabase Dashboard

在本教程中,我已经创建了一个项目,名为workout-next-supabase.

现在,让我们通过点击仪表板上的SQL编辑器图标并点击新查询来创建我们的数据库表。在编辑器中输入下面的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);

Execute Your Query

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

在创建表的同时,行级权限将被启用,以确保只有授权用户可以创建、更新或删除其锻炼的细节。

为了检查锻炼表的外观,我们可以点击仪表板上的表编辑器图标,查看我们刚刚创建的锻炼表。

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

  • user_id
  • user_email
  • id
  • title
  • loads
  • reps
  • Date stamp

Table Editor

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

将 Next.js 与 Supabase 数据库连接起来

要将Supabase与我们的Next.js应用程序连接起来,我们将需要我们的项目URLAnon Key。这两个都可以在我们的数据库仪表板上找到。要获得这两个密钥,请点击齿轮图标,进入设置,然后点击API。你会看到这两个密钥像这样显示出来。

Url Api Setup

当然,我们不想把这些值公开暴露在浏览器或我们的存储库中,因为这是敏感信息。对我们有利的是,Next.js提供了对环境变量的内置支持,允许我们在项目的根部创建一个.env.local 文件。这将加载我们的环境变量,并通过以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

注意,不要忘记在你的gitignore 文件中包含.env.local ,以防止在部署时被推送到GitHub repo(并让所有人都能看到)。

现在让我们创建我们的Supabase客户端文件,在我们项目的根部创建一个名为supabase.js 的文件。在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);

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

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

配置我们应用程序的用户界面

首先,我们需要将我们的应用程序配置成我们想要的样子。我们将有一个带有项目名称的导航条,以及在应用程序首次加载时的登录注册选项。当用户注册并登录时,我们将显示导航条,有主页注销创建锻炼按钮。

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

为了做到这一点,我们将创建一个component 文件夹,其中将存放Navbar.jsFooter.js 文件。然后,在_app.js ,我们将用NavbarFooter 组件来包装我们的pages 组件,这样它们就会显示在应用程序的每个页面上:

// _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;

现在,我们的主页应该看起来像这样:

Adrenargy Home Screen

实施用户认证

为了实现用户认证,我们将在我们的_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;

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

如果用户的凭证无效,将显示一个警告信息,告诉用户这个问题。他们会看到一个注册的选项来代替。

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

Confirm Signup Email

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

Login Page

现在,我们可以点击注册按钮,并输入一个电子邮件。

Sign Up Page

一旦我们点击这个,将发送一封电子邮件来确认电子邮件地址。确认后,它将登录我们,我们应该看到这样一个页面。

Welcome Screen With No Workouts Yet

注意,如果我们没有登录,我们就无法看到我们的活动仪表板,无法看到创建新的锻炼的按钮,也无法注销。这是最初提到的认证,是由Supabase提供给我们的!

实现锻炼功能

现在,我们将深入研究如何创建一个用户的能力,以创建、修改和删除他们的锻炼。

取出所有锻炼项目

我们需要获取我们将要创建的所有锻炼,并在主页上呈现它们。我们将在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>
 );
}

在这个组件中,我们正在对_app.js 文件中从page 道具中传递的session 对象进行解构,并使用它来验证授权用户。如果没有用户,仪表盘将不会被显示。如果有一个用户登录,就会出现锻炼的仪表板。如果没有创建锻炼,就会出现 "你还没有锻炼 "的文字和一个创建新锻炼的按钮。

为了呈现我们创建的锻炼,我们有两个状态。workouts一个是空数组,另一个是loading ,这个状态接收一个布尔值true 。我们使用useEffect ,在页面加载时从数据库中获取锻炼数据。

fetchWorkouts 函数用于调用Supabase实例,使用select 方法从我们数据库中的锻炼表返回所有数据。eq() 过滤方法被用来过滤并只返回与当前登录用户的用户ID相匹配的数据。然后,setWorkouts 被设置为从数据库发送的数据,一旦我们获取了数据,setLoading 被设置回false

如果数据仍在获取中,页面应该显示 "正在获取锻炼......",如果向数据库发出的请求返回了我们的锻炼数组,我们要通过数组进行映射并渲染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;

创建一个新的健身活动

现在我们已经登录了,我们的仪表板是新鲜和干净的。为了实现创建新锻炼的能力,我们将在pagesstyles 文件夹中分别添加create.jsCreate.module.css 文件,并实现一些逻辑和风格设计:

// /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;

在这里,基本的用户界面范围是,我们将有一个表单来创建一个新的锻炼。这个表单将由三个字段组成(标题、负荷和次数),正如我们在创建数据库时指定的那样。

一个初始状态对象被定义为处理所有这些被传递到workoutsData 状态的字段。onChange 函数被用来处理输入字段的变化。

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

最后,我们有一个警报祝词,通知我们新的锻炼已经创建。

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

Create New Project

Workout Successfully Created

Dashboard With Workouts Dumbell Press

更新一个锻炼项目

为了更新一个锻炼,我们将在我们的pages 文件夹中创建一个叫做edit 的文件夹,这个文件夹将存放我们的[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;

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

一旦锻炼卡的细节被返回,我们就可以创建我们的updateWorkout 函数,使用.update()函数修改细节。一旦用户更新了锻炼,并且点击了更新锻炼的按钮,就会发出一条提醒信息,用户将被重新引导到主页。

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

点击编辑图标,进入编辑页面。我们将把标题从 "哑铃压腿 "改成 "卷臂":

Edit Workout Dumbell Press

Edit Workout Successful

Edit Workout With Arm Curl

删除一个锻炼项目

为了删除每张卡片上的锻炼,我们将创建handleDelete ,该函数将接收id 作为参数。我们将调用Supabase实例来删除一张锻炼卡。

.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) ,用于检查被删除的卡是否属于该特定用户。该函数将被传递给index.js 文件中的WorkoutCard 组件,并对该组件本身的使用进行解构,如下所示:

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>
 );
};

一旦卡片被成功删除,就会显示一个警报祝词,用户将被重定向到主页。

部署到Vercel

现在,我们必须将我们的应用程序部署到Vercel,这样互联网上的任何人都可以使用它

要部署到Vercel,你必须先把你的代码推送到你的仓库,登录到你的Vercel仪表板,点击创建新项目,然后点击你刚刚推送代码的那个仓库。

环境变量字段中输入我们先前创建的环境变量及其值 (NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY),然后点击部署,将你的应用程序部署到生产中。

Deploy To Vercel

就这样,我们拥有了它!

总结

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

你可以根据你的使用情况定制造型,因为本教程主要侧重于创建一个全栈应用程序的逻辑。