1. 项目概述从零构建一个现代化的全栈电商平台最近花了不少时间基于 Next.js 和 Supabase 完整地搭建了一个名为 RoabH Mart 的电商平台。这不仅仅是一个简单的“玩具项目”而是按照生产级标准从项目架构、技术选型到具体实现一步步构建起来的全功能应用。如果你正在寻找一个使用现代技术栈Next.js 14 App Router, Supabase, Tailwind CSS构建电商网站的实战参考或者想了解如何将 Docker 容器化部署融入开发流程那么我踩过的坑和总结的经验或许能帮你省下不少时间。RoabH Mart 的核心定位是一个清新、高效的在线购物平台采用了绿色主题的 UI 设计。它涵盖了电商的核心闭环用户浏览商品、加入购物车、安全结账、管理个人订单。技术栈上前端选择了 Next.js 14 的 App Router 架构搭配 React 和高度定制化的 Tailwind CSS后端则完全依托于 Supabase 这个强大的 BaaS后端即服务平台用它来处理用户认证、数据库操作和文件存储。为了让开发和部署更丝滑项目从一开始就集成了 Docker 和 Docker Compose。接下来我会详细拆解整个项目的设计思路、关键技术实现、那些容易出错的细节以及如何让这个应用真正跑起来。2. 技术栈深度解析与选型理由为什么是这套组合拳在启动一个项目前技术选型决定了后续开发的效率和项目的可维护性。对于 RoabH Mart 这样的全栈电商应用我主要从开发体验、性能、成本和团队协作几个维度进行考量。2.1 前端基石Next.js 14 与 App Router选择 Next.js 14 并采用其全新的 App Router绝非盲目追新。App Router 基于 React Server Components 构建这为电商场景带来了显著优势。商品列表、详情页这类内容驱动且对 SEO 要求高的页面非常适合使用服务端组件来渲染。这意味着页面可以直接在服务器上获取数据并生成 HTML首屏加载速度极快并且对搜索引擎友好。同时对于需要交互的组件如“加入购物车”按钮又可以灵活地使用客户端组件保证了用户体验的流畅性。这种混合渲染模式让开发者能更精细地控制性能和用户体验。此外App Router 的文件式路由将组件放在app目录下即自动成为路由大大简化了路由管理。像app/products/[id]/page.tsx这样的结构直观地对应了商品详情页降低了心智负担。内置的布局Layouts、加载状态Loading、错误处理Error和流式渲染Streaming等特性让构建健壮的应用变得更加标准化。2.2 样式与交互Tailwind CSS 与 Framer Motion样式方面我放弃了传统的 CSS-in-JS 方案选择了 Tailwind CSS。对于一个需要高度定制化主题比如我们特定的绿色系的项目Tailwind 的实用性无与伦比。通过在tailwind.config.ts中扩展主题色我可以轻松地确保整个应用的颜色体系一致。它的原子化 CSS 类名使得开发过程就像搭积木效率极高也避免了传统 CSS 可能出现的类名冲突和样式冗余问题。配合apply指令也能轻松提取出重复的样式组合为自定义组件类。对于微交互和页面过渡Framer Motion 是 React 生态中动画库的佼佼者。它的声明式 API 非常简洁例如为商品卡片添加一个悬停放大和轻微阴影的效果只需要几行代码。在页面切换或数据加载时适当的动画能显著提升应用的质感让 RoabH Mart 看起来不那么“干巴巴”。2.3 后端即服务为什么是 Supabase这是项目后端部分最重要的决策。Supabase 是一个开源的 Firebase 替代品提供了 PostgreSQL 数据库、实时订阅、身份认证、存储和边缘函数等一系列服务。对于个人开发者或小团队来说它极大地降低了全栈应用的后端门槛。数据库使用成熟的 PostgreSQL支持完整的 SQL 查询和关系型数据建模。我们的商品、用户、订单数据可以很好地用表关系来组织。认证内置了多种登录方式邮箱/密码、第三方OAuth并自动处理 JWT、会话管理、用户策略Row Level Security安全性有保障。存储商品图片、用户头像等静态资源可以直接上传到 Supabase Storage并生成可访问的 URL。实时性虽然当前项目未深入使用但其基于 PostgreSQL 实时订阅的功能为未来实现如“库存实时更新”、“客服聊天”等特性预留了可能。本地开发Supabase CLI 可以一键在本地启动完整的开发环境包括数据库和认证服务使开发调试与生产环境高度一致。使用 Supabase我无需自己搭建和维护用户系统、数据库 API 接口可以将绝大部分精力聚焦在前端业务逻辑和用户体验上。2.4 状态管理与工具链状态管理上我采用了分层策略。对于全局的、简单的状态如用户认证状态、购物车状态使用 React Context API 足以应对避免了引入 Redux 等重型库的复杂度。对于需要同步到 URL 的状态例如商品列表的排序参数、筛选条件我选择了nuqs这个库。它能让 URL 的查询参数query string与 React 状态双向绑定用户分享或刷新页面时状态不会丢失体验更好。开发工具方面项目使用 TypeScript 确保类型安全这在处理复杂的商品、订单数据结构时尤为重要能有效减少运行时错误。代码编辑器我强烈推荐Cursor它基于 VS Code 但深度集成了 AI 辅助编程在编写组件、查询数据库、调试 Docker 配置时能提供非常精准的代码补全和建议极大提升了开发效率。3. 项目架构设计与核心模块拆解一个清晰的项目结构是维护性的基石。RoabH Mart 采用了 Next.js App Router 推荐的结构并在此基础上根据业务模块进行了细分。/src /app /(marketing) # 营销页面首页、关于我们等布局组 page.tsx # 首页 layout.tsx /products /[id] # 动态路由商品详情页 page.tsx page.tsx # 商品列表页 /cart page.tsx # 购物车页面 /checkout page.tsx # 结账页面 /account /orders # 用户订单列表 page.tsx page.tsx # 用户账户概览 /auth /login # 登录页 page.tsx /register # 注册页 page.tsx /components /ui # 基础UI组件按钮、输入框、卡片等 /layout # 布局组件Header, Footer, Sidebar /products # 商品相关组件ProductCard, ProductGallery /cart # 购物车相关组件CartItem, CartSummary /checkout # 结账相关组件AddressForm, PaymentMethod /lib /supabase # Supabase 客户端初始化、辅助函数 client.ts auth.ts db.types.ts # 从数据库生成的 TypeScript 类型 /utils # 通用工具函数格式化价格、验证等 /hooks # 自定义 React Hooks如 useCart, useProducts /types # 全局 TypeScript 类型定义设计思路解析按功能组织路由/appApp Router 的核心思想。每个文件夹代表一个路由段page.tsx是页面的主组件。将营销页面用(marketing)分组是为了让它们共享一个不包含购物车侧边栏的布局而产品相关的页面则使用另一个布局。组件模块化/components将组件按业务域划分而非按类型如“容器组件”、“展示组件”。这样当需要修改商品相关功能时所有相关组件都在同一个目录下查找和修改非常方便。逻辑抽象/lib和/hooks将与 Supabase 交互的逻辑、通用的工具函数以及可复用的状态逻辑自定义 Hook抽离出来。例如/lib/supabase/client.ts负责初始化 Supabase 客户端并导出确保整个应用使用同一个实例。自定义 HookuseCart则封装了购物车的所有操作添加、删除、更新数量、计算总价任何组件只需调用这个 Hook 即可实现了业务逻辑与 UI 的分离。实操心得类型安全是开发体验的保障在lib/supabase/db.types.ts中我使用了 Supabase CLI 的supabase gen types typescript命令。这个命令会连接到你的数据库本地或远程自动生成所有表和视图对应的 TypeScript 类型定义。之后在编写数据查询代码时返回值、插入数据的结构都有完整的类型提示和检查几乎杜绝了因字段名拼写错误或类型不匹配导致的 bug。这是使用 Supabase 和 TypeScript 结合的一大爽点。4. 核心功能实现与关键代码剖析4.1 Supabase 集成与数据层设计首先需要在 Supabase 官网创建项目并获取 API 密钥。然后在本地环境变量文件.env.local中配置NEXT_PUBLIC_SUPABASE_URL你的项目URL NEXT_PUBLIC_SUPABASE_ANON_KEY你的匿名密钥接着创建lib/supabase/client.tsimport { createClient } from supabase/supabase-js; import { Database } from ./db.types; // 自动生成的类型 const supabaseUrl process.env.NEXT_PUBLIC_SUPABASE_URL!; const supabaseAnonKey process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!; export const supabase createClientDatabase(supabaseUrl, supabaseAnonKey);数据库表的设计是核心。以下是一个简化的表结构思路profiles扩展 Supabase 内置的auth.users表存储用户的公开信息如姓名、头像URL。通过user_id与auth.users关联。products商品表包含id,name,description,price,images存储图片URL数组category_id,inventory库存等字段。categories商品分类表。cart_items购物车项表关联用户和商品包含quantity数量。orders与order_items订单采用主-子表结构。orders表记录订单总览用户、总金额、状态、地址order_items记录每个订单包含的具体商品和数量。注意事项启用行级安全RLSSupabase 默认所有表都是禁止访问的。必须在 Supabase 控制台为每张表创建并启用 RLS 策略Policies。例如对于products表可以创建一条策略允许所有人查询CREATE POLICY 允许公开读取商品 ON products FOR SELECT USING (true);。对于cart_items表则需要创建策略确保用户只能操作自己的购物车项USING (auth.uid() user_id)。这是保障数据安全的关键一步切勿跳过。4.2 商品展示与购物车状态管理商品列表页/app/products/page.tsx主要是一个服务端组件它可以直接在服务器端查询数据import { supabase } from /lib/supabase/client; export default async function ProductsPage() { const { data: products, error } await supabase .from(products) .select(*) .eq(is_active, true) .order(created_at, { ascending: false }); if (error) { // 处理错误 } return ( div h1所有商品/h1 div classNamegrid grid-cols-1 md:grid-cols-3 gap-6 {products?.map((product) ( ProductCard key{product.id} product{product} / ))} /div /div ); }购物车状态管理是前端交互的核心。我创建了一个CartContext和自定义 HookuseCart。Context 负责存储全局的购物车状态从cart_items表初始化而useCart则封装了所有操作并处理与 Supabase 的同步。// /lib/contexts/cart-context.tsx use client; // 购物车需要交互必须是客户端组件 import { createContext, useContext, useEffect, useState } from react; import { CartItem } from /types; import { useSupabaseSession } from /lib/hooks/use-supabase-session; interface CartContextType { items: CartItem[]; addItem: (productId: string, quantity: number) Promisevoid; removeItem: (cartItemId: string) Promisevoid; updateQuantity: (cartItemId: string, quantity: number) Promisevoid; totalItems: number; totalPrice: number; } const CartContext createContextCartContextType | undefined(undefined); export function CartProvider({ children }: { children: React.ReactNode }) { const [items, setItems] useStateCartItem[]([]); const { session } useSupabaseSession(); // ... 从 Supabase 初始化购物车数据的逻辑 const addItem async (productId: string, quantity: number) { if (!session?.user) { // 未登录可以先将商品暂存到本地存储LocalStorage // 但最终结账前需要合并到服务器 const localCart JSON.parse(localStorage.getItem(localCart) || []); // ... 处理本地逻辑 return; } // 已登录直接操作数据库 const { data, error } await supabase .from(cart_items) .upsert({ user_id: session.user.id, product_id: productId, quantity }) .select(*, products(*)) .single(); if (!error data) { setItems((prev) { // 更新本地状态 const existing prev.find(item item.product_id productId); if (existing) { return prev.map(item item.id data.id ? data : item); } return [...prev, data]; }); } }; // ... 其他函数removeItem, updateQuantity const value { items, addItem, removeItem, updateQuantity, totalItems: items.reduce((sum, item) sum item.quantity, 0), totalPrice: items.reduce((sum, item) sum (item.products?.price || 0) * item.quantity, 0), }; return CartContext.Provider value{value}{children}/CartContext.Provider; } export const useCart () { const context useContext(CartContext); if (context undefined) { throw new Error(useCart must be used within a CartProvider); } return context; };4.3 用户认证与受保护路由Next.js 与 Supabase 的认证集成非常顺畅。我创建了一个高阶组件AuthGuard或使用中间件Middleware来保护需要登录的路由如/account和/checkout。更优雅的方式是在middleware.ts中实现// /middleware.ts import { createMiddlewareClient } from supabase/auth-helpers-nextjs; import { NextResponse } from next/server; import type { NextRequest } from next/server; export async function middleware(req: NextRequest) { const res NextResponse.next(); const supabase createMiddlewareClient({ req, res }); const { data: { session }, } await supabase.auth.getSession(); // 如果用户未登录且试图访问受保护路径重定向到登录页 if (!session (req.nextUrl.pathname.startsWith(/account) || req.nextUrl.pathname.startsWith(/checkout))) { const redirectUrl new URL(/auth/login, req.url); redirectUrl.searchParams.set(redirectedFrom, req.nextUrl.pathname); return NextResponse.redirect(redirectUrl); } return res; } export const config { matcher: [/account/:path*, /checkout/:path*], // 指定需要保护的路径 };在登录页面调用supabase.auth.signInWithPassword或supabase.auth.signUp即可。Supabase 会自动管理会话 Cookie用户登录后在整个应用中都可以通过supabase.auth.getSession()或useSupabaseSessionHook 获取到用户信息。5. Docker 容器化部署从开发到生产为了让任何协作者或部署环境都能一键启动项目容器化是最佳实践。RoabH Mart 提供了完整的 Docker 支持。5.1 Dockerfile 配置解析项目的Dockerfile采用了多阶段构建以减小最终镜像体积。# 第一阶段依赖安装 FROM node:18-alpine AS deps WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --onlyproduction # 第二阶段构建应用 FROM node:18-alpine AS builder WORKDIR /app COPY --fromdeps /app/node_modules ./node_modules COPY . . # 构建时需要生成环境变量确保传入正确的 Supabase 密钥 ARG NEXT_PUBLIC_SUPABASE_URL ARG NEXT_PUBLIC_SUPABASE_ANON_KEY ENV NEXT_PUBLIC_SUPABASE_URL$NEXT_PUBLIC_SUPABASE_URL ENV NEXT_PUBLIC_SUPABASE_ANON_KEY$NEXT_PUBLIC_SUPABASE_ANON_KEY RUN npm run build # 第三阶段运行环境 FROM node:18-alpine AS runner WORKDIR /app ENV NODE_ENV production # 创建非 root 用户以增强安全性 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --frombuilder /app/public ./public # 将构建产物和必要的 node_modules 复制过来 COPY --frombuilder --chownnextjs:nodejs /app/.next/standalone ./ COPY --frombuilder --chownnextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 CMD [node, server.js]关键点说明多阶段构建deps阶段只安装生产依赖builder阶段进行构建runner阶段只包含运行所需的最小文件最终镜像非常精简。非 Root 用户使用nextjs用户运行容器遵循安全最佳实践。Standalone 输出Next.js 构建时配置output: standalone会生成一个包含最小化 Node.js 服务器的standalone目录无需安装所有node_modules进一步减小镜像体积。5.2 Docker Compose 编排docker-compose.yml文件用于定义和运行多容器应用。虽然 RoabH Mart 目前只有一个应用容器但用 Compose 管理起来更方便也为未来添加数据库如本地测试用的 PostgreSQL或缓存服务留出了空间。version: 3.8 services: web: build: context: . args: NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} NEXT_PUBLIC_SUPABASE_ANON_KEY: ${NEXT_PUBLIC_SUPABASE_ANON_KEY} ports: - 3000:3000 environment: - NODE_ENVproduction - NEXT_PUBLIC_SUPABASE_URL${NEXT_PUBLIC_SUPABASE_URL} - NEXT_PUBLIC_SUPABASE_ANON_KEY${NEXT_PUBLIC_SUPABASE_ANON_KEY} # 健康检查确保服务已就绪 healthcheck: test: [CMD, wget, --no-verbose, --tries1, --spider, http://localhost:3000/api/health] interval: 30s timeout: 10s retries: 3 start_period: 40s restart: unless-stopped开发环境配置另外创建一个docker-compose.dev.yml使用卷挂载volumes将本地代码目录映射到容器内并开启npm run dev的监听模式实现代码热重载。# 启动开发环境 docker-compose -f docker-compose.dev.yml up5.3 构建与运行命令详解使用 Docker Compose推荐# 构建并启动容器后台运行 docker-compose up -d --build # 查看日志 docker-compose logs -f web # 停止并移除容器 docker-compose down仅使用 Docker# 构建镜像传递构建参数 docker build -t roabh-mart \ --build-arg NEXT_PUBLIC_SUPABASE_URL你的URL \ --build-arg NEXT_PUBLIC_SUPABASE_ANON_KEY你的密钥 . # 运行容器 docker run -p 3000:3000 --env-file .env.production roabh-mart避坑指南环境变量处理Docker 构建阶段ARG和运行阶段ENV的环境变量是分开的。构建 Next.js 应用时部分环境变量以NEXT_PUBLIC_开头的会在构建时被直接替换到代码中。因此必须在docker build时通过--build-arg传入这些变量。而在运行时可能还需要其他环境变量如数据库连接字符串这些需要在docker run或docker-compose.yml的environment部分设置。混淆这两个阶段是常见的错误会导致应用运行时找不到变量。6. 开发与部署中的常见问题排查在实际开发和部署 RoabH Mart 的过程中我遇到了一些典型问题这里记录下来供你参考。6.1 数据库连接与 RLS 策略问题问题前端应用能运行但无法从 Supabase 获取数据控制台报错401或403。排查检查环境变量首先确认NEXT_PUBLIC_SUPABASE_URL和NEXT_PUBLIC_SUPABASE_ANON_KEY是否正确设置并且在 Docker 构建和运行时都已正确传递。检查网络确保你的服务器或本地环境能够访问 Supabase 的服务地址非中国开发者请忽略此条国内访问需考虑网络连通性。检查 RLS 策略这是最常见的原因。登录 Supabase 控制台进入Authentication - Policies确认对应表如products的 SELECT 策略是否已启用并正确配置。对于需要登录后访问的表如cart_items确保策略中的auth.uid()逻辑正确。解决仔细阅读 Supabase 官方文档关于 RLS 的部分为每张表编写合适的策略。可以使用bypass RLS权限的服务端密钥进行临时测试以确定是否是 RLS 导致的问题但切勿在生产环境中使用。6.2 Next.js 构建与 Docker 缓存问题问题修改代码后重新构建 Docker 镜像但更改似乎没有生效。排查Docker 会分层缓存构建过程。如果package.json没有变化RUN npm ci这一层会被缓存导致新的依赖没有安装。如果源代码文件变化但构建层缓存导致旧的构建结果被使用。解决在开发时使用docker-compose -f docker-compose.dev.yml up它通过卷挂载直接使用本地代码避免构建缓存问题。在生产构建时可以尝试docker-compose build --no-cache进行全新构建或者更精细地利用缓存将COPY package*.json ./和RUN npm ci放在 Dockerfile 靠前位置这样只有当依赖变更时才会重建这一层将COPY . .放在后面这样代码变更不会触发依赖层的重建能加速构建。6.3 静态资源与图片优化问题商品图片加载慢或 Supabase Storage 的图片 URL 在 Next.js Image 组件中无法优化。解决使用 Next.js Image 组件它提供自动的图片优化格式转换、尺寸调整、懒加载等功能。需要配置next.config.js中的images.remotePatterns将 Supabase 的图片域名加入白名单。// next.config.js module.exports { images: { remotePatterns: [ { protocol: https, hostname: 你的Supabase项目ID.supabase.co, pathname: /storage/v1/object/public/**, }, ], }, };图片预处理上传商品图片时可以要求或使用工具预先生成多种尺寸的缩略图存储在 Supabase Storage 的不同路径下前端根据设备像素比加载合适尺寸的图片。6.4 购物车状态同步与离线支持问题用户未登录时添加的商品登录后如何合并到服务器购物车解决这是一个经典的离线购物车模式。我的实现思路是用户未登录时购物车数据保存在localStorage中一个简单的数组包含productId和quantity。当用户成功登录后在CartProvider的初始化逻辑或一个专门的useEffect中读取localStorage中的本地购物车数据。遍历本地购物车数据为每一项调用 Supabase 的upsert操作合并到服务器的cart_items表中。合并成功后清空localStorage中的本地购物车数据并从服务器重新拉取完整的购物车列表更新 Context 状态。这个过程需要处理好可能出现的冲突比如服务器购物车中已存在同一商品则需要合并数量。6.5 性能优化实践数据库查询优化避免 N1 查询在获取购物车项时使用 Supabase 的关联查询一次性获取商品信息.select(*, products(*))。只选择需要的字段不要总是select(*)对于列表页只选择展示所需的字段如select(id, name, price, image_url)。使用分页商品列表实现无限滚动或分页使用.range(start, end)。前端优化使用 React Cache 和use在 Next.js 14 中对于重复的数据请求可以使用React.cache()进行记忆化结合useHook 在服务端组件中获取数据减少重复请求。图片懒加载Next.js Image 组件默认支持。代码分割Next.js 的 App Router 和动态导入import()会自动进行代码分割确保首屏加载的 JS 尽可能小。7. 项目总结与扩展方向构建 RoabH Mart 的过程是一次对现代全栈开发技术的深度实践。从利用 Next.js 14 的服务器组件提升性能到依靠 Supabase 快速搭建安全可靠的后端再到用 Docker 实现环境标准化每一个环节都有其最佳实践和需要避开的“坑”。这个项目目前已经实现了电商的核心 MVP最小可行产品。如果你以此为基础进行扩展可以考虑以下几个方向支付集成接入 Stripe、支付宝或微信支付等支付网关。Supabase 的 Edge Functions 可以用来安全地处理支付回调。后台管理系统创建一个受保护的管理员路由如/admin用于管理商品、处理订单、查看数据报表。全文搜索当商品数量增多时简单的数据库LIKE查询会力不从心。可以集成 Algolia、MeiliSearch 或使用 PostgreSQL 的全文搜索扩展pgvector如果做语义搜索。推荐系统基于用户的浏览和购买历史实现简单的“看了又看”、“买了也买”的推荐功能。性能监控与 Analytics集成 Vercel Analytics、或使用 OpenTelemetry 来监控应用性能了解用户行为。最后关于部署你可以将构建好的 Docker 镜像推送到 Docker Hub 或 GitHub Container Registry然后在任何支持 Docker 的云服务器如 AWS ECS, Google Cloud Run, 阿里云容器服务或自己的 VPS 上通过docker run或docker-compose一键部署。如果追求极简Vercel 对 Next.js 项目的部署体验是无与伦比的虽然它不完全等同于运行一个 Docker 容器但其与 Next.js 的深度集成能带来更好的开发体验。希望这份详尽的拆解能帮助你理解如何构建一个类似的现代 Web 应用。最重要的是动手去做在编码和调试中你会对这套技术栈有更深刻的理解。