你真的以为“全栈”只是把前后端粘一起就完事了吗?

举报
bug菌 发表于 2026/01/13 16:17:58 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

前言:全栈这碗饭,真没你想的那么“香”,但也没那么“玄”

我见过太多人一开口就说自己全栈,语气比咖啡还浓:“我前端 React、后端 Node、数据库也懂点”。
  我通常不会当场拆台,毕竟成年人嘛,留点体面——但我心里会默默补一句:**“懂点”到底是懂到能扛上线,还是懂到能扛住线上炸?”**🙃

说白了,“全栈”不是“技能点摊得很薄”,而是你能把一个产品从 0 → 可用 → 可上线 → 可维护 → 可扩展 这条链路打通。你得考虑:

  • 前端渲染策略怎么选(SSR / RSC / CSR / 静态),怎么降低 JS 负担
  • 后端怎么分层、怎么验证、怎么限流、怎么记录日志、怎么做错误治理
  • 数据库怎么建模、怎么迁移、怎么索引、怎么避免慢查询
  • 环境变量怎么管、容器怎么跑、CI 怎么测、怎么避免“在我电脑上可以”

今天我就用一个“能落地”的项目骨架,把这些串起来。你会看到一堆代码,当然也会看到我一路吐槽——因为工程这玩意儿,不吐槽两句真干不下去😂。

1. 技术选型:我为啥选 Next.js + NestJS + Prisma + Postgres?

先别急着喷“又是 Next.js”,我知道你想说什么:“现在谁还不用它啊?”
  但选型不是追潮流,是为了减少你未来的返工概率

1.1 前端:Next.js App Router ——别只会“pages”,不然你像拿着诺基亚拍 8K

  • App Router 天生拥抱 React 的 Server Components、流式渲染、缓存策略等能力。Next.js 文档明确说明:App Router 是基于文件系统的路由,利用了 React 的新特性(Server Components、Suspense、Server Functions 等)。
  • 默认页面与布局是 Server Components:你可以把“数据获取 + 渲染”放在服务端,减少客户端 bundle。

我自己的口头禅:
“能在服务端做的事,就别让浏览器背锅。”
浏览器的命也是命。

1.2 后端:NestJS ——结构化到你想哭,但哭完你会感谢它

NestJS 的核心价值不是“它很 Node”,而是它把后端工程化的那套(模块、依赖注入、分层)直接摆你脸上。官方文档就是这么设计的:模块化组织、Provider(服务)作为核心概念。

如果你写过“一个文件 2000 行,谁改谁挨骂”的 Node 项目,Nest 会让你像从城中村搬进电梯房——
不一定更快乐,但更体面。

1.3 ORM:Prisma ——迁移、类型、安全感,一套带走

Prisma Migrate 是 Prisma 官方主推的迁移工具,和 schema 建模强绑定,开发环境上手很顺。 ([Prisma][4])

你当然也可以手写 SQL 迁移,当然也可以“我就喜欢 SQL 的粗粝感”。
但团队协作时,Prisma 的“规范感”能减少很多口水仗。

1.4 数据库:PostgreSQL ——稳重、强大、还能把 JSONB 玩出花

聊性能绕不开索引,PostgreSQL 官方文档对 GIN 索引解释得很清楚:它适合处理复合值,并在复合项内部搜索元素值。

你后面会看到我用 JSONB + GIN 做“可扩展字段”的实践。
因为现实世界里,“需求永远会变”,而你不可能每次都重构表结构。

1.5 部署与协作:Docker Compose + GitHub Actions ——少点玄学,多点自动化

  • Docker Compose 文件格式与多容器编排细节官方参考写得很细。
  • GitHub Actions 文档明确支持 CI/CD 工作流编排。

2. 项目目标:做一个“轻量但真能用”的创作后台 + 内容展示

我们来做个小但完整的产品:Creator Hub(创作中台 + 展示)。

功能清单(别怕,后面都有代码)

  • 用户登录(先做邮箱+密码示例,生产建议 MFA/SSO 视场景)
  • 创建内容(草稿/发布)
  • 内容列表与详情(支持搜索)
  • 审计日志(谁改了什么)
  • 运行方式:本地一条命令启动(docker compose up)
  • CI:自动 lint / test / build

我知道你想加“点赞收藏评论关注私信”,先按住——
**全栈最怕的不是功能多,是链路不通。**链路通了你加什么都不慌。

3. 目录结构:别整花活,清晰比“酷”重要

creator-hub/
  apps/
    web/          # Next.js App Router
    api/          # NestJS
  packages/
    shared/       # DTO、工具函数、zod schema 等共享
  infra/
    docker/       # compose, nginx(可选)
  .github/
    workflows/
  README.md

这里我特意用了 monorepo(你也可以不用)。
我这么做的原因很直白:类型共享统一工程规范更轻松,尤其是 DTO、验证规则、错误码这些“容易扯皮”的东西。

4. 前端(Next.js App Router):RSC 不是玄学,是“把锅还给服务器”

4.1 页面默认是 Server Component:减少客户端 JS

Next.js 文档说明:App Router 默认布局和页面是 Server Components,适合服务端取数并渲染,再把结果流式传给客户端。

我们做一个内容列表页:

apps/web/app/(site)/posts/page.tsx

import Link from "next/link";

type Post = {
  id: string;
  title: string;
  excerpt: string;
  createdAt: string;
};

async function fetchPosts(): Promise<Post[]> {
  // 这里走后端 API,演示用
  const res = await fetch(`${process.env.API_BASE_URL}/posts`, {
    // App Router 的 fetch 默认可被缓存/去重(具体策略看你的设置)
    cache: "no-store",
  });

  if (!res.ok) throw new Error("Failed to fetch posts");
  return res.json();
}

export default async function PostsPage() {
  const posts = await fetchPosts();

  return (
    <main style={{ padding: 24 }}>
      <h1>Posts</h1>
      <p style={{ opacity: 0.7 }}>
        这页是 Server Component:浏览器少背点 JS 包袱,我心里也踏实点。
      </p>

      <ul style={{ marginTop: 16 }}>
        {posts.map((p) => (
          <li key={p.id} style={{ marginBottom: 12 }}>
            <Link href={`/posts/${p.id}`}>
              <strong>{p.title}</strong>
            </Link>
            <div style={{ opacity: 0.75 }}>{p.excerpt}</div>
            <small style={{ opacity: 0.6 }}>{new Date(p.createdAt).toLocaleString()}</small>
          </li>
        ))}
      </ul>
    </main>
  );
}

这段代码的“爽点”在于:

  • 页面渲染在服务端完成
  • 数据获取在服务端完成
  • 客户端几乎不用 hydration(除非你放交互组件)

你可能会问:“那我想要搜索框这种交互怎么办?”
别急,Client Component 上场。

4.2 只把“需要交互”的部分做成 Client Component

Next.js 官方建议:"use client" 是边界声明,别把整页都拖进客户端 bundle。

apps/web/app/(site)/posts/SearchBox.tsx

"use client";

import { useMemo, useState } from "react";

export default function SearchBox({
  onChange,
}: {
  onChange: (keyword: string) => void;
}) {
  const [value, setValue] = useState("");

  const placeholder = useMemo(() => {
    const arr = ["搜标题?", "搜关键词?", "搜你忘了写啥的草稿?"];
    return arr[Math.floor(Math.random() * arr.length)];
  }, []);

  return (
    <div style={{ margin: "16px 0" }}>
      <input
        value={value}
        placeholder={placeholder}
        onChange={(e) => {
          const v = e.target.value;
          setValue(v);
          onChange(v);
        }}
        style={{
          padding: 10,
          width: "100%",
          maxWidth: 520,
          borderRadius: 10,
          border: "1px solid #ddd",
        }}
      />
    </div>
  );
}

然后在 Server Component 页面里引入它(注意:Server Component 可以渲染 Client Component,但交互只发生在 client):

import SearchBox from "./SearchBox";

export default async function PostsPage() {
  // 仍然服务端取数
  const posts = await fetchPosts();

  return (
    <main style={{ padding: 24 }}>
      <h1>Posts</h1>
      <SearchBox
        onChange={(k) => {
          // 演示:真实项目你可能用 router + searchParams
          console.log("keyword:", k);
        }}
      />
      {/* ...列表... */}
    </main>
  );
}

你看,交互是交互,渲染是渲染,锅分得很清楚。
“清楚”这种东西,写项目时比“聪明”更重要。

5. 后端(NestJS):把“写得快”换成“活得久”

5.1 NestJS 的模块化:别嫌麻烦,它是在帮你躲未来的坑

Nest 官方文档体系强调 Module / Provider(依赖注入)等概念。

我们做一个 Posts 模块:

apps/api/src/posts/
  posts.module.ts
  posts.controller.ts
  posts.service.ts
  dto/

posts.module.ts

import { Module } from "@nestjs/common";
import { PostsController } from "./posts.controller";
import { PostsService } from "./posts.service";

@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

posts.controller.ts

import { Controller, Get, Param, Query } from "@nestjs/common";
import { PostsService } from "./posts.service";

@Controller("posts")
export class PostsController {
  constructor(private readonly posts: PostsService) {}

  @Get()
  async list(@Query("q") q?: string) {
    return this.posts.list(q);
  }

  @Get(":id")
  async detail(@Param("id") id: string) {
    return this.posts.detail(id);
  }
}

posts.service.ts

import { Injectable, NotFoundException } from "@nestjs/common";

type Post = {
  id: string;
  title: string;
  content: string;
  createdAt: string;
};

@Injectable()
export class PostsService {
  private data: Post[] = [
    {
      id: "p1",
      title: "我第一次把 RSC 用明白了",
      content: "……(省略一万字情绪)",
      createdAt: new Date().toISOString(),
    },
    {
      id: "p2",
      title: "写 NestJS 的人,最后都会开始爱上分层",
      content: "……(真的,不骗你)",
      createdAt: new Date().toISOString(),
    },
  ];

  async list(q?: string) {
    if (!q) return this.data.map(this.toListItem);
    const k = q.toLowerCase();
    return this.data
      .filter((p) => p.title.toLowerCase().includes(k) || p.content.toLowerCase().includes(k))
      .map(this.toListItem);
  }

  async detail(id: string) {
    const found = this.data.find((p) => p.id === id);
    if (!found) throw new NotFoundException("Post not found");
    return found;
  }

  private toListItem(p: Post) {
    return {
      id: p.id,
      title: p.title,
      excerpt: p.content.slice(0, 60) + (p.content.length > 60 ? "…" : ""),
      createdAt: p.createdAt,
    };
  }
}

这只是“能跑”,离“能用”还差数据库、鉴权、校验、日志。
别急,我们一口口把它吃下去——但别噎着😅。

6. 数据库(PostgreSQL + Prisma):别让“数据”变成你最熟悉的陌生人

6.1 Prisma Schema:先把“内容”与“审计”建起来

Prisma Migrate 走的是 schema 驱动迁移。

apps/api/prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  createdAt DateTime @default(now())
  posts     Post[]
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String
  status    PostStatus @default(DRAFT)
  meta      Json?    // 给“可变字段”留口子(后面配 GIN)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id])
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  logs      AuditLog[]
}

model AuditLog {
  id        String   @id @default(cuid())
  action    String
  detail    Json?
  postId    String
  post      Post     @relation(fields: [postId], references: [id])
  createdAt DateTime @default(now())
}

enum PostStatus {
  DRAFT
  PUBLISHED
}

迁移命令(示例):

npx prisma migrate dev --name init

到这一步,你项目的“骨架”终于开始长肉了。
但接下来有个问题会冒出来:
“meta 是 JSON,怎么查?怎么快?”

6.2 JSONB + GIN:让“灵活字段”不至于把性能搞成笑话

PostgreSQL 官方文档解释 GIN 索引擅长在复合值中检索元素。 ([PostgreSQL][5])

我们可以给 Post.meta 建 GIN(在 Prisma 里你可能用 migration SQL):

apps/api/prisma/migrations/.../migration.sql(示意)

-- Prisma 默认会建表,但 JSONB 的索引你最好显式补上
CREATE INDEX IF NOT EXISTS idx_post_meta_gin
ON "Post"
USING GIN ("meta");

然后你就能做类似查询:

  • meta 里包含某个 tag
  • 查某个字段存在
  • 查结构化条件(具体操作符与写法按你使用方式来)

我特别喜欢这套组合的原因很现实:
产品经理一句“我想加个字段”,你不需要连夜改表、迁移、回填、上线。
你只需要:meta 里加,并保持必要的索引策略。

7. 鉴权与会话:别把“登录”当成一行代码,它是半个安全体系

我知道你想跳过这章——毕竟“安全”最不讨喜,写起来枯燥,还容易挨骂。
但你想想:用户信息、内容后台、发布权限,哪个不是安全敏感点?

OWASP 的认证与会话管理建议非常务实,比如:认证强度、MFA、会话失效、超时、Cookie 安全属性等,都是你需要系统性考虑的点。

7.1 一个现实的折中:演示版用 JWT,生产版更推荐“短 token + 刷新机制 + 绑定策略”

先声明:
JWT 不是原罪,乱用才是。
“把 30 天有效期的 JWT 塞 localStorage”这种操作,属于“自信且危险”。

NestJS 示例:登录签发 token(简化)

import { Injectable, UnauthorizedException } from "@nestjs/common";
import * as jwt from "jsonwebtoken";
import * as crypto from "crypto";

@Injectable()
export class AuthService {
  // 真实项目请用 bcrypt/argon2 之类做密码哈希,这里演示用
  private hash(pwd: string) {
    return crypto.createHash("sha256").update(pwd).digest("hex");
  }

  async login(email: string, password: string) {
    const user = await this.findUserByEmail(email);
    if (!user) throw new UnauthorizedException("Invalid credentials");

    if (user.password !== this.hash(password)) {
      throw new UnauthorizedException("Invalid credentials");
    }

    const token = jwt.sign(
      { sub: user.id, email: user.email },
      process.env.JWT_SECRET!,
      { expiresIn: "15m" } // 让它短一点,别恋战
    );

    return { token };
  }

  private async findUserByEmail(email: string) {
    // TODO: 用 Prisma 查库
    return null as any;
  }
}

7.2 会话管理:过期、失效、登出、绑定,都是“你迟早要补的债”

OWASP 会话管理指南提到会话应具备唯一性、不可猜测、失效机制与超时策略等要求。

你至少要做到:

  • 短期访问 token(15m 这种)
  • 刷新 token 存在 HttpOnly Cookie(降低 XSS 窃取风险)
  • 服务端维护 refresh token 的撤销(黑名单或版本号)
  • 登出时立刻失效
  • Cookie 设置 Secure / SameSite 等属性(按你的站点策略选择)

我每次看到“token 永不过期”,心里都想给自己也发一个“永不过期的工资”。
但现实通常不允许,对吧🙂。

8. API 设计:别只顾“能用”,也得顾“好维护”

8.1 错误结构统一:让前端少猜谜

建议统一返回结构(示例):

{
  "code": "POST_NOT_FOUND",
  "message": "Post not found",
  "requestId": "..."
}

并且服务端日志里带 requestId。
别小看这个——线上排查时它能救你命。

8.2 输入校验:别信“前端会帮我挡住”

你可以用 class-validator / zod 等。核心原则是:所有进入后端的数据都不可信
(是的,包括你自己写的前端。)

9. Docker Compose:把“在我电脑上能跑”按在地上摩擦

Docker Compose 官方参考强调:Compose 用于定义多容器应用的服务、网络、卷等。

infra/docker/docker-compose.yml(示例)

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_USER: postgres
      POSTGRES_DB: creator_hub
    ports:
      - "5432:5432"
    volumes:
      - db_data:/var/lib/postgresql/data

  api:
    build:
      context: ../../
      dockerfile: infra/docker/api.Dockerfile
    environment:
      DATABASE_URL: postgres://postgres:postgres@db:5432/creator_hub
      JWT_SECRET: "dev_secret_change_me"
    ports:
      - "4000:4000"
    depends_on:
      - db

  web:
    build:
      context: ../../
      dockerfile: infra/docker/web.Dockerfile
    environment:
      API_BASE_URL: http://api:4000
    ports:
      - "3000:3000"
    depends_on:
      - api

volumes:
  db_data:

你会发现,Compose 一旦写顺了,团队协作体验会变得很“正常”。
正常到你会怀疑:**“以前我为啥要手动装一堆东西然后祈祷它们别打架?”**😵‍💫

10. CI(GitHub Actions):让质量靠流程,而不是靠祈祷

GitHub Actions 官方文档说明它能在仓库内自动化 CI/CD 工作流。

.github/workflows/ci.yml(示例)

name: ci

on:
  push:
    branches: ["main"]
  pull_request:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Use Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Install
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Test
        run: npm run test --if-present

      - name: Build
        run: npm run build

CI 这东西最讽刺的一点是:
你不配它,大家都觉得“没必要”;
你配了它,大家才开始觉得“幸好配了”。
——典型的“事后诸葛亮型价值”。

11. 性能与缓存:别等用户骂你“卡”,你才想起优化

11.1 Next.js 的渲染拆分:别一刀切

Next.js 文档讲得很明白:Server/Client Components 的边界决定了渲染与 hydration 的分配方式。 ([Next.js][2])

你可以把策略总结成一句话:

  • 静态内容:尽量服务端渲染/缓存
  • 强交互:局部 Client Component
  • 列表页:分页、搜索、缓存要规划
  • 详情页:按更新频率决定缓存策略

11.2 数据库索引:别“等慢了再加”,那时你加的不是索引,是眼泪

GIN 索引只是例子,真正要做的是:

  • 查得最多的字段:BTree
  • 文本搜索:tsvector + GIN/GiST
  • JSONB:GIN
  • 组合条件:复合索引
  • 写多读少:别乱加索引(写放大很痛)

12. 工程习惯:看起来“啰嗦”,其实是在给未来的你写情书

我特别想把下面这几条钉在每个全栈项目的墙上:

  1. 写清楚环境变量清单.env.example
  2. 统一错误码(别让前端靠字符串判断)
  3. 请求日志 + requestId(线上问题 80% 靠它定位)
  4. 数据库迁移可追溯(别手动改生产库)
  5. 安全基线(密码存储、会话策略、Cookie 属性、CSRF/XSS 对策)
  6. CI 里跑 lint/test/build(别把质量寄托在“我检查过了”)

13. 结尾:你现在还觉得“全栈”只是把两端拼起来吗?

如果你看到这里,说明你至少愿意把“全栈”当一件正经事。
这比什么都重要。

全栈不是“我都会一点”,而是你能回答这些问题:

  • 当页面白屏时,你能判断是构建、路由、渲染还是接口问题吗?
  • 当接口变慢时,你能从日志、SQL、索引、缓存一路排查到根因吗?
  • 当权限被绕过时,你能从会话、鉴权、边界条件找到漏洞在哪吗?
  • 当需求改动时,你能让架构“改得动”,而不是“改不动就重写”吗?

所以我最后送你一句我自己写代码时常对自己说的话(带点情绪,别笑):

“别急着写酷的,先把对的写稳。”
酷是锦上添花,稳才是活下去的底气。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。