跳到主要内容

4.2 现代开发框架

为什么需要框架?

想象一下你要做一顿饭。

方式一:从零开始。 你去菜市场买菜、洗菜、切菜、调料、掌握火候……每一环节都自己来。做一顿饭可能要两小时,而且味道好不好全靠经验。

方式二:用预制菜/料理包。 食材洗好切好了,调料配好了,你只需要下锅炒几分钟。速度快,味道稳定,虽然灵活性差一点,但对大多数人来说足够好了。

框架就是编程世界的"料理包"。

理论上你可以用纯 HTML、CSS、JavaScript 手搓一个网站出来。但你会发现——写到第三页就开始崩溃了。每个页面都重复一堆导航栏代码,改个颜色要在十个文件里改,页面切换的时候整页刷新白屏闪一下……

框架帮你把这些"重复、通用、烦人"的事情都封装好了。路由、状态管理、样式方案、构建打包、性能优化……开箱即用。

为什么 AI 编程时代框架更关键? 因为 AI 对流行框架的理解最深、生成的代码质量最高。你让 AI 写一个 Next.js 的页面,它大概率写得又快又好;你让它写一个冷门框架的代码,它可能一本正经地胡说八道。选主流框架 = 让 AI 发挥最大能力。


Next.js:全栈开发的瑞士军刀

什么是 Next.js?

Next.js 是本书选择的 Web 全栈框架。它基于 React,并提供路由、服务端渲染、后端接口和部署集成,适合用一套项目完成本书的练习。

为什么选它? 四个理由:

  1. 全栈框架 —— 前端页面和后端 API 在同一个项目里,不用维护两套代码。就像你不需要单独租一个厨房了,Next.js 给你的是一个"前厅带厨房"的商铺。
  2. 部署集成 —— Next.js 与 Vercel 集成紧密,代码推到 GitHub 后可以自动生成预览和生产部署。额度与商业使用条款以当前计划为准。
  3. AI 友好 —— 目前 AI 编程工具(Cursor、Claude Code 等)对 Next.js 的支持最好,生成的代码能直接跑。
  4. 性能优秀 —— 自动代码分割、图片优化、服务端渲染,不用你手动优化。

创建项目

打开终端,一行命令搞定:

npx create-next-app@latest my-app --yes --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"

显式写出参数可以避免脚手架升级后交互问题变化,导致读者选出不同的项目结构。

为什么要这样选? TypeScript 让 AI 更容易理解你的代码;Tailwind CSS 是目前最好的样式方案;App Router 是 Next.js 13+ 推荐的新架构,性能更好。

项目结构完全解读

创建完成后,你会看到这样的目录结构:

my-app/
├── public/ # 静态资源(图片、字体等)
│ ├── next.svg # Next.js logo
│ └── vercel.svg # Vercel logo
├── src/
│ └── app/ # 核心目录!所有页面都在这里
│ ├── favicon.ico # 网站图标
│ ├── globals.css # 全局样式
│ ├── layout.tsx # 根布局(所有页面共用的外壳)
│ └── page.tsx # 首页(访问 / 时显示)
├── next.config.ts # Next.js 配置文件
├── package.json # 项目依赖和脚本
├── tsconfig.json # TypeScript 配置
└── postcss.config.mjs # PostCSS 配置(Tailwind 底层依赖)

目录会随 Next.js 和 Tailwind 版本变化。例如 Tailwind CSS v4 默认不再生成 tailwind.config.ts。判断项目是否正确,应以实际生成结果和官方升级文档为准,不要强行补齐旧教程里的文件。

重点理解 src/app/ 目录:这是你写代码的核心战场。每个文件夹代表一个路由(网址),每个文件名有特殊含义:

文件名作用必须?
page.tsx这个路由显示的页面内容
layout.tsx包裹子页面的布局外壳(导航栏、侧边栏等)可选
loading.tsx页面加载时显示的 loading 状态可选
error.tsx页面出错时显示的错误页面可选
not-found.tsx404 页面可选

页面和路由(文件即路由)

Next.js 最酷的设计之一:文件路径就是网址路径。你不需要手动配置路由表,创建文件就行。

src/app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
├── blog/
│ ├── page.tsx → /blog(文章列表)
│ └── [slug]/
│ └── page.tsx → /blog/my-first-post(动态路由)
├── dashboard/
│ ├── layout.tsx → 仪表盘的共同布局
│ ├── page.tsx → /dashboard
│ └── settings/
│ └── page.tsx → /dashboard/settings
└── api/
└── hello/
└── route.ts → GET /api/hello

方括号 [slug] 是动态路由,意思是这个位置可以是任何值。比如 /blog/hello-world/blog/nextjs-tutorial 都会匹配 blog/[slug]/page.tsx

来看一个实际的页面代码:

// src/app/about/page.tsx
// 这就是一个普通的 React 组件,Next.js 自动把它变成一个页面

export default function AboutPage() {
return (
<div>
<h1>关于我们</h1>
<p>这是一个用 Next.js 搭建的网站。</p>
</div>
)
}
// src/app/layout.tsx — 根布局,所有页面都被它包裹
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "我的网站",
description: "用 Next.js + Tailwind 打造",
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body className="bg-gray-50 text-gray-900 antialiased">
<nav className="p-4 bg-white shadow">
<a href="/" className="mr-4">首页</a>
<a href="/about" className="mr-4">关于</a>
<a href="/blog">博客</a>
</nav>
<main className="max-w-4xl mx-auto p-6">
{children}
</main>
</body>
</html>
);
}

children 就是当前页面的 page.tsx 渲染的内容。所有页面都会自动套上这个布局(导航栏 + main 容器)。

Server Components vs Client Components

Next.js App Router 引入了一个重要概念:默认所有组件都是 Server Components(服务端组件)

Server Components(服务端组件):

  • 在服务器上渲染,HTML 直接发给浏览器
  • 可以直接访问数据库、读文件、调用 API
  • 不能用 useStateuseEffect、事件监听(onClick 等)
  • 适合:展示数据、获取内容、不需要交互的页面

Client Components(客户端组件):

  • 在浏览器里渲染,可以交互
  • 需要在文件顶部加 "use client" 声明
  • 可以用 useStateuseEffect、事件监听
  • 适合:表单、按钮点击、实时更新、动画

怎么选择? 简单原则:默认用 Server Component,需要交互时才加 "use client"

// ✅ Server Component — 展示文章内容,不需要交互
// src/app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const res = await fetch(`https://api.example.com/posts/${slug}`);
const post = await res.json();

return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
</article>
);
}
// ✅ Client Component — 有按钮点击交互
// src/components/LikeButton.tsx
"use client";

import { useState } from "react";

export default function LikeButton() {
const [likes, setLikes] = useState(0);

return (
<button
onClick={() => setLikes(likes + 1)}
className="bg-pink-500 text-white px-4 py-2 rounded-lg hover:bg-pink-600"
>
❤️ 点赞 ({likes})
</button>
);
}

然后在 Server Component 里直接引入 Client Component:

// src/app/blog/[slug]/page.tsx
import LikeButton from "@/components/LikeButton";

export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
// ... 获取数据
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<LikeButton /> {/* 交互部分单独抽成 Client Component */}
</article>
);
}

API Routes(后端接口)

Next.js 的 API 路由让你在同一个项目里写后端逻辑,不需要单独搭后端服务器。

// src/app/api/hello/route.ts
import { NextResponse } from "next/server";

// GET 请求处理
export async function GET() {
return NextResponse.json({ message: "你好,世界!" });
}

// POST 请求处理
export async function POST(request: Request) {
const body = await request.json();
return NextResponse.json({
message: `收到你的数据:${body.name}`,
});
}

访问 http://localhost:3000/api/hello 就能拿到 { "message": "你好,世界!" }

你可以在前端直接调用:

const res = await fetch("/api/hello");
const data = await res.json();
// data.message === "你好,世界!"

数据获取(Server Components 中直接 fetch)

在 Server Components 里,你可以直接 await fetch(),非常直觉:

// src/app/blog/page.tsx
interface Post {
id: number;
title: string;
excerpt: string;
}

export default async function BlogPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts: Post[] = await res.json();

return (
<div>
<h1 className="text-3xl font-bold mb-6">博客文章</h1>
<div className="space-y-4">
{posts.map((post) => (
<a
key={post.id}
href={`/blog/${post.id}`}
className="block p-4 bg-white rounded-lg shadow hover:shadow-md transition-shadow"
>
<h2 className="text-xl font-semibold">{post.title}</h2>
<p className="text-gray-500 mt-1">{post.excerpt}</p>
</a>
))}
</div>
</div>
);
}

不需要 useEffect,不需要 useState,不需要 loading 状态——因为数据在服务器上就获取好了,浏览器收到的直接是渲染好的 HTML。

完整实战:一个博客系统

让我们把上面的知识串起来,做一个完整的博客。

// src/app/blog/[slug]/page.tsx — 动态博客文章页

interface Post {
id: number;
title: string;
body: string;
}

export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const res = await fetch(
`https://jsonplaceholder.typicode.com/posts/${slug}`
);
const post: Post = await res.json();

return (
<article className="max-w-2xl mx-auto py-8">
<h1 className="text-4xl font-bold mb-4">{post.title}</h1>
<div className="prose prose-lg">
<p>{post.body}</p>
</div>
<div className="mt-8 pt-4 border-t">
<a href="/blog" className="text-blue-500 hover:underline">
← 返回文章列表
</a>
</div>
</article>
);
}
// src/app/blog/[slug]/not-found.tsx — 文章不存在时显示

export default function NotFound() {
return (
<div className="text-center py-20">
<h1 className="text-6xl font-bold text-gray-300">404</h1>
<p className="text-xl text-gray-500 mt-4">这篇文章不存在</p>
<a href="/blog" className="text-blue-500 hover:underline mt-4 block">
回到博客首页
</a>
</div>
);
}

访问 /blog/1 就能看到文章内容,访问 /blog/99999 会显示 404。整个过程零 JavaScript 发送到浏览器(Server Component),SEO 友好,加载飞快。


Tailwind CSS:写样式的新方式

什么是 Tailwind?

传统写 CSS 是这样的:你先给按钮起个类名 class="submit-btn",然后在 CSS 文件里写:

.submit-btn {
background: #3b82f6;
color: white;
padding: 8px 20px;
border-radius: 8px;
font-weight: bold;
}

一个项目写下来,CSS 文件几百行,各种类名互相覆盖,改一个地方可能炸另一个地方。

Tailwind 换了一种思路——直接在 HTML 里用"工具类"

<button class="bg-blue-500 text-white px-5 py-2 rounded-lg font-bold">
提交
</button>

看到没?bg-blue-500 是蓝色背景,text-white 是白色文字,px-5 py-2 是内边距,rounded-lg 是圆角。每个类只做一件小事,组合起来就是完整的样式。

为什么大家都爱 Tailwind?

  1. 速度快 —— 不用在 HTML 和 CSS 文件之间来回跳了,样式一目了然。AI 写 Tailwind 也特别溜,因为它用标准化类名,不需要了解你的自定义 CSS 命名规范。
  2. 一致性好 —— 颜色、间距、字号都有固定比例,不会出现"这个按钮 12px 圆角,那个 15px"的混乱。
  3. 文件小 —— 自动删掉没用到的 CSS,最终打包通常只有几 KB。
  4. 响应式简单 —— 加个前缀就是不同屏幕尺寸的样式。

常用类名速查表

文字样式:

text-xs / text-sm / text-base / text-lg / text-xl / text-2xl / text-3xl → 字号
font-normal / font-medium / font-semibold / font-bold → 字重
text-gray-500 / text-blue-600 / text-red-500 → 颜色
text-center / text-left / text-right → 对齐

背景和颜色:

bg-white / bg-gray-100 / bg-blue-500 / bg-green-600 → 背景色
bg-gradient-to-r from-blue-500 to-purple-500 → 渐变背景
opacity-50 / opacity-75 → 透明度

Flexbox 布局:

flex → 启用 flex 布局
flex-row → 水平排列(默认)
flex-col → 垂直排列
justify-center → 水平居中
justify-between → 两端对齐
items-center → 垂直居中
gap-4 → 子元素间距

Grid 布局:

grid → 启用 grid
grid-cols-3 → 3 列等宽
grid-cols-[200px_1fr] → 自定义列宽
col-span-2 → 占 2 列

间距(最常用的):

p-4 / px-4 / py-4 / pt-4 / pr-4 / pb-4 / pl-4 → padding(内边距)
m-4 / mx-auto / mt-4 / mb-4 → margin(外边距)
gap-2 / gap-4 / gap-8 → 子元素间距
数字对应:1=4px, 2=8px, 3=12px, 4=16px, 6=24px, 8=32px

边框和圆角:

border / border-2 / border-gray-200 → 边框
rounded / rounded-lg / rounded-full → 圆角(从小到大)
shadow / shadow-lg / shadow-xl → 阴影

响应式(加前缀):

sm: → ≥640px(手机横屏)
md: → ≥768px(平板)
lg: → ≥1024px(笔记本)
xl: → ≥1280px(桌面)

用法:grid-cols-1 md:grid-cols-2 lg:grid-cols-3
意思是:手机1列,平板2列,桌面3列

实战:用 Tailwind 做一个卡片组件

function ProductCard() {
return (
<div className="bg-white rounded-2xl shadow-lg overflow-hidden max-w-sm">
{/* 图片 */}
<img
src="/product.jpg"
alt="产品图"
className="w-full h-48 object-cover"
/>

{/* 内容区 */}
<div className="p-6">
<span className="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded-full">
新品上市
</span>
<h3 className="text-xl font-bold text-gray-900 mt-3">
超好用的编程键盘
</h3>
<p className="text-gray-500 text-sm mt-2">
Cherry MX 红轴,打字手感丝滑,写代码效率翻倍。
</p>

{/* 价格和按钮 */}
<div className="flex items-center justify-between mt-4">
<span className="text-2xl font-bold text-red-500">¥299</span>
<button className="bg-blue-500 text-white px-5 py-2 rounded-lg hover:bg-blue-600 transition-colors">
加入购物车
</button>
</div>
</div>
</div>
HEREDOC_END