Next.js从入门到实战保姆级教程:综合实战项目(下)——前端页面、性能优化与部署

Next.js 独立开发教程(十):部分预渲染 在构建现代 Web 应用程序时,性能优化和用户体验提升是开发者关注的核心目标。部分预渲染(Partial Prerendering)是 Next.js 提供的一种强大功能,可以结合静态生成(Static Generation, SSG)和动态渲染(Server-Side Rendering, SSR),让开发者在灵活性和性能之间找到最佳平衡。它尤其适用于那些页面结构相对固定、但部分内容需要动态更新的场景。 阅读详情

本系列文章将围绕Next.js技术栈,旨在为AI Agent开发者提供一套完整的客户端侧工程实践指南。

这是全栈博客系统实战的下篇。在上篇《全栈博客系统架构与核心功能》中,我们完成了数据库设计、认证系统、Server Actions 等后端核心功能。本篇将聚焦前端页面开发、用户体验优化和生产部署,带你完成从代码到上线的完整流程。

一、📖 前置准备

在开始之前,请确保你已经:

  • ✅ 完成了上篇的所有内容
  • ✅ 数据库已初始化并运行
  • ✅ Auth.js 配置完成
  • ✅ Server Actions 可以正常调用

如果还没有,建议先回顾上篇内容《https://blog.youkuaiyun.com/FrontAI/article/details/160083752》。


二、🎨 Markdown 渲染与代码高亮

1. 为什么选择 MDX?

传统 Markdown 的局限性:

  • ❌ 无法使用 React 组件
  • ❌ 交互功能受限
  • ❌ 动态内容难以集成

MDX (Markdown + JSX) 的优势:

  • ✅ 在 Markdown 中嵌入 React 组件
  • ✅ 支持自定义渲染逻辑
  • ✅ 完美的 TypeScript 类型支持

例如,你可以在文章中这样写:

这是一段普通文本。

<Callout type="info">
  这是一个提示框组件!
</Callout>

```javascript
console.log('代码块自动高亮');
```

2. 安装依赖

npm install next-mdx-remote shiki rehype-autolink-headings rehype-slug

依赖说明:

  • next-mdx-remote: 在服务端安全地渲染 MDX
  • shiki: VS Code 同款语法高亮引擎(比 Prism.js 更准确)
  • rehype-autolink-headings: 自动为标题添加锚点链接
  • rehype-slug: 为标题生成 ID

3. 创建 MDX 渲染器组件

创建 components/MDXRenderer.tsx:

// components/MDXRenderer.tsx
import { MDXRemote } from 'next-mdx-remote/rsc';
import { serialize } from 'next-mdx-remote/serialize';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import rehypeSlug from 'rehype-slug';
import { CodeBlock } from './CodeBlock';
import { Callout } from './Callout';

interface MDXRendererProps {
  content: string;
}

/**
 * MDX 内容渲染器
 * 
 * 工作流程:
 * 1. serialize: 将 Markdown 字符串编译为 MDX AST
 * 2. MDXRemote: 在服务端渲染为 HTML
 * 3. components: 自定义组件映射表
 * 
 * @param content - Markdown 内容
 */
export async function MDXRenderer({ content }: MDXRendererProps) {
  // 序列化 MDX 内容
  const mdxSource = await serialize(content, {
    mdxOptions: {
      rehypePlugins: [
        rehypeSlug,  // 先生成 slug
        [rehypeAutolinkHeadings, { 
          behavior: 'wrap',  // 将整个标题包装为链接
          properties: {
            className: ['anchor-link'],
          },
        }],
      ],
    },
  });

  return (
    <article className="prose prose-lg max-w-none dark:prose-invert prose-headings:relative">
      <MDXRemote
        {...mdxSource}
        components={{
          // 自定义代码块渲染
          pre: CodeBlock,
          // 自定义提示框
          Callout,
          // 可以添加更多自定义组件
          img: CustomImage,
          a: CustomLink,
        }}
      />
    </article>
  );
}

/**
 * 自定义图片组件(懒加载)
 */
function CustomImage(props: React.ImgHTMLAttributes<HTMLImageElement>) {
  return (
    <img 
      {...props} 
      loading="lazy"  // 懒加载
      className="rounded-lg shadow-md"
    />
  );
}

/**
 * 自定义链接组件(外部链接新窗口打开)
 */
function CustomLink(props: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
  const href = props.href;
  const isExternal = href?.startsWith('http');

  return (
    <a
      {...props}
      {...(isExternal && {
        target: '_blank',
        rel: 'noopener noreferrer',
      })}
      className="text-blue-600 hover:underline"
    />
  );
}

🔍 代码解析:

(1) 为什么使用 next-mdx-remote/rsc?
import { MDXRemote } from 'next-mdx-remote/rsc';  // RSC 版本
  • RSC 版本: 在服务端渲染,性能更好
  • 客户端版本: next-mdx-remote/client,用于交互式 MDX
(2) Rehype 插件的作用
rehypePlugins: [
  rehypeSlug,  // 为 h1-h6 添加 id 属性
  [rehypeAutolinkHeadings, { behavior: 'wrap' }],  // 将标题变为可点击链接
]

执行顺序很重要:

  1. rehypeSlug 先执行,生成 id="introduction"
  2. rehypeAutolinkHeadings 后执行,包裹为 <a href="#introduction"><h2>...</h2></a>
(3) Components 映射表
components={{
  pre: CodeBlock,  // 替换所有 <pre> 标签
  Callout,         // 支持自定义 <Callout> 组件
}}

当 MDX 中出现 <pre> 时,会自动使用 CodeBlock 组件渲染。

4. 代码高亮组件

创建 components/CodeBlock.tsx:

// components/CodeBlock.tsx
import { codeToHtml } from 'shiki';

interface CodeBlockProps {
  children: React.ReactNode;
  className?: string;
}

/**
 * 代码块组件(带语法高亮)
 * 
 * Shiki 优势:
 * - 使用 TextMate grammar,与 VS Code 一致
 * - 支持主题切换
 * - 输出静态 HTML,无运行时 JS
 */
export async function CodeBlock({ children, className }: CodeBlockProps) {
  // 提取语言信息(如 language-jsx)
  const match = /language-(\w+)/.exec(className || '');
  const lang = match ? match[1] : 'text';
  
  // 获取代码内容
  const code = String(children).replace(/\n$/, '');

  // 使用 Shiki 生成高亮 HTML
  const html = await codeToHtml(code, {
    lang,
    theme: 'github-dark',  // 可切换主题
  });

  return (
    <div className="relative my-6 rounded-lg overflow-hidden">
      {/* 语言标签 */}
      <div className="absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 text-gray-300 rounded">
        {lang}
      </div>
      
      {/* 高亮代码 */}
      <div 
        dangerouslySetInnerHTML={{ __html: html }}
        className="overflow-x-auto"
      />
    </div>
  );
}

⚡ 性能优化:

Shiki 是异步的,所以组件必须是 async:

export async function CodeBlock({ ... }) {
  const html = await codeToHtml(code, { ... });
  // ...
}

Next.js 会在服务端等待异步操作完成,然后缓存结果。

5. 提示框组件

创建 components/Callout.tsx:

// components/Callout.tsx
interface CalloutProps {
  type?: 'info' | 'warning' | 'error' | 'success';
  children: React.ReactNode;
}

const icons = {
  info: '💡',
  warning: '⚠️',
  error: '❌',
  success: '✅',
};

const styles = {
  info: 'bg-blue-50 border-blue-200 text-blue-800',
  warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
  error: 'bg-red-50 border-red-200 text-red-800',
  success: 'bg-green-50 border-green-200 text-green-800',
};

/**
 * 提示框组件
 * 
 * 使用示例:
 * <Callout type="warning">
 *   这是一个警告提示
 * </Callout>
 */
export function Callout({ type = 'info', children }: CalloutProps) {
  return (
    <div className={`p-4 my-4 border-l-4 rounded ${styles[type]}`}>
      <div className="flex items-start gap-3">
        <span className="text-xl">{icons[type]}</span>
        <div className="flex-1">{children}</div>
      </div>
    </div>
  );
}

三、🏠 首页文章列表

1. 页面结构

创建 app/page.tsx:

// app/page.tsx
import { getPosts } from '@/lib/posts';
import Link from 'next/link';
import Image from 'next/image';
import { formatDate } from '@/lib/utils';

// ==================== 元数据 ====================

export const metadata = {
  title: '全栈博客 - 分享技术与思考',
  description: '专注于 Next.js、React、TypeScript 等现代 Web 开发技术',
};

// ==================== 缓存策略 ====================

/**
 * 每小时重新验证一次
 * 
 * 为什么不是静态生成?
 * - 文章可能频繁更新
 * - 需要显示最新评论数、点赞数
 * - revalidate 平衡了性能和时效性
 */
export const revalidate = 3600;

// ==================== 页面组件 ====================

export default async function HomePage() {
  // 获取第一页的 10 篇文章
  const { posts, pagination } = await getPosts({ 
    page: 1, 
    pageSize: 10 
  });

  return (
    <div className="container mx-auto px-4 py-8">
      {/* Hero 区域 */}
      <section className="mb-12 text-center">
        <h1 className="text-5xl font-bold mb-4 bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
          全栈博客
        </h1>
        <p className="text-xl text-gray-600">
          分享 Next.js、React、TypeScript 等现代 Web 开发技术
        </p>
      </section>

      {/* 文章列表 */}
      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {posts.map(post => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>

      {/* 分页 */}
      {pagination.totalPages > 1 && (
        <Pagination 
          currentPage={pagination.page}
          totalPages={pagination.totalPages}
        />
      )}

      {/* 空状态 */}
      {posts.length === 0 && (
        <EmptyState />
      )}
    </div>
  );
}

/**
 * 文章卡片组件
 */
function PostCard({ post }: { post: any }) {
  return (
    <article className="group border rounded-lg overflow-hidden hover:shadow-lg transition-all duration-300">
      {/* 封面图 */}
      {post.coverImage && (
        <Link href={`/blog/${post.slug}`}>
          <Image
            src={post.coverImage}
            alt={post.title}
            width={400}
            height={200}
            className="w-full h-48 object-cover group-hover:scale-105 transition-transform"
          />
        </Link>
      )}
      
      <div className="p-4">
        {/* 标题 */}
        <h2 className="text-xl font-semibold mb-2 line-clamp-2">
          <Link 
            href={`/blog/${post.slug}`}
            className="hover:text-blue-600 transition-colors"
          >
            {post.title}
          </Link>
        </h2>
        
        {/* 摘要 */}
        <p className="text-gray-600 text-sm mb-4 line-clamp-2">
          {post.excerpt}
        </p>
        
        {/* 元信息 */}
        <div className="flex items-center justify-between text-sm text-gray-500">
          <div className="flex items-center gap-2">
            {post.author.image && (
              <Image
                src={post.author.image}
                alt={post.author.name || ''}
                width={24}
                height={24}
                className="rounded-full"
              />
            )}
            <span>{post.author.name}</span>
          </div>
          
          <div className="flex gap-3">
            <span title="浏览量">👁 {post.viewCount}</span>
            <span title="评论数">💬 {post._count.comments}</span>
            <span title="点赞数">❤️ {post._count.likes}</span>
          </div>
        </div>
        
        {/* 日期和阅读时间 */}
        <div className="mt-3 flex items-center gap-3 text-xs text-gray-400">
          <time dateTime={post.publishedAt?.toISOString()}>
            {formatDate(post.publishedAt || post.createdAt)}
          </time>
          {post.readingTime && (
            <>
              <span>•</span>
              <span>{post.readingTime} 分钟阅读</span>
            </>
          )}
        </div>
        
        {/* 标签 */}
        {post.tags.length > 0 && (
          <div className="flex flex-wrap gap-2 mt-3">
            {post.tags.slice(0, 3).map(({ tag }) => (
              <Link
                key={tag.id}
                href={`/tags/${tag.slug}`}
                className="px-2 py-1 text-xs rounded-full hover:opacity-80 transition-opacity"
                style={{ 
                  backgroundColor: `${tag.color}20`, 
                  color: tag.color 
                }}
              >
                {tag.name}
              </Link>
            ))}
          </div>
        )}
      </div>
    </article>
  );
}

/**
 * 分页组件
 */
function Pagination({ 
  currentPage, 
  totalPages 
}: { 
  currentPage: number;
  totalPages: number;
}) {
  return (
    <nav className="flex justify-center gap-2 mt-8">
      {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
        <Link
          key={page}
          href={`/?page=${page}`}
          className={`px-4 py-2 rounded ${
            page === currentPage
              ? 'bg-blue-600 text-white'
              : 'bg-gray-100 hover:bg-gray-200'
          }`}
        >
          {page}
        </Link>
      ))}
    </nav>
  );
}

/**
 * 空状态组件
 */
function EmptyState() {
  return (
    <div className="text-center py-12">
      <div className="text-6xl mb-4">📝</div>
      <h3 className="text-xl font-semibold mb-2">暂无文章</h3>
      <p className="text-gray-600">
        博主正在努力创作中,敬请期待...
      </p>
    </div>
  );
}

📖 设计要点解析:

(1) 渐进增强原则
<Link href={`/blog/${post.slug}`}>
  <Image src={post.coverImage} alt={post.title} />
</Link>

即使 JavaScript 未加载,用户仍可点击链接跳转,保证基本可用性。

(2) 图片优化
<Image
  src={post.coverImage}
  width={400}
  height={200}
  className="group-hover:scale-105 transition-transform"
/>

next/image 自动:

  • ✅ 生成多种尺寸的图片
  • ✅ 转换为现代格式(WebP/AVIF)
  • ✅ 懒加载(非首屏图片)
  • ✅ 防止布局偏移(CLS)
(3) 文本截断
className="line-clamp-2"  // 最多显示 2 行

Tailwind CSS 的实用类,优雅地处理长文本。


四、📄 文章详情页

1. 动态路由页面

创建 app/blog/[slug]/page.tsx:

// app/blog/[slug]/page.tsx
import { getPostBySlug } from '@/lib/posts';
import { notFound } from 'next/navigation';
import { MDXRenderer } from '@/components/MDXRenderer';
import { CommentSection } from '@/components/CommentSection';
import { LikeButton } from '@/components/LikeButton';
import { BookmarkButton } from '@/components/BookmarkButton';
import { auth } from '@/auth';
import Image from 'next/image';

interface BlogPostPageProps {
  params: Promise<{ slug: string }>;
}

// ==================== 元数据生成 ====================

export async function generateMetadata({ params }: BlogPostPageProps) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  
  if (!post) {
    return {};
  }

  return {
    title: post.title,
    description: post.excerpt || post.content.substring(0, 160),
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: post.coverImage ? [{ url: post.coverImage }] : [],
      type: 'article',
      publishedTime: post.publishedAt?.toISOString(),
      authors: [post.author.name].filter(Boolean),
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: post.coverImage ? [post.coverImage] : [],
    },
  };
}

// ==================== 静态参数生成(可选优化) ====================

/**
 * 预生成热门文章的静态页面
 * 
 * 适用场景:
 * - 访问量高的文章
 * - 不经常更新的内容
 * 
 * 注意:如果文章很多,不要全部预生成,会导致构建缓慢
 */
export async function generateStaticParams() {
  // 只预生成最近 10 篇文章
  const { posts } = await getPosts({ 
    page: 1, 
    pageSize: 10,
    published: true 
  });

  return posts.map(post => ({
    slug: post.slug,
  }));
}

// ==================== 页面组件 ====================

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const { slug } = await params;
  
  // 并行获取文章和当前用户
  const [post, session] = await Promise.all([
    getPostBySlug(slug),
    auth(),
  ]);

  if (!post) {
    notFound();
  }

  return (
    <article className="container mx-auto px-4 py-8 max-w-4xl">
      {/* 文章头部 */}
      <PostHeader post={post} />

      {/* 互动按钮 */}
      <InteractionBar 
        postId={post.id}
        initialLiked={false}
        initialBookmarked={false}
        user={session?.user || null}
      />

      {/* 文章内容 */}
      <MDXRenderer content={post.content} />

      {/* 标签 */}
      <PostTags tags={post.tags} />

      {/* 作者信息 */}
      <AuthorCard author={post.author} />

      {/* 评论区 */}
      <CommentSection 
        postId={post.id} 
        comments={post.comments}
        currentUser={session?.user || null}
      />
    </article>
  );
}

/**
 * 文章头部组件
 */
function PostHeader({ post }: { post: any }) {
  return (
    <header className="mb-8 pb-8 border-b">
      {/* 标题 */}
      <h1 className="text-4xl md:text-5xl font-bold mb-6">
        {post.title}
      </h1>
      
      {/* 作者和日期 */}
      <div className="flex flex-wrap items-center gap-4 text-gray-600">
        {post.author.image && (
          <Image
            src={post.author.image}
            alt={post.author.name || ''}
            width={40}
            height={40}
            className="rounded-full"
          />
        )}
        <span className="font-medium">{post.author.name}</span>
        <span>•</span>
        <time dateTime={post.publishedAt?.toISOString()}>
          {new Date(post.publishedAt || post.createdAt).toLocaleDateString('zh-CN', {
            year: 'numeric',
            month: 'long',
            day: 'numeric',
          })}
        </time>
        <span>•</span>
        <span>{post.readingTime} 分钟阅读</span>
        <span>•</span>
        <span>👁 {post.viewCount} 次阅读</span>
      </div>

      {/* 封面图 */}
      {post.coverImage && (
        <div className="mt-6 rounded-lg overflow-hidden">
          <Image
            src={post.coverImage}
            alt={post.title}
            width={1200}
            height={600}
            priority  // 首屏图片,优先加载
            className="w-full h-auto"
          />
        </div>
      )}
    </header>
  );
}

/**
 * 互动按钮栏
 */
function InteractionBar({ 
  postId, 
  initialLiked, 
  initialBookmarked,
  user 
}: { 
  postId: string;
  initialLiked: boolean;
  initialBookmarked: boolean;
  user: any;
}) {
  return (
    <div className="flex gap-4 mb-8 pb-8 border-b">
      <LikeButton 
        postId={postId} 
        initialLiked={initialLiked}
        isAuthenticated={!!user}
      />
      <BookmarkButton 
        postId={postId} 
        initialBookmarked={initialBookmarked}
        isAuthenticated={!!user}
      />
    </div>
  );
}

/**
 * 标签组件
 */
function PostTags({ tags }: { tags: any[] }) {
  if (tags.length === 0) return null;

  return (
    <div className="flex flex-wrap gap-2 my-8">
      {tags.map(({ tag }) => (
        <Link
          key={tag.id}
          href={`/tags/${tag.slug}`}
          className="px-3 py-1 text-sm rounded-full transition-opacity hover:opacity-80"
          style={{ 
            backgroundColor: `${tag.color}20`, 
            color: tag.color 
          }}
        >
          #{tag.name}
        </Link>
      ))}
    </div>
  );
}

/**
 * 作者卡片
 */
function AuthorCard({ author }: { author: any }) {
  return (
    <div className="my-12 p-6 bg-gray-50 rounded-lg">
      <div className="flex items-center gap-4">
        {author.image && (
          <Image
            src={author.image}
            alt={author.name || ''}
            width={60}
            height={60}
            className="rounded-full"
          />
        )}
        <div>
          <h3 className="font-semibold text-lg">{author.name}</h3>
          {author.bio && (
            <p className="text-gray-600 text-sm mt-1">{author.bio}</p>
          )}
        </div>
      </div>
    </div>
  );
}

🎯 关键知识点:

(1)Metadata API
export async function generateMetadata({ params }) {
  return {
    title: post.title,
    openGraph: { /* Facebook/Twitter 预览 */ },
    twitter: { /* Twitter Card */ },
  };
}

SEO 最佳实践:

  • ✅ title: 控制在 60 字符以内
  • ✅ description: 150-160 字符,包含关键词
  • ✅ openGraph.images: 至少 1200x630 像素
  • ✅ twitter.card: 使用 summary_large_image 获得大卡片
(2) generateStaticParams
export async function generateStaticParams() {
  const { posts } = await getPosts({ page: 1, pageSize: 10 });
  return posts.map(post => ({ slug: post.slug }));
}

何时使用?

  • ✅ 访问量高的页面(首页、热门文章)
  • ✅ 内容不频繁变化
  • ❌ 文章数量巨大(会导致构建缓慢)

效果:

  • 这些页面在构建时生成静态 HTML
  • 访问时无需服务端渲染,速度极快
(3)并行数据获取
const [post, session] = await Promise.all([
  getPostBySlug(slug),
  auth(),
]);

而不是串行:

// ❌ 慢
const post = await getPostBySlug(slug);
const session = await auth();

五、💬 评论组件实现

1. 评论列表

创建 components/CommentSection.tsx:

// components/CommentSection.tsx
'use client';

import { useState } from 'react';
import { createComment } from '@/app/actions/comment';
import Image from 'next/image';
import { formatDate } from '@/lib/utils';

interface CommentSectionProps {
  postId: string;
  comments: any[];
  currentUser: any;
}

/**
 * 评论区组件
 * 
 * 功能:
 * - 显示评论列表(支持嵌套)
 * - 发表评论
 * - 回复评论
 * - Optimistic UI(乐观更新)
 */
export function CommentSection({ 
  postId, 
  comments,
  currentUser 
}: CommentSectionProps) {
  const [commentList, setCommentList] = useState(comments);
  const [replyingTo, setReplyingTo] = useState<string | null>(null);
  const [content, setContent] = useState('');
  const [loading, setLoading] = useState(false);

  /**
   * 提交评论
   */
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    if (!content.trim()) return;
    if (!currentUser) {
      alert('请先登录');
      return;
    }

    setLoading(true);

    try {
      const result = await createComment({
        postId,
        content,
        parentId: replyingTo || undefined,
      });

      if (result.success && result.comment) {
        // Optimistic Update: 立即更新 UI
        if (replyingTo) {
          // 添加到回复列表
          setCommentList(prev =>
            prev.map(comment =>
              comment.id === replyingTo
                ? {
                    ...comment,
                    replies: [...(comment.replies || []), result.comment],
                  }
                : comment
            )
          );
        } else {
          // 添加到顶级评论
          setCommentList(prev => [...prev, result.comment]);
        }

        // 清空表单
        setContent('');
        setReplyingTo(null);
      } else {
        alert(result.error || '评论失败');
      }
    } catch (error) {
      console.error(error);
      alert('评论失败,请稍后重试');
    } finally {
      setLoading(false);
    }
  };

  return (
    <section className="mt-12 pt-8 border-t">
      <h2 className="text-2xl font-bold mb-6">
        评论 ({commentList.length})
      </h2>

      {/* 评论表单 */}
      <CommentForm
        content={content}
        onChange={setContent}
        onSubmit={handleSubmit}
        loading={loading}
        placeholder={
          replyingTo ? '撰写回复...' : '写下你的评论...'
        }
        onCancel={() => setReplyingTo(null)}
        isReply={!!replyingTo}
      />

      {/* 评论列表 */}
      <div className="space-y-6 mt-8">
        {commentList.map(comment => (
          <CommentItem
            key={comment.id}
            comment={comment}
            currentUser={currentUser}
            onReply={(commentId) => setReplyingTo(commentId)}
            replyingTo={replyingTo}
          />
        ))}

        {commentList.length === 0 && (
          <p className="text-center text-gray-500 py-8">
            暂无评论,来发表第一条评论吧!
          </p>
        )}
      </div>
    </section>
  );
}

/**
 * 评论表单组件
 */
function CommentForm({
  content,
  onChange,
  onSubmit,
  loading,
  placeholder,
  onCancel,
  isReply,
}: any) {
  return (
    <form onSubmit={onSubmit} className="mb-8">
      <textarea
        value={content}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        rows={4}
        className="w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
        required
      />
      
      <div className="flex justify-end gap-2 mt-3">
        {isReply && (
          <button
            type="button"
            onClick={onCancel}
            className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
          >
            取消
          </button>
        )}
        <button
          type="submit"
          disabled={loading || !content.trim()}
          className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {loading ? '提交中...' : '发表评论'}
        </button>
      </div>
    </form>
  );
}

/**
 * 单条评论组件
 */
function CommentItem({ 
  comment, 
  currentUser, 
  onReply,
  replyingTo 
}: any) {
  const isReplying = replyingTo === comment.id;

  return (
    <div className="flex gap-4">
      {/* 头像 */}
      {comment.author.image && (
        <Image
          src={comment.author.image}
          alt={comment.author.name || ''}
          width={40}
          height={40}
          className="rounded-full flex-shrink-0"
        />
      )}

      <div className="flex-1">
        {/* 评论头部 */}
        <div className="flex items-center gap-2 mb-2">
          <span className="font-medium">{comment.author.name}</span>
          <time 
            className="text-sm text-gray-500"
            dateTime={comment.createdAt}
          >
            {formatDate(comment.createdAt)}
          </time>
        </div>

        {/* 评论内容 */}
        <p className="text-gray-700 mb-3 whitespace-pre-wrap">
          {comment.content}
        </p>

        {/* 回复按钮 */}
        {currentUser && !isReplying && (
          <button
            onClick={() => onReply(comment.id)}
            className="text-sm text-blue-600 hover:underline"
          >
            回复
          </button>
        )}

        {/* 回复表单 */}
        {isReplying && (
          <div className="mt-4 ml-8">
            <CommentForm
              content=""
              onChange={() => {}}
              onSubmit={async (e: any) => {
                e.preventDefault();
                // 实际应由父组件处理
              }}
              loading={false}
              placeholder="撰写回复..."
              onCancel={() => onReply(null)}
              isReply={true}
            />
          </div>
        )}

        {/* 回复列表 */}
        {comment.replies?.length > 0 && (
          <div className="mt-4 space-y-4 ml-8">
            {comment.replies.map((reply: any) => (
              <CommentItem
                key={reply.id}
                comment={reply}
                currentUser={currentUser}
                onReply={onReply}
                replyingTo={replyingTo}
              />
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

💡 Optimistic UI 原理:

提交评论的时候采用了乐观更新的方式:

// 1. 立即更新 UI(假设成功)
setCommentList(prev => [...prev, newComment]);

// 2. 发送请求
const result = await createComment(data);

// 3. 如果失败,回滚
if (!result.success) {
  setCommentList(prev => prev.filter(c => c.id !== newComment.id));
}

优势:

  • ✅ 用户体验极佳,无需等待服务器响应
  • ✅ 减少感知延迟

风险:

  • ⚠️ 需要处理失败情况
  • ⚠️ 不适合关键操作(如支付)

后续的点赞收藏功能也采用乐观更新。


六、❤️ 点赞与收藏按钮

1. 点赞按钮

创建 components/LikeButton.tsx:

// components/LikeButton.tsx
'use client';

import { useState } from 'react';
import { toggleLike } from '@/app/actions/interaction';

interface LikeButtonProps {
  postId: string;
  initialLiked: boolean;
  isAuthenticated: boolean;
}

/**
 * 点赞按钮(Optimistic UI)
 * 
 * 交互流程:
 * 1. 用户点击
 * 2. 立即切换 UI 状态
 * 3. 后台发送请求
 * 4. 如果失败,回滚状态
 */
export function LikeButton({ 
  postId, 
  initialLiked,
  isAuthenticated 
}: LikeButtonProps) {
  const [liked, setLiked] = useState(initialLiked);
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    if (!isAuthenticated) {
      alert('请先登录');
      return;
    }

    // Optimistic Update
    const previousState = liked;
    setLiked(!previousState);
    setLoading(true);

    try {
      const result = await toggleLike(postId);

      if (!result.success) {
        // 回滚
        setLiked(previousState);
        alert(result.error);
      }
    } catch (error) {
      // 回滚
      setLiked(previousState);
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handleClick}
      disabled={loading}
      className={`flex items-center gap-2 px-4 py-2 rounded-full transition-all ${
        liked
          ? 'bg-red-50 text-red-600 hover:bg-red-100'
          : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
      }`}
    >
      <span className={`text-xl ${liked ? 'animate-pulse' : ''}`}>
        {liked ? '❤️' : '🤍'}
      </span>
      <span>{liked ? '已点赞' : '点赞'}</span>
    </button>
  );
}

2. 收藏按钮

创建 components/BookmarkButton.tsx:

// components/BookmarkButton.tsx
'use client';

import { useState } from 'react';
import { toggleBookmark } from '@/app/actions/interaction';

interface BookmarkButtonProps {
  postId: string;
  initialBookmarked: boolean;
  isAuthenticated: boolean;
}

export function BookmarkButton({ 
  postId, 
  initialBookmarked,
  isAuthenticated 
}: BookmarkButtonProps) {
  const [bookmarked, setBookmarked] = useState(initialBookmarked);
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    if (!isAuthenticated) {
      alert('请先登录');
      return;
    }

    const previousState = bookmarked;
    setBookmarked(!previousState);
    setLoading(true);

    try {
      const result = await toggleBookmark(postId);

      if (!result.success) {
        setBookmarked(previousState);
        alert(result.error);
      }
    } catch (error) {
      setBookmarked(previousState);
      console.error(error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <button
      onClick={handleClick}
      disabled={loading}
      className={`flex items-center gap-2 px-4 py-2 rounded-full transition-all ${
        bookmarked
          ? 'bg-yellow-50 text-yellow-600 hover:bg-yellow-100'
          : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
      }`}
    >
      <span className="text-xl">
        {bookmarked ? '⭐' : '☆'}
      </span>
      <span>{bookmarked ? '已收藏' : '收藏'}</span>
    </button>
  );
}

七、🔐 登录页面

1. 自定义登录页

创建 app/auth/signin/page.tsx:

// app/auth/signin/page.tsx
import { signIn } from '@/auth';
import { Github } from 'lucide-react';

export const metadata = {
  title: '登录 - 全栈博客',
  description: '使用 GitHub 账号登录',
};

export default function SignInPage() {
  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
        <h1 className="text-3xl font-bold text-center mb-8">欢迎回来</h1>
        
        <form
          action={async () => {
            'use server';
            await signIn('github', { 
              redirectTo: '/' 
            });
          }}
          className="space-y-4"
        >
          <button
            type="submit"
            className="w-full flex items-center justify-center gap-3 px-6 py-3 bg-gray-900 text-white rounded-lg hover:bg-gray-800 transition-colors"
          >
            <Github className="w-5 h-5" />
            使用 GitHub 登录
          </button>
        </form>

        <p className="mt-6 text-center text-sm text-gray-600">
          登录后即可评论、点赞、收藏文章
        </p>
      </div>
    </div>
  );
}

🔑 Server Actions 表单:

<form action={async () => {
  'use server';
  await signIn('github', { redirectTo: '/' });
}}>
  <button type="submit">登录</button>
</form>

这种写法:

  • ✅ 无需 JavaScript 也可工作
  • ✅ 自动处理 CSRF Token
  • ✅ 简洁优雅

八、⚡ 性能优化深度实践

1. 图片懒加载与优先级

// 首屏图片:优先加载
<Image
  src={heroImage}
  priority  // 关键!
  alt="Hero"
/>

// 非首屏图片:懒加载(默认行为)
<Image
  src={thumbnail}
  alt="Thumbnail"
  loading="lazy"  // 可省略,默认就是 lazy
/>

2. 字体优化

创建 app/layout.tsx:

// app/layout.tsx
import { Inter } from 'next/font/google';

// Next.js 自动优化字体
const inter = Inter({ 
  subsets: ['latin'],
  display: 'swap',  // 避免 FOIT(Flash of Invisible Text)
});

export default function RootLayout({ children }) {
  return (
    <html lang="zh-CN">
      <body className={inter.className}>
        {children}
      </body>
    </html>
  );
}

优势:

  • ✅ 自动托管字体文件(CDN)
  • ✅ 消除布局偏移
  • ✅ 预加载关键字体

3. 代码分割

Next.js App Router 自动进行代码分割:

  • 每个路由独立 bundle
  • 客户端组件按需加载
  • 第三方库 Tree Shaking

无需手动配置!

4. 流式渲染(Streaming)

对于慢查询,可以使用 Suspense:

// app/blog/[slug]/page.tsx
import { Suspense } from 'react';

export default function BlogPostPage({ params }) {
  return (
    <article>
      {/* 快速加载的部分 */}
      <PostHeader />
      
      {/* 慢查询部分:流式加载 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </article>
  );
}

async function Comments() {
  // 模拟慢查询
  await new Promise(resolve => setTimeout(resolve, 2000));
  return <div>评论内容...</div>;
}

function CommentsSkeleton() {
  return <div className="animate-pulse">加载中...</div>;
}

效果:

  • 用户先看到文章头部
  • 评论逐步加载,无需等待

九、🚀 部署上线

1. Vercel 部署(推荐)

步骤 1:推送代码到 GitHub
git init
git add .
git commit -m "feat: 完成博客系统"
git remote add origin https://github.com/yourusername/fullstack-blog.git
git push -u origin main
步骤 2:连接 Vercel
  1. 访问 vercel.com
  2. 点击 “New Project”
  3. 导入 GitHub 仓库
  4. 配置环境变量
步骤 3:配置环境变量

在 Vercel Dashboard → Settings → Environment Variables 中添加:

DATABASE_URL=postgresql://...
AUTH_SECRET=your-secret-key
GITHUB_ID=your-github-id
GITHUB_SECRET=your-github-secret
OPENAI_API_KEY=sk-your-key
NEXT_PUBLIC_APP_URL=https://your-domain.vercel.app
步骤 4:自动部署

每次推送到 main 分支,Vercel 会自动:

  1. 安装依赖
  2. 执行 next build
  3. 部署到全球 CDN
  4. 提供预览 URL

2. 数据库托管(Neon)

Neon 提供免费 Serverless PostgreSQL:

  1. 注册 neon.tech
  2. 创建新项目
  3. 获取连接字符串
  4. 更新 DATABASE_URL

优势:

  • ✅ 免费 tier: 0.5 GB 存储
  • ✅ 自动扩缩容
  • ✅ 分支功能(类似 Git)

3. 自定义域名

在 Vercel Dashboard → Settings → Domains 中:

  1. 添加你的域名
  2. 按提示配置 DNS(CNAME/A Record)
  3. 等待 SSL 证书签发(自动)

十一、📊 监控与分析

1. Vercel Analytics

在 app/layout.tsx 中添加:

import { Analytics } from '@vercel/analytics/react';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
      </body>
    </html>
  );
}

安装依赖:

npm install @vercel/analytics

功能:

  • 页面浏览量
  • 用户地理位置
  • 设备类型
  • 性能指标

2. Core Web Vitals 监控

Vercel 自动收集:

  • LCP(Largest Contentful Paint): 最大内容绘制时间
  • FID(First Input Delay): 首次输入延迟
  • CLS(Cumulative Layout Shift): 累积布局偏移

目标值:

  • LCP < 2.5s
  • FID < 100ms
  • CLS < 0.1

十二、📝 本章小结

通过上下两篇的学习,你已完成了一个生产级全栈博客系统:

✅ 已完成功能

模块功能技术栈
用户系统GitHub OAuth 登录Auth.js
文章管理CRUD、Markdown 渲染Prisma、MDX
AI 增强自动摘要、标签推荐OpenAI API
社交互动评论、点赞、收藏Server Actions
性能优化缓存、懒加载、流式渲染Next.js 内置
部署运维Vercel 自动化部署CI/CD

🎯 核心知识点回顾

  1. App Router 架构: 文件系统路由、嵌套布局、并行路由
  2. React Server Components: 服务端渲染、减少客户端 JS
  3. Server Actions: 类型安全的表单处理
  4. 数据缓存策略: revalidateTag、generateStaticParams
  5. 性能优化: next/image、字体优化、代码分割
  6. SEO 最佳实践: Metadata API、Open Graph、Sitemap

🚀 下一步扩展方向

  1. 全文搜索: 集成 Meilisearch 或 Algolia
  2. RSS 订阅: 生成 RSS/Atom Feed
  3. 邮件通知: 新评论提醒(Resend/SendGrid)
  4. 管理后台: 文章审核、数据统计、用户管理
  5. 暗黑模式: next-themes 实现主题切换
  6. 国际化: next-intl 多语言支持
  7. PWA: 离线访问、推送通知

💪 练习作业

  1. 实现"相关文章推荐"功能(基于标签相似度)
  2. 添加"阅读进度条"(客户端组件)
  3. 实现"代码复制"按钮(CodeBlock 组件)
  4. 添加 Google Analytics 集成
  5. 实现简单的站内搜索(使用 Prisma 全文搜索)

🎉 结语

恭喜你完成了这个完整的 Next.js 全栈项目!

从环境配置到生产部署,你已掌握了:

  • ✅ 现代 Web 开发的最佳实践
  • ✅ 全栈应用的架构设计思路
  • ✅ 性能优化与 SEO 技巧
  • ✅ 自动化部署与监控

记住: 学习编程最好的方式就是不断实践。在此基础上,尝试添加新功能、优化现有代码、重构架构。

祝你成为一名优秀的 Next.js 全栈开发者! 🚀


资源链接:

掌握React与Next.js:构建与部署实践指南 是一个专为Next.js设计的官方脚手架工具,它简化了项目的创建、配置和开发的初始化工作。使用可以快速搭建起一个基于Next.js的开发环境,同时它也提供了一个预设的项目结构和配置文件,帮助开发者避开繁琐的配置环节,能够将精力更多地集中在业务逻辑和页面构建上。是一个在构建时(构建过程中的静态生成)获取数据的函数。它仅在运行构建时执行一次,并且在生产环境中不可用于服务器端渲染。允许我们返回props,这些props将被用于页面组件的渲染。 阅读详情

相关推荐

UCOS-III 系统移植

UCOSIII系统移植到GD32F303详细过程 工程代码:https://github.com/jslaobing/techIot_ucosiii_porting

TechIoT - 探索物联网与嵌入式系统 2087

AI时代前端破局:Next.js全栈开发从入门到部署实战

Next.js全栈开发实战指南:AI时代前端转型全栈的完整路径,涵盖Node.js、Prisma、数据库、NextAuth认证等核心技术,通过博客项目实战

weixin_67775627的博客 807

【论文笔记】LIME: low-light image enhancement via illumination map estimation

【论文笔记】LIME: low-light image enhancement via illumination map estimation 一篇低光图像增强,引用超1000次的经典论文

weixin_45565519的博客 1794

30分钟学会使用 Next.js:从入门到实战的全栈开发指南

Next.js 是一个基于 React 的全栈框架,支持服务端渲染(SSR)、静态站点生成(SSG)和 API 路由,适用于构建从静态网站到动态复杂应用的多种场景。本文在30分钟内系统介绍了 Next.js 的核心功能,包括项目搭建、路由与数据获取、页面渲染模式、API 路由构建、性能优化、部署实战及常见问题解决。通过实战案例、流程图和对比表格,帮助开发者快速掌握 Next.js 的理论与实践技能,提升开发效率和项目质量。适合有一定 JavaScript 基础、希望高效构建现代 Web 应用的开发者。

领码SPARK - 以无代码之星火,燎原数字之未来! 2032

《Linux运维总结:etcd 3.5.15集群数据备份与恢复》

《Linux运维总结:etcd 3.5.15集群数据备份与恢复》

东城绝神 1080

各种加法器的比对分析与Verilog实现(3)

了Kogge-Stone加法器和brent-kung加法器,本文将用Verilog代码进行实现。

Albert_yeager的博客 1198

Next.js 入门学习

在现代 Web 开发领域,Next.js 已成为构建高性能、可扩展且用户体验卓越的 React 应用程序的重要框架。它基于 React 并提供了一系列强大的特性和工具,能够帮助开发者更高效地构建服务器端渲染(SSR)、静态站点生成(SSG)以及客户端渲染(CSR)应用,无论是开发复杂的企业级应用还是简单的个人博客,Next.js 都展现出了其独特的优势。本文将带领读者从 Next.js 的基础知识开始,逐步深入到高级应用场景,全面掌握 Next.js 的开发技能。

candy的博客 2689

PGM-Explainer实验总结

PGM-Explainer建立了一个概率图模型,为GNN提供实例级解释。 论文标题:PGM-Explainer: Probabilistic Graphical Model Explanations for Graph Neural Networks 论文地址:2010.05788.pdf (arxiv.org) PGM-Explainer实验 实验目的 研究PGM-Explainer的有效性 实验方案 合成数据集的节点分类 与其他解释器做对比实验 使用六个合成数据集。 每个输入图都是一个

笔记|思路|总结 1万+

Next.js Ecommerce部署教程:轻松将项目上线到生产环境

想要将你的Next.js电子商务网站快速部署到生产环境吗?这篇完整指南将带你一步步完成Next.js Ecommerce项目的部署过程,从环境配置到最终上线,让你轻松掌握专业的部署技巧!🚀 ## 为什么选择Next.js构建电子商务网站? Next.js Ecommerce是一个基于React的现代化电子商务解决方案,它结合了Next.js的强大功能和美观的UI设计。这个项目使用了TypeS

gitblog_01150的博客 959

uniapp - 超详细 H5 网页引入百度地图教程,附地图示例 / 获取当前设备定位的省市区、详细地址、经纬度信息 / 用户当前 IP 属地定位地理位置等等(新手小白快速上手,详细配置及使用全流程)

uniapp百度地图,uniapp百度地图ip属地定位,uniapp百度地图获取省市区详细地址经纬度等,uniapp百度地图当前用户设备定位省份市区详细所在地ip属地等详细教程示例,uniapp网页h5引入百度地图详细教程,uniapp h5网站使用百度地图详细教程,uniappH5端如何接入使用百度地图api,uniapp H5网页端百度地图获取地理位置信息,uniapp h5页面引入百度地图,uniapp h5平台百度地图获取城市信息,uniapp百度地图获取经纬度,uniapp 百度地图拿省市区用什么

🏆 前端领域优质创作者 6217

App爬虫实战篇-以华为真机手机爬取集换社的app为例

在开始学习这篇文章之前,建议你先按照之前2篇文章(和),配置必要的环境,才可以继续完成本章节内容。

youbingchen的博客 780

如何用Next.js和Shopify Hydrogen React构建高性能电商前端:Next.js Shopify Storefront完整教程

想要快速构建一个现代化的高性能电商网站吗?Next.js Shopify Storefront是一个基于Next.js和Shopify Hydrogen React的完整解决方案,让你能够在短时间内搭建出专业级的电商前端。这个开源项目集成了最新的前端技术栈,为开发者提供了开箱即用的电商功能实现。 ## 🚀 为什么选择Next.js Shopify Storefront? 在当今竞争激烈的电商

gitblog_00158的博客 1110

微信公众号 - 最新详细实现H5网页接入微信支付功能,开发移动端、PC端网站集成使用微信支付流程及示例代码,公众号网页利用JS-SDK/JSAPI/H5支付授权唤起微信支付,可脱离微信环境支付或内置浏

微信公众号,微信支付,网页,移动端,h5,扫码支付,二维码付款,手机网页,电脑pc网页生成支付二维码,js-sdk,jsapi,外部浏览器调起微信支付,脱离微信环境,在微信内置浏览器如何进行微信支付,前端H5微信支付,web网站,vue2,vue3,nuxt2,nuxt3,jquery,微信公众号h5支付流程,如何实现,怎么做,回调报错,redirect_uri参数错误,必须部署才能调试吗,怎么不打包发布就能在本机测试,微信H5支付能力,uniapp微信支付,h5页面调起支付,支付没反应,无效,失败,H5页

🏆 前端领域优质创作者 6103

Next.js从入门到精通的万字长文教程

Next.js作为React生态中的全栈框架,提供了开箱即用的服务端渲染、静态站点生成和API路由功能。本文从入门到精通,系统讲解了Next.js的核心特性和实战应用:1. 基础篇:介绍项目创建、文件路由和Server/Client组件混合渲染模式 2. 核心概念:深入解析数据获取策略(SSG/SSR/ISR)、样式方案和API路由实现 3. 实战演练:构建包含CRUD功能的博客系统,实现动态路由和布局共享 4. 优化部署:涵盖中间件、图像优化、元数据管理和性能调优技巧 通过分层渐进的教学,帮助开发者掌握N

油墨香^-^的博客 1649

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Next.js从入门到实战保姆级教程:实战项目(上)——全栈博客系统架构与核心功能

从需求分析到数据库设计,逐步构建生产级博客系统的后端架构。涵盖 Prisma 数据建模、认证系统集成、Server Actions 实践等核心知识点,为 AI 时代的全栈开发打下坚实基础。

FrontAI的博客 842

visio安装包

Microsoft Office Visio 是一款专业的制图软件。利用经过更新的形状、协作工具和数据链接图表,创建专业级图表以简化复杂的信息。

Flatdraw部署指南:Next.js静态导出与Cloudflare Workers配置终极教程

Flatdraw是一个基于TypeScript、React和Next.js构建的简单画布绘图Web应用,提供响应式用户界面和丰富的绘图功能。本文将为您提供完整的Flatdraw部署指南,涵盖Next.js静态导出和Cloudflare Workers配置的详细步骤,帮助您快速部署这个强大的绘图工具。🚀 ## 为什么选择Flatdraw静态部署方案? Flatdraw采用Next.js的静态导

gitblog_00827的博客 959

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

AssetRipper Unity资产提取上手指南:三步拿到可打开的Unity工程

AssetRipper 是一个跨平台的 Unity 资产提取 GUI 工具:读取游戏的序列化文件和资源包,把资产转成原生 Unity 引擎格式,并恢复脚本内容。适合做 Mod 的开发、旧项目资产恢复人员,以及需要分析游戏资产结构的学习者。 ## 📥 谁会用这个工具 - **Mod 开发者**:从目标游戏里提取模型、贴图、音频,保存为 Unity 可直接导入的格式后二次开发。 - **资产恢复

gitblog_00200的博客 431

Next.js从入门到实战保姆级教程:性能优化深度实践

深入理解 Next.js 的性能优化体系,从 Core Web Vitals 到代码分割、懒加载、缓存策略,系统掌握打造高性能 Next.js 应用的方法论。

FrontAI的博客 659

OpenCascade.js与前端框架集成:React、Vue和Next.js实现方案

OpenCascade.js是一个革命性的WebAssembly CAD库,它将强大的OpenCascade三维建模引擎带到了浏览器端。对于前端开发者来说,掌握OpenCascade.js与主流前端框架的集成方案,可以让你轻松构建基于Web的3D建模应用。本文将为你详细介绍如何在React、Vue和Next.js项目中集成OpenCascade.js,并提供实用的实现方案。🎯 ## 📦 Op

gitblog_01099的博客 1113

Next.js Starter Kit 使用教程

Next.js Starter Kit 使用教程 项目介绍 Next.js Starter Kit 是一个基于 Next.js 的开源项目,旨在为开发者提供一个快速启动的模板,包含了许多现代 Web 应用所需的功能和工具。该项目集成了多种服务和库,如 Supabase、Clerk、Upstash 等,使得开发者可以快速构建和部署 SAAS 产品。 项目快速启动 安装依赖 首先,克隆项目仓库到本地:...

gitblog_01099的博客 981

Next.js全栈开发:从入门到精通

回顾这段简史,你会发现 Web 渲染的演进逻辑并不是一种技术取代另一种技术,而是在时间轴(构建时 vs 请求时)

YunWisdom 2442

Next.js Runtime 使用教程

Next.js Runtime 使用教程 项目介绍 Next.js Runtime 是一个允许 Next.js 在 Netlify 上运行的零配置运行时。它使得开发者能够在 Netlify 平台上无缝部署和运行 Next.js 应用,无需额外配置。该项目支持 Next.js 10-13 版本和 Node.js 18 及以上版本。 项目快速启动 以下是快速启动 Next.js Runtime 的步骤...

gitblog_01003的博客 969

最完整的Next.js入门指南:从零开始构建你的第一个React应用

Next.js作为当下最热门的React框架,以其卓越的性能优化和开发体验,成为构建现代Web应用的首选工具。本指南将带你从零开始,快速掌握Next.js的核心概念与实战技巧,轻松打造属于你的第一个React应用。 ## 🚀 为什么选择Next.js?三大核心优势解析 Next.js之所以能在众多React框架中脱颖而出,源于其独特的技术架构和开发者友好的设计理念: 1. **混合渲染模式

gitblog_00392的博客 1077

Next.js配置文件next.config.js深度解析与实战指南

Next.js作为现代React框架,其配置文件next.config.js是连接框架能力与业务需求的重要桥梁。本文将系统梳理配置文件的结构体系,通过多个实战场景带你掌握从基础配置到高级优化的全流程。 ## 配置文件基础架构 Next.js采用约定式配置设计,默认情况下项目可以无需配置文件运行,但实际开发中几乎都会通过自定义配置解锁高级功能。标准的配置文件位于项目根目录,采用CommonJS模

gitblog_00570的博客 1185

Tailwind Next.js Starter Blog部署教程:Vercel平台一键发布

你还在为技术博客的部署流程繁琐而头疼吗?从环境配置到服务器维护,每一步都可能成为技术写作的绊脚石。本文将带你使用Vercel平台一键部署Tailwind Next.js Starter Blog,全程无需复杂命令,即使是非专业开发者也能在5分钟内完成从代码到上线的全流程。读完本文,你将获得: - 零服务器配置的部署方案 - 自动HTTPS与全球CDN加速 - 代码提交即自动更新的CI/CD流程 -...

gitblog_00154的博客 1141

发现Next.js 13的魔力:实战入门教程项目

发现Next.js 13的魔力:实战入门教程项目 去发现同类优质开源项目:https://gitcode.com/ 随着Web开发的迭代加速,Next.js作为现代前端框架中的佼佼者,其第13版本更是带来了前所未有的创新与提升。今天,我们特别推荐一个源自Next.js 13 Crash Course的教学实践项目,它不仅展示了前沿的技术应用,更是开发者快速上手Next.js 13的理想起点。 项目...

gitblog_00025的博客 372
上一篇: Next.js从入门到实战保姆级教程:实战项目(上)——全栈博客系统架构与核心功能
下一篇: 深入浅出 LangChain — 导读
FrontAI
博客等级 码龄1年 61粉丝 61原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值