Published on

NextJS 零成本建站(含管理系统)

Authors
  • avatar
    Name
    Prover
    Twitter

next 零成本建站

要使用的技术栈:next + supabase(auth/database) 部署服务器:vecel(服务器/CI/CD)

创建一个 next 项目

参考官网的文章

  1. 直接命令行创建!

    npx create-next-app@latest
    
  2. 然后会让你选配置

    # 项目名
    What is your project named? my-app
    # 是否使用 TS
    Would you like to use TypeScript? No / Yes
    # 是否使用 EsLint
    Would you like to use ESLint? No / Yes
    # 是否使用 Tailwind CSS(推荐一下,不用自己想类名很爽hhh)
    Would you like to use Tailwind CSS? No / Yes
    # 要不要把建一个 src 文件夹来放代码(额,没试过选 no)
    Would you like to use `src/` directory? No / Yes
    # !!!! 很重要,app router 是新推出的(坑会多一点,本文也是基于这个),no 的话就还是 pages router
    Would you like to use App Router? (recommended) No / Yes
    # 是否要自定义 @/* 作为默认的引入前缀(从 src/ 出发),选 no 即可
    Would you like to customize the default import alias (@/*)? No / Yes
    # 上面选 yes 这里就会让你自己配置
    What import alias would you like configured? @/*
    
  3. 等待一会,会自动安装依赖,然后进目录就可以启动啦

    npm run dev [-- -p 更换默认端口号]
    

数据库

使用 supabase 作为免费的 Postgres 数据库存储(包括 500MB的数据存储 10,000条Postgres行的限制 50个并发连接)

  1. 安装依赖

    npm install @supabase/supabase-js @supabase/ssr
    
  2. 找个喜欢的位置(我选的是 src/lib/supabase )创建一下这两个文件

    server.ts

    import { createServerClient, type CookieOptions } from "@supabase/ssr";
    import { cookies } from "next/headers";
    
    export const supabase = () => {
        // 这里因为 cookies 不是动态的(每次请求都会自动获取),所以需要每次都要手动调用
        const cookieStore = cookies();
    
        return createServerClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL!,
            process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
            {
                cookies: {
                    get(name: string) {
                        return cookieStore.get(name)?.value;
                    },
                    set(name: string, value: string, options: CookieOptions) {
                        try {
                            cookieStore.set({ name, value, ...options });
                        } catch (error) {
                            console.warn("ss-set-cookie-error", error);
                            // The `set` method was called from a Server Component.
                            // This can be ignored if you have middleware refreshing
                            // user sessions.
                        }
                    },
                    remove(name: string, options: CookieOptions) {
                        try {
                            cookieStore.set({ name, value: "", ...options });
                        } catch (error) {
                            // The `delete` method was called from a Server Component.
                            // This can be ignored if you have middleware refreshing
                            // user sessions.
                        }
                    },
                },
            }
        );
    };
    

    client.ts

    import { createBrowserClient } from "@supabase/ssr";
    
    export const ccSupabase = () =>
    createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
    
    • 关于这里为什么要创建两种客户端,可能刚学 next app router 的不知道,next app router 分为服务器组件客户端组件,所以就需要创建两种对应的客户端
  3. src/ 目录下创建一个 middleware.ts (位置是固定的, 这个文件就相当于 next 请求的拦截器)

    import { NextResponse, type NextRequest } from "next/server";
    import { createServerClient, type CookieOptions } from "@supabase/ssr";
    
    export async function middleware(request: NextRequest) {
        const response = NextResponse.next({
            request: {
                headers: request.headers,
            },
        });
    
        const supabase = createServerClient(
            process.env.NEXT_PUBLIC_SUPABASE_URL!,
            process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
            {
                cookies: {
                    get(name: string) {
                        return request.cookies.get(name)?.value;
                    },
                    set(name: string, value: string, options: CookieOptions) {
                        request.cookies.set({ name, value, ...options });
                        response = NextResponse.next({
                            request: {
                                headers: request.headers,
                            },
                        })
                        response.cookies.set({ name, value, ...options });
                    },
                    remove(name: string, options: CookieOptions) {
                        request.cookies.set({ name, value: "", ...options });
                        response = NextResponse.next({
                            request: {
                                headers: request.headers,
                            },
                        })
                        response.cookies.set({ name, value: "", ...options });
                    },
                },
            }
        );
    
        await supabase.auth.getUser();
    
        return response;
    }
    
    export const config = {
        // Skip all paths that should not be internationalized.
        // This skips the folders "api", "_next" and all files
        // with an extension (e.g. favicon.ico)
        matcher: ["/((?!api|_next|.*\\..*).*)"],
    };
    
    • 国际化使用 next-itnl 整合 supabase 需要修改 response 的获取

      TODO:挖坑

  4. 去 supabase 上创建一个项目并进入到对应的 HOME 看板页,右上角有个 connect 按钮

    点进去 -> App Frameworks -> Nextjs / App router / supabase-js 就可以获取到相关的 NEXT_PUBLIC_SUPABASE_URLNEXT_PUBLIC_SUPABASE_ANON_KEY 复制下来保存到项目根目录下的 .env.local

  5. 通过 supabase 的 Table Editor 面板创建表格和列

  6. 接下来就可以通过 supabase 的客户端进行数据的 CRUD,具体的可以看[官方文档](Supabase Javascript Client - Fetch data)

认证

这里直接用的 supabase auth 提供的 ui,手写 UI 的会不一样

  1. 安装下依赖

    npm i @supabase/auth-ui-react @supabase/auth-ui-shared
    
  2. 引入 Auth 组件并配置客户端连接

    "use client";
    import { ccSupabase } from "@/lib/supabase/client";
    import { Auth } from "@supabase/auth-ui-react";
    
    export default function AuthModal() {
        return (
        	<Auth supabaseClient={ccSupabase()} />
        )
    }
    
  3. [修改主题]

    import { ThemeSupa } from "@supabase/auth-ui-shared";
    
    <Auth
        supabaseClient={ccSupabase()}
        appearance={{
            theme: ThemeSupa,
            // 如果要覆盖默认样式的话需要通过覆盖 css 变量来实现
            variables: {
                default: {
                    colors: {
                        brand: "rgba(96,165,250,0.88)",
                        brandAccent: "rgb(96,165,250)",
                    },
                },
            },
        }}
    />
    
  4. [关闭默认链接的显示]

    {/* 
            showLinks: 是否显示以下链接
              没有帐户?报名
              已经有帐户?登入
              发送魔法链接电子邮件
              忘记密码了吗?
            */}
    <Auth showLinks={false} ...
    
  5. [使用魔方链接代替邮箱密码注册]

     <Auth view="magin_link" ...
    
  6. [修改重定向的位置!]

    1. 新增 redirectTo 属性为指定的路径

      <Auth redirectTo="http://localhost:3858/auth/callback" ...
      
    2. 在对应的路由下新建一个 route.ts, 比如我这里是 /auth/callback 对应的文件路径就是 /app/auth/callback/route.ts

      route.ts:nextjs 用来处理对应路由下的请求,类似于一个小型的 middleware

      import { NextResponse } from "next/server";
      import { supabase } from "@/lib/supabase/server";
      
      export async function GET(request: Request) {
          const { searchParams, origin } = new URL(request.url);
          const code = searchParams.get("code");
          const next = searchParams.get("next") ?? "/";
      
          if (code) {
              // 这里主要是通过 code 调用 exchangeCodeForSession 方法获取 token 并保存
              const { error } = await supabase().auth.exchangeCodeForSession(code);
              if (!error) {
                  // 没有任何错误就跳转到之前的页面或者首页
                  return NextResponse.redirect(`${origin}${next}`);
              }
          }
      
          // 有错误就跳转到对应的处理页
          return NextResponse.redirect(`${origin}/auth/auth-code-error`);
      }
      
    3. 页面获取客户信息

      import { User } from "@supabase/supabase-js";
      import { ccSupabase } from "@/lib/supabase/client";
      import { useEffect, useState } from "react";
      
      const [user, setUser] = useState<User | null>(null);
      
      useEffect(() => {
          async function init() {
              const { data } = await ccSupabase().auth.getUser();
      
              setUser(data.user);
          }
      
          init();
      }, []);
      

Github 登录

supabase 支持很多种社交登录,也可以在这里看官方的[文档](Auth | Supabase Docs)

  1. 先去 supabase 对应项目中的 Authentication 面板,选择 Providers 打开 Github 的开关就可以得到 Callback URL

  2. 在 github 的个人账户上创建一个 OAuth APP,输入对应的 Callback URL 和 Homepage URL,就可以得到 ClientID 和 ClientSecret

  3. 在回到刚刚获取 Callback URL 的位置,输入得到的 ClientID 和 ClientSecret 即可

  4. 然后再对应的 Auth 组件的 providers 填入 github 即可

    <Auth providers={["github"]}
    

部署

这一步非常简单,只要再 Vecel 创建账户,关联仓库就可以,可以先本地打包看一下有没有一些报错

如果要添加生产环境的变量,可以在对应的 Vecel 项目面板中选择 Settings -> Environment Variables,在这里引入环境变量

加载中...