用 Next.js 和 Supabase 进行“全栈”开发的入门

文章目录

  • (零)前言
  • (一)创建Next.js应用程序
    • (1.1)新建工程目录
    • (1.2)安装依赖环境
    • (1.3)创建Tailwind配置
  • (二)创建Supabase项目
  • (三)Next.js应用的Supabase配置
  • (四)Next.js应用的页面
    • (4.1)/pages/_app.js
    • (4.2)/pages/index.js
    • (4.3)/pages/profile.js
    • (4.4)/pages/create-post.js
    • (4.5)/pages/posts/[id].js
  • (五)运行

(零)前言

没正经做过WEB开发。
刚初步了解html,css,javascript,然后试了下electron,就跳跃到这里了。

主要参考参考了这篇: 《使用 Next.js 和 Supabase 进行全栈开发》 <- 细节都请参考它(简称:原教程)。
本以为可以无脑复制粘贴,结果因为自己太不熟悉,以及版本变化,遇到了不少问题,特此记录。

概念:

  • 💡Next.js :基于 Node.js (中文) 的 React框架,它提供了服务器渲染、静态站点生成、路由、优化等高级功能。
  • 💡Supabase :开源的 Firebase 替代品。使用 Postgres 数据库、身份验证、即时 API、边缘函数、实时订阅、存储和矢量嵌入。

(一)创建Next.js应用程序

按照上面文章里的例子,完成个类似论坛的WEB项目。
可以做到注册发帖,前端使用Next.js,后端使用Supabase。
首先我们需要新建前端项目。

(1.1)新建工程目录

概念:

  • 💡NPM = Node(javascript) Package Manager,就是包管理器,类似python的pip呢。
  • 💡NPX = Node Package eXecute,包执行器,是npm5.2后带的命令行工具,用来执行包指令。

执行命令(我这里已经安装好node.js的)。

PS D:\XXX> npx create-next-app shion-forum

⚠️注意
因为太不熟悉,所以需要和文章中的目录结构相同,创建项目时需要选择如下:
先不使用App Router,也不使用TypeScript,这样就和作者的例子目录与文件结构一致了。
在这里插入图片描述

(1.2)安装依赖环境

cd .\shion-forum\
PS D:\XXX\shion-forum> npm install --legacy-peer-deps @supabase/supabase-js @supabase/ui react-
PS D:\XXX\shion-forum> npm install --legacy-peer-deps tailwindcss@latest @tailwindcss/typography 

⚠️注意
使用--legacy-peer-deps是因为依赖的版本冲突,从npm7.0开始,需要指定这个参数才能忽略冲突。
直接npm install会有一堆报错,类似如下:
在这里插入图片描述

(1.3)创建Tailwind配置

概念:

  • 💡Tailwind:实用程序优先的 CSS 框架,包含flex, pt-4, text-center, rotate-90等类,可以直接在标记中构建任何设计。

执行指令初始化文件:

PS D:\XXX\shion-forum> npx tailwindcss init -p

更新tailwind.config.js文件中这部分:

plugins: [
  require('@tailwindcss/typography')
]

再将styles/globals.css中的样式替换为以下内容(多的删掉)。

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

(二)创建Supabase项目

然后去Supabase.io创建一个官网托管的项目。

这部分没有需要注意或修改的,
可以完全参考: 原教程

建表脚本中可以看出,有记录级别的校验。
用户新建的帖子,只有用户自己可以修改删除。
但任何用户都可以查看所有的帖子。

CREATE TABLE posts (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users not null,
  user_email text,
  title text,
  content text,
  inserted_at timestamp with time zone default timezone('utc'::text, now()) not null
);

alter table posts enable row level security;

create policy "Individuals can create posts." on posts for
    insert with check (auth.uid() = user_id);

create policy "Individuals can update their own posts." on posts for
    update using (auth.uid() = user_id);

create policy "Individuals can delete their own posts." on posts for
    delete using (auth.uid() = user_id);

create policy "Posts are public." on posts for
    select using (true);

(三)Next.js应用的Supabase配置

在项目的根目录创建.env.local文件,并添加以下配置。

NEXT_PUBLIC_SUPABASE_URL= %YOUR_PROJECT_URL%
NEXT_PUBLIC_SUPABASE_ANON_KEY= %YOUR_PROJECT_API_KEY%

上面的%YOUR_PROJECT_URL%%YOUR_PROJECT_API_KEY%这两个地方。
需要填写的内容到Supabase网站你的托管项目中的:settings -> API 中查看,并修改上面文件的内容为URLAPI KEY的实际值。
在这里插入图片描述

然后在项目的根目录创建api.js文件,并添加以下代码:

// api.js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

(四)Next.js应用的页面

这部分代码由于原教程用的 supabase api 版本(v1)和现在(v2)不一样。
所以一些代码经过修改才能正常用,文件和代码如下:

(4.1)/pages/_app.js

// pages/_app.js
import Link from 'next/link'
import Head from 'next/head'
import { useState, useEffect } from 'react'
import { supabase } from '../api'
import '../styles/globals.css'

function MyApp({ Component, pageProps }) {
  const [session, setSession] = useState(null)
  const [user, setUser] = useState(null);

  async function checkUser() {
    const {
      data: { user },
    } = await supabase.auth.getUser()
    setUser(user)
  }

  useEffect(() => {
    supabase.auth.getSession().then(({ data: { session } }) => {
      setSession(session)
    })

    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((_event, session) => {
      setSession(session)
      checkUser()
    })
    checkUser()
    return () => subscription.unsubscribe()
  }, [])

  return (
    <div>
      <Head>
        <title>Shion's Test forum</title>
        <meta property="og:title" content="My page title" key="title" />
      </Head>
      <nav className="p-6 border-b border-gray-300 flex-auto text-lg font-semibold text-sky-500 dark:text-sky-400">
        <Link href="/">
          <span className="mr-6 cursor-pointer">首页(Home)</span>
        </Link>
        {
          session && user && (
            <Link href="/create-post">
              <span className="mr-6 cursor-pointer">新帖(Create Post)</span>
            </Link>
          )
        }
        <Link href="/profile">
          <span className="mr-6 cursor-pointer">用户(Profile)</span>
        </Link>
      </nav>
      {
        session && user && (
          <div className="py-2 px-16">
            <span className="text-m font-semibold">登录用户(login User) {user.email}</span>
          </div>
        )
      }
      <div className="py-8 px-16">
        <Component {...pageProps} />
      </div>
    </div>
  )
}

export default MyApp

(4.2)/pages/index.js

主界面,显示帖子列表。

// pages/index.js
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../api'

export default function Home() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)
  useEffect(() => {
    fetchPosts()
  }, [])
  async function fetchPosts() {
    const { data, error } = await supabase
      .from('posts')
      .select()
    setPosts(data)
    setLoading(false)
  }
  if (loading) return <p className="text-2xl">载入帖子中(Loading)...</p>
  if (!posts.length) return <p className="text-2xl">完全是空的(No posts.)</p>
  return (
    <div>
      <h1 className="text-3xl font-semibold tracking-wide mt-6 mb-2">帖子(Posts)</h1>
      {
        posts.map(post => (
          <Link key={post.id} href={`/posts/${post.id}`}>
            <div className="cursor-pointer border-b border-gray-300	mt-8 pb-4">
              <h2 className="text-xl font-semibold">{post.title}</h2>
              <p className="text-gray-500 mt-2">{post.user_email}</p>
            </div>
          </Link>)
        )
      }
    </div>
  )
}

(4.3)/pages/profile.js

用户登录界面,直接使用了supabase的auth。
可通过注册邮箱来登录系统。

// pages/profile.js
import { Typography, Button } from "@supabase/ui";
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
const { Text } = Typography
import { supabase } from '../api'

function Profile(props) {
    const { user } = Auth.useUser()
    if (user)
        return (
            <>
                <div className="w-80 shadow rounded">
                <Text>登录用户(Signed in): {user.email}</Text>
                <Button block onClick={() => props.supabaseClient.auth.signOut()}>
                    退出登录(Sign out)
                </Button>
                </div>
            </>
        );
    return props.children
}

export default function AuthProfile() {
    return (
        <Auth.UserContextProvider supabaseClient={supabase}>
            <Profile supabaseClient={supabase}>
                <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />
            </Profile>
        </Auth.UserContextProvider>
    )
}

(4.4)/pages/create-post.js

发新帖界面。

// pages/create-post.js
import { useState } from 'react'
import { v4 as uuid } from 'uuid'
import { useRouter } from 'next/router'
import dynamic from 'next/dynamic'
import "easymde/dist/easymde.min.css"
import { supabase } from '../api'

const SimpleMDE = dynamic(() => import('react-simplemde-editor'), { ssr: false })
const initialState = { title: '', content: '' }

function CreatePost() {
    const [post, setPost] = useState(initialState)
    const { title, content } = post
    const router = useRouter()
    function onChange(e) {
        setPost(() => ({ ...post, [e.target.name]: e.target.value }))
    }
    async function createNewPost() {
        if (!title || !content) return
        const {
            data: { user },
        } = await supabase.auth.getUser()
        const id = uuid()
        post.id = id
        const { data } = await supabase
            .from('posts')
            .insert([
                { title, content, user_id: user.id, user_email: user.email }
            ])
            .select()
            .single()
        router.push(`/posts/${data.id}`)
    }
    return (
        <div>
            <h1 className="text-3xl font-semibold tracking-wide mt-6">发新帖(Create new post)</h1>
            <input
                onChange={onChange}
                name="title"
                placeholder="Title"
                value={post.title}
                className="border-b pb-2 text-lg my-4 focus:outline-none w-full font-light text-gray-500 placeholder-gray-500 y-2"
            />
            <SimpleMDE
                value={post.content}
                                options={{
                    spellChecker: false,
                    toolbar: [
                       'bold',
                       'italic',
                       'heading',
                       '|',
                       'quote',
                       'code',
                       'table',
                       'horizontal-rule',
                       'unordered-list',
                       'ordered-list',
                       '|',
                       'link',
                       'image',
                       '|',
                       'side-by-side',
                       'fullscreen',
                       '|',
                       'guide'
                    ]
                }}
                onChange={value => setPost({ ...post, content: value })}
            />
            <button
                type="button"
                className="mb-4 bg-green-600 text-white font-semibold px-8 py-2 rounded-lg"
                onClick={createNewPost}
            >提交(Submit)</button>
        </div>
    )
}

export default CreatePost

(4.5)/pages/posts/[id].js

单个帖子查看界面。
这里需要动态的创建页面。

// /pages/posts/[id].js
import { useRouter } from 'next/router'
import ReactMarkdown from 'react-markdown'
import { supabase } from '../../api'

export default function Post({ post }) {
    const router = useRouter()
    if (router.isFallback) {
        return <div>Loading...</div>
    }
    return (
        <div>
            <h1 className="text-5xl mt-4 font-semibold tracking-wide">{post.title}</h1>
            <p className="text-sm font-light my-4">by {post.user_email}</p>
            <div className="mt-8">
                <ReactMarkdown className='prose' children={post.content} />
            </div>
        </div>
    )
}

export async function getStaticPaths() {
    const { data, error } = await supabase
        .from('posts')
        .select('id')
    const paths = data.map(post => ({ params: { id: JSON.stringify(post.id) } }))
    return {
        paths,
        fallback: true
    }
}

export async function getStaticProps({ params }) {
    const { id } = params
    const { data } = await supabase
        .from('posts')
        .select()
        .filter('id', 'eq', id)
        .single()
    return {
        props: {
            post: data
        }
    }
}

(五)运行

项目目录中执行:

PS D:\XXX\shion-forum>  npm run dev

> shion-forum@0.1.0 dev
> next dev

  ▲ Next.js 14.2.3
  - Local:        http://localhost:3000
  - Environments: .env.local

 ✓ Starting...
 ✓ Ready in 4.2s
 ○ Compiling / ...
 ✓ Compiled / in 2.5s (359 modules)
……

然后浏览器访问http://localhost:3000
初始状态如下图:
在这里插入图片描述


注册用户,登录。
然后发一些帖子后,
主界面如下图:
在这里插入图片描述


发帖后的帖子展示,同理首页点击某个帖子标题后。
如下图:
在这里插入图片描述
至此基本功能就OK了。
原教程后面部分内容就懒得弄了。
PS:为什么我的MD编辑器按钮都没图标……😢

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

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

相关文章

Leetcode—232. 用栈实现队列【简单】

2024每日刷题&#xff08;131&#xff09; Leetcode—232. 用栈实现队列 实现代码 class MyQueue { public:MyQueue() {}void push(int x) {st.push(x);}int pop() {if(show.empty()) {if(empty()) {return -1;} else {int ans show.top();show.pop();return ans;}} else {i…

管道液位传感器怎么接线

管道光电液位传感器是用来检测水管缺水的一种液位传感器&#xff0c;有水无水输出不同电压信号&#xff0c;在洗地机领域有着广泛的应用&#xff0c;那么管道液位传感器怎么接线&#xff1f; 管道液位传感器通常有三根线&#xff0c;电源线、地线和信号线&#xff0c;电源线接…

window golang 升级版本

执行go tidy&#xff0c;发现执行不了&#xff0c;得升级一下版本了 进入官网&#xff0c;并选择合适的系统以及版本。https://go.dev/dl/ 这台电脑是windows&#xff0c;我本人比较喜欢下载zip自己解压。 解压&#xff0c;这里我选择直接覆盖原文件&#xff0c;需要保留原版…

2024智能电网与能源系统国际学术会议(ICSGES2024)

2024智能电网与能源系统国际学术会议&#xff08;ICSGES2024) 会议简介 我们诚挚邀请您参加将在南京隆重举行的2024年智能电网与能源系统国际学术会议&#xff08;ICSGES2024&#xff09;。南京&#xff0c;一座历史与现代交织的城市&#xff0c;将为这场盛会提供独特的学术…

AVL树的原理及其实现

文章目录 前言了解AVL树AVL树的特点AVL树的节点调整方案右单旋为什么要右单旋呢&#xff1f;右单旋代码 左单旋为什么要左单旋&#xff1f;左单旋代码 左右双旋左右双旋之后平衡因子的情况左右双旋代码实现 右左双旋右左双旋代码&#xff1a; 简单测试 前言 回顾我们对于二叉搜…

HarmonyOS开发案例:【生活健康app之实现打卡功能】(2)

实现打卡功能 首页会展示当前用户已经开启的任务列表&#xff0c;每条任务会显示对应的任务名称以及任务目标、当前任务完成情况。用户只可对当天任务进行打卡操作&#xff0c;用户可以根据需要对任务列表中相应的任务进行点击打卡。如果任务列表中的每个任务都在当天完成则为…

基于 AI 的 Python 爬虫

✦ 支持 OPENAI、Gemini、Groq、本地 Ollama、Azure 等 LLM ✦ 只需传递 Prompt 和链接 注意&#xff1a; 调用 Ollama 模型&#xff0c;需要运行下方指令&#xff0c;拉取 embedding 模型&#xff1a; ollama pull nomic-embed-text 问题&#xff1a; 似乎不能换成兼容 Ope…

进程间通信 管道

前言 ubuntu系统的默认用户名不为root的解决方案&#xff08;但是不建议&#xff09;&#xff1a;轻量应用服务器 常见问题-文档中心-腾讯云 (tencent.com) 进程间通信的基本概念 进程间通信目的&#xff1a;进程间也是需要协同的&#xff0c;比如数据传输、资源共享、通知事件…

人脸图像生成(DCGAN)

一、理论基础 1.什么是深度卷积对抗网络&#xff08;Deep Convolutional Generative Adversarial Network&#xff0c;&#xff09; 深度卷积对抗网络&#xff08;Deep Convolutional Generative Adversarial Network&#xff0c;DCGAN&#xff09;是一种生成对抗网络&#xf…

计算机通信SCI期刊推荐,JCR1区,IF=6+,审稿快,无版面费!

一、期刊名称 Computer Communications 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;计算机科学 影响因子&#xff1a;6 中科院分区&#xff1a;3区 出版方式&#xff1a;订阅模式/开放出版 版面费&#xff1a;选择开放出版需支付$2300 三、期刊征稿…

STM32中的ICACHE是什么有什么用?如何使用?

什么是ICACHE&#xff1f; icache是一种用于缓存指令的存储器&#xff0c;其目的是提高CPU执行指令的效率。 在计算机系统中&#xff0c;icache&#xff08;指令缓存&#xff09;是处理器核心内部的一个关键组件&#xff0c;它专门用来存储最近使用过的指令。当CPU需要执行一个…

Bean的作用域

Bean的作用域 Bean的作用域是指在Spring整个框架中的某种行为模式&#xff0c;比如singleton单例作用域&#xff0c;就表示Bean在整个spring中只有一份&#xff0c;它是全局共享的&#xff0c;那么当其他人修改了这个值时&#xff0c;那么另一个人读取到的就是被修改的值 Bea…

每日OJ题_记忆化搜索②_力扣62. 不同路径(三种解法)

目录 力扣62. 不同路径 解析代码1_暴搜递归&#xff08;超时&#xff09; 解析代码2_记忆化搜索 解析代码3_动态规划 力扣62. 不同路径 62. 不同路径 难度 中等 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器…

element-ui skeleton 组件源码分享

今日简单分享 skeleton 骨架屏组件源码&#xff0c;主要从以下四个方面来讲解&#xff1a; 1、skeleton 组件的页面结构 2、skeleton 组件的属性 3、skeleton item 组件的属性 4、skeleton 组件的 slot 一、skeleton 组件的页面结构 二、skeleton 组件的属性 2.1 animate…

BS架构 数据权限--字段级权限 设计与实现

一、需求场景 1. 销售发货场景 销售出库单上 有 商品名称、发货数量、单价、总金额 等信息。 销售人员 关注 上述所有信息&#xff0c;但 仓管人员 不需要知道 单价、总金额 信息。 2. 配方、工艺保密 场景 配方研发人员 掌握核心配方&#xff0c; 但 交给车间打样、生产时…

一款免费的PDF转换工具分享

最近在吾爱上发现一款PDF免费转换工具&#xff0c;支持多种格式转换&#xff0c;试了一下&#xff0c;还不错 最重要的是免费&#xff0c;不用开会员转换&#xff0c;也没有限制&#xff08;文末有工具地址&#xff09; ps:转换完成后看一下是否符合&#xff0c;可能会有些许…

即插即用模块:Convolutional Triplet注意力模块(论文+代码)

目录 一、摘要 二、创新点总结 三、代码详解 论文&#xff1a;https://arxiv.org/pdf/2010.03045v2 代码&#xff1a;https://github. com/LandskapeAI/triplet-attention 一、摘要 由于注意机制具有在通道或空间位置之间建立相互依赖关系的能力&#xff0c;近年来在各种计…

Web功能测试之表单、搜索测试

初入职场接触功能测试老是碰到以下情况不知道怎么写测试用例&#xff1a; 一个界面很多搜索条件怎么写用例&#xff1f; 下拉框测试如何考虑测试点&#xff1f; 上传要考虑哪些验证点&#xff1f;...... 所以这篇主要是整理关于web测试之表单、搜索测试的相关要点。 一、表…

小程序(三)

十三、自定义组件 &#xff08;二&#xff09;数据方法声明位置 在js文件中 A、数据声明位置&#xff1a;data中 B、方法声明位置methods中&#xff0c;这点和普通页面不同&#xff01; Component({/*** 组件的属性列表*/properties: {},/*** 组件的初始数据*/data: {isCh…

离线使用evaluate

一、目录 步骤demorouge-n 含义 二、实现 步骤 离线使用evaluate: 1. 下载evaluate 文件&#xff1a;https://github.com/huggingface/evaluate/tree/main2. 离线使用 路径/evaluate-main/metrics/rougedemo import evaluate离线使用evaluate: 1. 下载evaluate 文件&…