文章目录
- (零)前言
- (一)创建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 中查看,并修改上面文件的内容为URL
和API 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编辑器按钮都没图标……😢