Appearance
第4章:开发环境搭建与工具链配置
4.1 开发环境概览
工具链架构图
环境要求清单
工具/框架 | 最低版本 | 推荐版本 | 说明 |
---|---|---|---|
Node.js | 16.14.0 | 18.17.0+ | 支持 ES2022 特性 |
React | 18.0.0 | 18.2.0+ | 必须支持 Concurrent Features |
TypeScript | 4.9.0 | 5.0.0+ | 更好的类型推断 |
Next.js | 13.0.0 | 14.0.0+ | App Router 支持 |
Webpack | 5.75.0 | 5.88.0+ | RSC 插件支持 |
4.2 Next.js 环境搭建
项目初始化
bash
# 1. 创建 Next.js 项目
npx create-next-app@latest my-rsc-app --typescript --tailwind --eslint --app
# 2. 进入项目目录
cd my-rsc-app
# 3. 安装额外依赖
npm install @types/node @types/react @types/react-dom
# 4. 开发依赖
npm install -D @typescript-eslint/eslint-plugin @typescript-eslint/parser
项目结构
my-rsc-app/
├── app/ # App Router 目录
│ ├── globals.css # 全局样式
│ ├── layout.tsx # 根布局 (Server Component)
│ ├── page.tsx # 首页 (Server Component)
│ ├── loading.tsx # 加载 UI
│ ├── error.tsx # 错误 UI
│ ├── not-found.tsx # 404 页面
│ └── api/ # API 路由
│ └── hello/
│ └── route.ts
├── components/ # 组件目录
│ ├── server/ # Server Components
│ ├── client/ # Client Components
│ └── shared/ # 共享组件
├── lib/ # 工具库
│ ├── db.ts # 数据库配置
│ ├── utils.ts # 工具函数
│ └── types.ts # 类型定义
├── public/ # 静态资源
├── next.config.js # Next.js 配置
├── tailwind.config.js # Tailwind 配置
├── tsconfig.json # TypeScript 配置
└── package.json
Next.js 配置文件
javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 启用实验性功能
experimental: {
// 服务器组件
serverComponents: true,
// 服务器操作
serverActions: true,
// 部分预渲染
ppr: true,
},
// 编译配置
compiler: {
// 移除 console.log(生产环境)
removeConsole: process.env.NODE_ENV === 'production',
},
// 图片优化
images: {
domains: ['example.com', 'cdn.example.com'],
formats: ['image/webp', 'image/avif'],
},
// 重定向配置
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
];
},
// 重写配置
async rewrites() {
return [
{
source: '/api/proxy/:path*',
destination: 'https://external-api.com/:path*',
},
];
},
// 环境变量
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY,
},
// Webpack 配置
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
// 自定义 Webpack 配置
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
fs: false,
net: false,
tls: false,
};
}
return config;
},
};
module.exports = nextConfig;
TypeScript 配置
json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "ES2022"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/components/*": ["./components/*"],
"@/lib/*": ["./lib/*"],
"@/app/*": ["./app/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts"
],
"exclude": ["node_modules"]
}
ESLint 配置
json
// .eslintrc.json
{
"extends": [
"next/core-web-vitals",
"@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
// React Server Components 相关规则
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
// TypeScript 规则
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/prefer-const": "error",
// 通用规则
"no-console": "warn",
"prefer-const": "error",
"no-var": "error"
},
"overrides": [
{
// Server Components 特殊规则
"files": ["app/**/*.tsx", "components/server/**/*.tsx"],
"rules": {
"react-hooks/rules-of-hooks": "off"
}
}
]
}
4.3 基础组件示例
根布局组件
typescript
// app/layout.tsx - Root Layout (Server Component)
import './globals.css';
import { Inter } from 'next/font/google';
import { Metadata } from 'next';
import { Analytics } from '@/components/analytics';
import { ThemeProvider } from '@/components/theme-provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: {
default: 'React Server Components Demo',
template: '%s | RSC Demo'
},
description: 'React Server Components 演示应用',
keywords: ['React', 'Server Components', 'Next.js'],
authors: [{ name: 'Your Name' }],
openGraph: {
title: 'React Server Components Demo',
description: 'React Server Components 演示应用',
url: 'https://your-domain.com',
siteName: 'RSC Demo',
images: [
{
url: 'https://your-domain.com/og-image.jpg',
width: 1200,
height: 630,
},
],
locale: 'zh_CN',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'React Server Components Demo',
description: 'React Server Components 演示应用',
images: ['https://your-domain.com/twitter-image.jpg'],
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
};
interface RootLayoutProps {
children: React.ReactNode;
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="zh-CN" suppressHydrationWarning>
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="min-h-screen bg-background font-sans antialiased">
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-14 items-center">
<nav className="flex items-center space-x-6 text-sm font-medium">
<a href="/" className="transition-colors hover:text-foreground/80">
首页
</a>
<a href="/posts" className="transition-colors hover:text-foreground/80">
文章
</a>
<a href="/about" className="transition-colors hover:text-foreground/80">
关于
</a>
</nav>
</div>
</header>
<main className="flex-1">
{children}
</main>
<footer className="border-t py-6 md:py-0">
<div className="container flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
<p className="text-center text-sm leading-loose text-muted-foreground md:text-left">
Built with React Server Components and Next.js
</p>
</div>
</footer>
</div>
</ThemeProvider>
<Analytics />
</body>
</html>
);
}
首页组件
typescript
// app/page.tsx - Home Page (Server Component)
import { Suspense } from 'react';
import { PostList } from '@/components/server/post-list';
import { UserStats } from '@/components/server/user-stats';
import { InteractiveDemo } from '@/components/client/interactive-demo';
import { LoadingSpinner } from '@/components/shared/loading-spinner';
import { getRecentPosts, getUserStats } from '@/lib/data';
// 这个组件在服务器端运行
export default async function HomePage() {
// 并行获取数据
const [recentPosts, stats] = await Promise.all([
getRecentPosts(6),
getUserStats()
]);
return (
<div className="container mx-auto px-4 py-8">
{/* Hero Section */}
<section className="text-center py-12">
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
React Server Components
</h1>
<p className="mt-6 text-lg leading-8 text-gray-600">
体验零 Bundle 影响的服务器端渲染
</p>
</section>
{/* Stats Section */}
<section className="py-8">
<Suspense fallback={<LoadingSpinner />}>
<UserStats stats={stats} />
</Suspense>
</section>
{/* Posts Section */}
<section className="py-8">
<h2 className="text-2xl font-bold mb-6">最新文章</h2>
<Suspense fallback={<PostListSkeleton />}>
<PostList posts={recentPosts} />
</Suspense>
</section>
{/* Interactive Demo */}
<section className="py-8">
<h2 className="text-2xl font-bold mb-6">交互演示</h2>
<InteractiveDemo />
</section>
</div>
);
}
// 骨架屏组件
function PostListSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="animate-pulse">
<div className="bg-gray-200 h-48 rounded-lg mb-4"></div>
<div className="bg-gray-200 h-4 rounded mb-2"></div>
<div className="bg-gray-200 h-4 rounded w-3/4"></div>
</div>
))}
</div>
);
}
Server Component 示例
typescript
// components/server/post-list.tsx
import { Post } from '@/lib/types';
import { PostCard } from './post-card';
interface PostListProps {
posts: Post[];
}
// Server Component - 在服务器端渲染
export async function PostList({ posts }: PostListProps) {
// 可以在这里进行额外的数据处理
const processedPosts = posts.map(post => ({
...post,
readingTime: calculateReadingTime(post.content)
}));
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{processedPosts.map(post => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
function calculateReadingTime(content: string): number {
const wordsPerMinute = 200;
const words = content.split(' ').length;
return Math.ceil(words / wordsPerMinute);
}
typescript
// components/server/post-card.tsx
import { Post } from '@/lib/types';
import { formatDate } from '@/lib/utils';
import Link from 'next/link';
interface PostCardProps {
post: Post & { readingTime: number };
}
export function PostCard({ post }: PostCardProps) {
return (
<article className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
{post.coverImage && (
<img
src={post.coverImage}
alt={post.title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h3 className="text-xl font-semibold mb-2">
<Link
href={`/posts/${post.slug}`}
className="hover:text-blue-600 transition-colors"
>
{post.title}
</Link>
</h3>
<p className="text-gray-600 mb-4 line-clamp-3">
{post.excerpt}
</p>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{formatDate(post.publishedAt)}</span>
<span>{post.readingTime} 分钟阅读</span>
</div>
<div className="flex flex-wrap gap-2 mt-4">
{post.tags.map(tag => (
<span
key={tag}
className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded"
>
{tag}
</span>
))}
</div>
</div>
</article>
);
}
Client Component 示例
typescript
// components/client/interactive-demo.tsx
'use client';
import { useState, useEffect } from 'react';
import { Button } from '@/components/shared/button';
export function InteractiveDemo() {
const [count, setCount] = useState(0);
const [mounted, setMounted] = useState(false);
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// 防止 hydration 不匹配
useEffect(() => {
setMounted(true);
}, []);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/demo-data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('获取数据失败:', error);
} finally {
setLoading(false);
}
};
const handleIncrement = () => {
setCount(prev => prev + 1);
// 保存到本地存储
if (typeof window !== 'undefined') {
localStorage.setItem('demo-count', (count + 1).toString());
}
};
// 从本地存储恢复状态
useEffect(() => {
if (typeof window !== 'undefined') {
const savedCount = localStorage.getItem('demo-count');
if (savedCount) {
setCount(parseInt(savedCount, 10));
}
}
}, []);
if (!mounted) {
return (
<div className="bg-gray-50 p-6 rounded-lg">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="h-10 bg-gray-200 rounded w-32"></div>
</div>
</div>
);
}
return (
<div className="bg-gray-50 p-6 rounded-lg">
<h3 className="text-lg font-semibold mb-4">客户端交互演示</h3>
<div className="space-y-4">
<div className="flex items-center space-x-4">
<span className="text-2xl font-bold">{count}</span>
<Button onClick={handleIncrement}>
增加计数
</Button>
</div>
<div className="flex items-center space-x-4">
<Button
onClick={fetchData}
disabled={loading}
variant="outline"
>
{loading ? '加载中...' : '获取数据'}
</Button>
</div>
{data.length > 0 && (
<div className="mt-4">
<h4 className="font-medium mb-2">获取的数据:</h4>
<ul className="space-y-1">
{data.slice(0, 5).map((item, index) => (
<li key={index} className="text-sm text-gray-600">
{JSON.stringify(item)}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
}
4.4 数据层配置
数据库配置
typescript
// lib/db.ts
import { Pool } from 'pg';
import { drizzle } from 'drizzle-orm/node-postgres';
import { migrate } from 'drizzle-orm/node-postgres/migrator';
// 数据库连接池
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
// Drizzle ORM 实例
export const db = drizzle(pool);
// 数据库迁移
export async function runMigrations() {
await migrate(db, { migrationsFolder: './drizzle' });
}
// 连接测试
export async function testConnection() {
try {
const client = await pool.connect();
const result = await client.query('SELECT NOW()');
client.release();
console.log('数据库连接成功:', result.rows[0]);
return true;
} catch (error) {
console.error('数据库连接失败:', error);
return false;
}
}
// 优雅关闭
process.on('SIGINT', async () => {
await pool.end();
process.exit(0);
});
数据模型定义
typescript
// lib/schema.ts
import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// 用户表
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
avatar: text('avatar'),
bio: text('bio'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// 文章表
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content').notNull(),
excerpt: text('excerpt'),
coverImage: text('cover_image'),
published: boolean('published').default(false),
authorId: integer('author_id').references(() => users.id),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
// 标签表
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: text('name').notNull().unique(),
slug: text('slug').notNull().unique(),
description: text('description'),
createdAt: timestamp('created_at').defaultNow(),
});
// 文章标签关联表
export const postTags = pgTable('post_tags', {
id: serial('id').primaryKey(),
postId: integer('post_id').references(() => posts.id),
tagId: integer('tag_id').references(() => tags.id),
});
// 关系定义
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
postTags: many(postTags),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
postTags: many(postTags),
}));
export const postTagsRelations = relations(postTags, ({ one }) => ({
post: one(posts, {
fields: [postTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postTags.tagId],
references: [tags.id],
}),
}));
数据访问层
typescript
// lib/data.ts
import { db } from './db';
import { users, posts, tags, postTags } from './schema';
import { eq, desc, and, like, sql } from 'drizzle-orm';
import { cache } from 'react';
// React 缓存装饰器
export const getUser = cache(async (id: number) => {
const user = await db.select().from(users).where(eq(users.id, id)).limit(1);
return user[0] || null;
});
export const getUserByEmail = cache(async (email: string) => {
const user = await db.select().from(users).where(eq(users.email, email)).limit(1);
return user[0] || null;
});
export const getRecentPosts = cache(async (limit: number = 10) => {
return await db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
excerpt: posts.excerpt,
coverImage: posts.coverImage,
publishedAt: posts.publishedAt,
author: {
id: users.id,
name: users.name,
avatar: users.avatar,
},
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.publishedAt))
.limit(limit);
});
export const getPostBySlug = cache(async (slug: string) => {
const result = await db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
content: posts.content,
excerpt: posts.excerpt,
coverImage: posts.coverImage,
publishedAt: posts.publishedAt,
author: {
id: users.id,
name: users.name,
avatar: users.avatar,
bio: users.bio,
},
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(and(eq(posts.slug, slug), eq(posts.published, true)))
.limit(1);
return result[0] || null;
});
export const getPostTags = cache(async (postId: number) => {
return await db
.select({
id: tags.id,
name: tags.name,
slug: tags.slug,
})
.from(tags)
.leftJoin(postTags, eq(tags.id, postTags.tagId))
.where(eq(postTags.postId, postId));
});
export const searchPosts = cache(async (query: string, limit: number = 10) => {
return await db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
excerpt: posts.excerpt,
publishedAt: posts.publishedAt,
})
.from(posts)
.where(
and(
eq(posts.published, true),
like(posts.title, `%${query}%`)
)
)
.orderBy(desc(posts.publishedAt))
.limit(limit);
});
export const getUserStats = cache(async () => {
const [totalUsers, totalPosts, totalTags] = await Promise.all([
db.select({ count: sql<number>`count(*)` }).from(users),
db.select({ count: sql<number>`count(*)` }).from(posts).where(eq(posts.published, true)),
db.select({ count: sql<number>`count(*)` }).from(tags),
]);
return {
users: totalUsers[0].count,
posts: totalPosts[0].count,
tags: totalTags[0].count,
};
});
4.5 开发工具配置
VS Code 配置
json
// .vscode/settings.json
{
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.suggest.autoImports": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true,
"source.organizeImports": true
},
"files.associations": {
"*.css": "tailwindcss"
},
"emmet.includeLanguages": {
"javascript": "javascriptreact",
"typescript": "typescriptreact"
},
"tailwindCSS.experimental.classRegex": [
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"],
["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
]
}
json
// .vscode/extensions.json
{
"recommendations": [
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"ms-vscode.vscode-typescript-next",
"formulahendry.auto-rename-tag",
"christian-kohler.path-intellisense",
"ms-vscode.vscode-json"
]
}
调试配置
json
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node",
"request": "attach",
"port": 9229,
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
},
{
"name": "Next.js: debug full stack",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"args": ["dev"],
"env": {
"NODE_OPTIONS": "--inspect"
},
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal"
}
]
}
脚本配置
json
// package.json scripts
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
"type-check": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check .",
"db:generate": "drizzle-kit generate:pg",
"db:migrate": "drizzle-kit up:pg",
"db:studio": "drizzle-kit studio",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"analyze": "ANALYZE=true next build",
"clean": "rm -rf .next out dist"
}
}
小结
本章详细介绍了 React Server Components 开发环境的搭建:
- 工具链配置:Node.js、TypeScript、ESLint 等基础工具的配置
- Next.js 设置:项目结构、配置文件和基础组件示例
- 数据层配置:数据库连接、模型定义和数据访问层
- 开发工具:VS Code 配置、调试设置和脚本配置
下一章我们将深入探讨传统 SSR 的实现原理,为理解 RSC 的优势做好准备。
