现代全栈开发:Next.js与Node.js实战指南

前言

随着Web开发的不断演进,前后端分离已成为主流架构模式。然而,全栈JavaScript开发凭借其一致的开发体验和高效的代码复用,正在获得越来越多开发者的青睐。本文将带你深入了解如何结合Next.js与Node.js打造现代全栈应用,从架构设计到部署上线,全面覆盖开发流程。

目录

  1. 全栈JavaScript开发的优势
  2. Next.js:超越传统React应用
  3. 搭建健壮的Node.js后端服务
  4. 前后端数据交互最佳实践
  5. 性能优化策略
  6. 实战案例:全栈博客系统开发
  7. 部署与运维技巧

1. 全栈JavaScript开发的优势

在当今技术生态中,选择JavaScript作为全栈开发语言具有显著优势:

  • 语言统一:前后端使用同一种语言,减少上下文切换成本
  • 代码共享:类型定义、验证逻辑、工具函数可在前后端复用
  • 生态系统:npm提供了超过150万个包,几乎能满足所有开发需求
  • 开发效率:统一的调试工具链和开发环境
kotlin 复制代码
javascript复制代码
// 前后端共享的数据验证逻辑示例
const validateUser = (user) => {
  if (!user.email || !user.email.includes('@')) {
    return { valid: false, error: '邮箱格式不正确' };
  }
  if (!user.password || user.password.length < 8) {
    return { valid: false, error: '密码长度不足8位' };
  }
  return { valid: true };
};

// 可在前端表单验证和后端API验证中复用
export { validateUser };

2. Next.js:超越传统React应用

Next.js已发展成为React应用开发的首选框架,尤其是其v13版本引入的App Router和React Server Components:

核心优势

  • 多种渲染模式:SSR、SSG、ISR、CSR按需选择
  • 路由系统:基于文件系统的直观路由
  • 零配置:内置TypeScript、CSS模块等支持
  • 优化:自动图像优化、字体优化
  • API路由:无需单独后端即可创建API

实战代码示例

javascript 复制代码
jsx复制代码
// app/blog/[slug]/page.js - React Server Component
export async function generateStaticParams() {
  const posts = await fetchPosts();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }) {
  // 这段代码在服务器上执行,不会增加客户端JS体积
  const post = await fetchPostBySlug(params.slug);
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

3. 搭建健壮的Node.js后端服务

构建可靠的Node.js服务需要注意以下方面:

Express.js与架构设计

php 复制代码
javascript复制代码
// 分层架构示例
const express = require('express');
const app = express();

// 中间件层
app.use(express.json());
app.use(cors());
app.use(helmet()); // 安全增强
app.use(morgan('combined')); // 日志

// 路由层
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);

// 控制器层 (userController.js)
exports.getUser = async (req, res) => {
  try {
    const user = await UserService.findById(req.params.id);
    res.json(user);
  } catch (error) {
    res.status(500).json({ message: error.message });
  }
};

// 服务层 (userService.js)
class UserService {
  static async findById(id) {
    // 数据访问逻辑
  }
}

API设计最佳实践

  • RESTful设计原则
  • 错误处理标准化
  • 参数验证
  • 身份认证与授权

4. 前后端数据交互最佳实践

现代全栈应用的数据交互主要采用以下方式:

React Query/SWR数据获取

javascript 复制代码
jsx复制代码
// 前端数据获取示例
import { useQuery, useMutation } from 'react-query';

// 获取数据
function UserProfile({ userId }) {
  const { data, isLoading, error } = useQuery(['user', userId], 
    () => fetch(`/api/users/${userId}`).then(res => res.json())
  );
  
  if (isLoading) return <p>加载中...</p>;
  if (error) return <p>出错了: {error.message}</p>;
  
  return <div>{data.name}</div>;
}

// 修改数据
function UpdateProfile() {
  const mutation = useMutation(
    newData => fetch('/api/profile', {
      method: 'PUT',
      body: JSON.stringify(newData)
    }).then(res => res.json())
  );
  
  return (
    <button onClick={() => mutation.mutate({ name: 'New Name' })}>
      更新
    </button>
  );
}

5. 性能优化策略

前端优化

  • 组件懒加载
  • 图片优化
  • 字体优化
  • 代码分割

后端优化

  • 数据库查询优化
  • 缓存策略
  • 水平扩展
  • 负载均衡

6. 实战案例:全栈博客系统开发

实战案例:全栈博客系统开发

接下来,让我们通过一个完整的博客系统实例,展示前后端结合开发的流程和关键代码。我们将使用Next.js 13+(App Router)构建前端,Node.js/Express搭建后端API,Prisma作为ORM,PostgreSQL作为数据库。

1. 数据模型设计

使用Prisma定义我们的数据模型,创建prisma/schema.prisma

kotlin 复制代码
复制代码
// 数据库连接
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Prisma客户端生成器
generator client {
  provider = "prisma-client-js"
}

// 用户模型
model User {
  id            String    @id @default(cuid())
  name          String
  email         String    @unique
  password      String
  bio           String?
  image         String?
  posts         Post[]
  comments      Comment[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

// 文章模型
model Post {
  id            String    @id @default(cuid())
  title         String
  slug          String    @unique
  content       String
  excerpt       String?
  published     Boolean   @default(false)
  featuredImage String?
  author        User      @relation(fields: [authorId], references: [id])
  authorId      String
  comments      Comment[]
  tags          Tag[]
  createdAt     DateTime  @default(now())
  updatedAt     DateTime  @updatedAt
}

// 评论模型
model Comment {
  id            String   @id @default(cuid())
  content       String
  author        User     @relation(fields: [authorId], references: [id])
  authorId      String
  post          Post     @relation(fields: [postId], references: [id])
  postId        String
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

// 标签模型
model Tag {
  id            String   @id @default(cuid())
  name          String   @unique
  posts         Post[]
  createdAt     DateTime @default(now())
  updatedAt     DateTime @updatedAt
}

2. 后端服务实现

创建Node.js/Express API服务:

php 复制代码
javascript复制代码
// server/index.js
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const cors = require('cors');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const app = express();
const prisma = new PrismaClient();

app.use(cors());
app.use(express.json());

// 中间件:验证JWT token
const authenticateToken = (req, res, next) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];
  
  if (!token) return res.status(401).json({ error: '未授权' });
  
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) return res.status(403).json({ error: '令牌无效' });
    req.user = user;
    next();
  });
};

// 用户注册
app.post('/api/auth/register', async (req, res) => {
  try {
    const { name, email, password } = req.body;
    
    // 检查用户是否已存在
    const existingUser = await prisma.user.findUnique({ where: { email } });
    if (existingUser) {
      return res.status(400).json({ error: '该邮箱已被注册' });
    }
    
    // 密码加密
    const hashedPassword = await bcrypt.hash(password, 10);
    
    // 创建用户
    const user = await prisma.user.create({
      data: {
        name,
        email,
        password: hashedPassword
      }
    });
    
    // 返回用户信息(不包含密码)
    res.status(201).json({
      id: user.id,
      name: user.name,
      email: user.email
    });
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

// 用户登录
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // 查找用户
    const user = await prisma.user.findUnique({ where: { email } });
    if (!user) {
      return res.status(401).json({ error: '邮箱或密码不正确' });
    }
    
    // 验证密码
    const validPassword = await bcrypt.compare(password, user.password);
    if (!validPassword) {
      return res.status(401).json({ error: '邮箱或密码不正确' });
    }
    
    // 生成JWT
    const token = jwt.sign(
      { id: user.id, email: user.email },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );
    
    res.json({ token });
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

// 获取所有文章
app.get('/api/posts', async (req, res) => {
  try {
    const posts = await prisma.post.findMany({
      where: { published: true },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            image: true
          }
        },
        tags: true
      },
      orderBy: {
        createdAt: 'desc'
      }
    });
    
    res.json(posts);
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

// 根据slug获取文章详情
app.get('/api/posts/:slug', async (req, res) => {
  try {
    const { slug } = req.params;
    
    const post = await prisma.post.findUnique({
      where: { slug },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            image: true,
            bio: true
          }
        },
        comments: {
          include: {
            author: {
              select: {
                id: true,
                name: true,
                image: true
              }
            }
          },
          orderBy: {
            createdAt: 'desc'
          }
        },
        tags: true
      }
    });
    
    if (!post) {
      return res.status(404).json({ error: '文章不存在' });
    }
    
    res.json(post);
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

// 创建新文章(需要认证)
app.post('/api/posts', authenticateToken, async (req, res) => {
  try {
    const { title, content, excerpt, tags, published, featuredImage } = req.body;
    const authorId = req.user.id;
    
    // 生成slug
    const slug = title
      .toLowerCase()
      .replace(/[^\w\s]/gi, '')
      .replace(/\s+/g, '-');
      
    // 创建文章及关联标签
    const post = await prisma.post.create({
      data: {
        title,
        slug: `${slug}-${Date.now()}`, // 确保唯一性
        content,
        excerpt,
        published: published || false,
        featuredImage,
        author: {
          connect: { id: authorId }
        },
        tags: {
          connectOrCreate: tags.map(tag => ({
            where: { name: tag },
            create: { name: tag }
          }))
        }
      },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            image: true
          }
        },
        tags: true
      }
    });
    
    res.status(201).json(post);
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

// 添加评论(需要认证)
app.post('/api/posts/:postId/comments', authenticateToken, async (req, res) => {
  try {
    const { postId } = req.params;
    const { content } = req.body;
    const authorId = req.user.id;
    
    const comment = await prisma.comment.create({
      data: {
        content,
        author: {
          connect: { id: authorId }
        },
        post: {
          connect: { id: postId }
        }
      },
      include: {
        author: {
          select: {
            id: true,
            name: true,
            image: true
          }
        }
      }
    });
    
    res.status(201).json(comment);
  } catch (error) {
    res.status(500).json({ error: '服务器错误', details: error.message });
  }
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
  console.log(`API服务运行在端口 ${PORT}`);
});

3. 前端实现

3.1 共享类型(TypeScript)

创建types/index.ts

css 复制代码
typescript复制代码
export interface User {
  id: string;
  name: string;
  email: string;
  bio?: string;
  image?: string;
}

export interface Post {
  id: string;
  title: string;
  slug: string;
  content: string;
  excerpt?: string;
  published: boolean;
  featuredImage?: string;
  author: User;
  comments?: Comment[];
  tags: Tag[];
  createdAt: string;
  updatedAt: string;
}

export interface Comment {
  id: string;
  content: string;
  author: User;
  createdAt: string;
  updatedAt: string;
}

export interface Tag {
  id: string;
  name: string;
}

3.2 API服务(Next.js封装)

创建lib/api.ts

typescript 复制代码
typescript复制代码
import { User, Post, Comment } from '@/types';

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000/api';

// 获取本地存储的token
const getToken = () => {
  if (typeof window !== 'undefined') {
    return localStorage.getItem('token');
  }
  return null;
};

// 请求工具函数
async function fetcher<T>(url: string, options?: RequestInit): Promise<T> {
  const token = getToken();
  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    ...(token ? { Authorization: `Bearer ${token}` } : {}),
    ...(options?.headers || {})
  };

  const response = await fetch(`${API_URL}${url}`, {
    ...options,
    headers
  });

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.error || 'API请求失败');
  }

  return data as T;
}

// 用户API
export const authAPI = {
  register: (userData: { name: string; email: string; password: string }) => 
    fetcher<User>('/auth/register', {
      method: 'POST',
      body: JSON.stringify(userData)
    }),
    
  login: (credentials: { email: string; password: string }) => 
    fetcher<{ token: string }>('/auth/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    })
};

// 文章API
export const postAPI = {
  getAll: () => fetcher<Post[]>('/posts'),
  
  getBySlug: (slug: string) => fetcher<Post>(`/posts/${slug}`),
  
  create: (postData: Omit<Post, 'id' | 'author' | 'createdAt' | 'updatedAt'>) => 
    fetcher<Post>('/posts', {
      method: 'POST',
      body: JSON.stringify(postData)
    }),
    
  addComment: (postId: string, content: string) => 
    fetcher<Comment>(`/posts/${postId}/comments`, {
      method: 'POST',
      body: JSON.stringify({ content })
    })
};

3.3 主要UI组件

博客首页组件 - app/page.tsx
javascript 复制代码
tsx复制代码
import { Suspense } from 'react';
import Link from 'next/link';
import { postAPI } from '@/lib/api';
import PostCard from '@/components/PostCard';
import LoadingPosts from '@/components/LoadingPosts';

async function getPosts() {
  // Next.js的Server Component可以直接返回Promise
  return postAPI.getAll();
}

export default async function HomePage() {
  return (
    <main className="container mx-auto px-4 py-8">
      <h1 className="text-4xl font-bold mb-8 text-center">博客首页</h1>
      
      <Suspense fallback={<LoadingPosts />}>
        <PostGrid />
      </Suspense>
    </main>
  );
}

// 文章列表Grid组件
async function PostGrid() {
  const posts = await getPosts();
  
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}
文章卡片组件 - components/PostCard.tsx
javascript 复制代码
tsx复制代码
'use client';

import { Post } from '@/types';
import Image from 'next/image';
import Link from 'next/link';
import { formatDate } from '@/lib/utils';

interface PostCardProps {
  post: Post;
}

export default function PostCard({ post }: PostCardProps) {
  return (
    <div className="bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow duration-300">
      {post.featuredImage && (
        <div className="relative h-48">
          <Image 
            src={post.featuredImage}
            alt={post.title}
            fill
            className="object-cover"
          />
        </div>
      )}
      
      <div className="p-4">
        <div className="flex gap-2 mb-2">
          {post.tags.map(tag => (
            <span 
              key={tag.id} 
              className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded-full"
            >
              {tag.name}
            </span>
          ))}
        </div>
        
        <h2 className="text-xl font-semibold mb-2">
          <Link href={`/blog/${post.slug}`} className="hover:text-blue-600">
            {post.title}
          </Link>
        </h2>
        
        <p className="text-gray-600 text-sm mb-4">{post.excerpt}</p>
        
        <div className="flex items-center justify-between">
          <div className="flex items-center">
            {post.author.image ? (
              <Image 
                src={post.author.image}
                alt={post.author.name}
                width={24}
                height={24}
                className="rounded-full mr-2"
              />
            ) : (
              <div className="w-6 h-6 bg-gray-300 rounded-full mr-2"></div>
            )}
            <span className="text-sm">{post.author.name}</span>
          </div>
          
          <span className="text-xs text-gray-500">
            {formatDate(post.createdAt)}
          </span>
        </div>
      </div>
    </div>
  );
}
文章详情页 - app/blog/[slug]/page.tsx
javascript 复制代码
tsx复制代码
import { Suspense } from 'react';
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { postAPI } from '@/lib/api';
import { formatDate } from '@/lib/utils';
import CommentSection from '@/components/CommentSection';
import MarkdownRenderer from '@/components/MarkdownRenderer';

interface BlogPostParams {
  params: {
    slug: string;
  };
}

// 用于静态生成的路径
export async function generateStaticParams() {
  const posts = await postAPI.getAll();
  return posts.map((post) => ({
    slug: post.slug,
  }));
}

async function getPost(slug: string) {
  try {
    return await postAPI.getBySlug(slug);
  } catch (error) {
    notFound();
  }
}

export default async function BlogPost({ params }: BlogPostParams) {
  const post = await getPost(params.slug);
  
  return (
    <article className="container mx-auto px-4 py-8 max-w-4xl">
      <header className="mb-8">
        <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
        
        <div className="flex flex-wrap gap-2 mb-4">
          {post.tags.map(tag => (
            <span 
              key={tag.id} 
              className="bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm"
            >
              {tag.name}
            </span>
          ))}
        </div>
        
        <div className="flex items-center mb-6">
          {post.author.image && (
            <Image
              src={post.author.image}
              alt={post.author.name}
              width={48}
              height={48}
              className="rounded-full mr-4"
            />
          )}
          
          <div>
            <p className="font-medium">{post.author.name}</p>
            <p className="text-gray-500 text-sm">
              {formatDate(post.createdAt)}
            </p>
          </div>
        </div>
        
        {post.featuredImage && (
          <div className="relative h-80 md:h-96 mb-8">
            <Image
              src={post.featuredImage}
              alt={post.title}
              fill
              className="object-cover rounded-lg"
              priority
            />
          </div>
        )}
      </header>
      
      <div className="prose prose-lg max-w-none mb-12">
        <MarkdownRenderer content={post.content} />
      </div>
      
      <Suspense fallback={<p>加载评论...</p>}>
        <CommentSection postId={post.id} comments={post.comments || []} />
      </Suspense>
    </article>
  );
}
文章创建表单 - app/dashboard/new-post/page.tsx
ini 复制代码
tsx复制代码
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useMutation } from 'react-query';
import { postAPI } from '@/lib/api';

export default function NewPostPage() {
  const router = useRouter();
  const [formData, setFormData] = useState({
    title: '',
    content: '',
    excerpt: '',
    tags: '',
    featuredImage: '',
    published: false
  });
  
  const createPostMutation = useMutation(postAPI.create, {
    onSuccess: (post) => {
      router.push(`/blog/${post.slug}`);
    }
  });
  
  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
    const { name, value, type } = e.target;
    const newValue = type === 'checkbox' 
      ? (e.target as HTMLInputElement).checked 
      : value;
      
    setFormData(prev => ({ ...prev, [name]: newValue }));
  };
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    // 将标签字符串转换为数组
    const tagsArray = formData.tags
      .split(',')
      .map(tag => tag.trim())
      .filter(Boolean);
      
    createPostMutation.mutate({
      ...formData,
      tags: tagsArray
    });
  };
  
  return (
    <div className="container mx-auto px-4 py-8 max-w-4xl">
      <h1 className="text-3xl font-bold mb-6">创建新文章</h1>
      
      <form onSubmit={handleSubmit} className="space-y-6">
        <div>
          <label htmlFor="title" className="block text-sm font-medium mb-1">
            标题
          </label>
          <input
            type="text"
            id="title"
            name="title"
            value={formData.title}
            onChange={handleChange}
            className="w-full px-4 py-2 border rounded-md"
            required
          />
        </div>
        
        <div>
          <label htmlFor="excerpt" className="block text-sm font-medium mb-1">
            摘要
          </label>
          <input
            type="text"
            id="excerpt"
            name="excerpt"
            value={formData.excerpt}
            onChange={handleChange}
            className="w-full px-4 py-2 border rounded-md"
          />
        </div>
        
        <div>
          <label htmlFor="featuredImage" className="block text-sm font-medium mb-1">
            特色图片URL
          </label>
          <input
            type="url"
            id="featuredImage"
            name="featuredImage"
            value={formData.featuredImage}
            onChange={handleChange}
            className="w-full px-4 py-2 border rounded-md"
            placeholder="https://example.com/image.jpg"
          />
        </div>
        
        <div>
          <label htmlFor="tags" className="block text-sm font-medium mb-1">
            标签 (用逗号分隔)
          </label>
          <input
            type="text"
            id="tags"
            name="tags"
            value={formData.tags}
            onChange={handleChange}
            className="w-full px-4 py-2 border rounded-md"
            placeholder="JavaScript, React, Web开发"
          />
        </div>
        
        <div>
          <label htmlFor="content" className="block text-sm font-medium mb-1">
            内容 (支持Markdown)
          </label>
          <textarea
            id="content"
            name="content"
            value={formData.content}
            onChange={handleChange}
            rows={15}
            className="w-full px-4 py-2 border rounded-md font-mono"
            required
          ></textarea>
        </div>
        
        <div className="flex items-center">
          <input
            type="checkbox"
            id="published"
            name="published"
            checked={formData.published}
            onChange={handleChange}
            className="mr-2"
          />
          <label htmlFor="published" className="text-sm font-medium">
            立即发布
          </label>
        </div>
        
        <div>
          <button
            type="submit"
            className="bg-blue-600 text-white px-6 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
            disabled={createPostMutation.isLoading}
          >
            {createPostMutation.isLoading ? '保存中...' : '保存文章'}
          </button>
        </div>
        
        {createPostMutation.error && (
          <div className="text-red-600">
            {(createPostMutation.error as Error).message}
          </div>
        )}
      </form>
    </div>
  );
}
评论组件 - components/CommentSection.tsx
typescript 复制代码
tsx复制代码
'use client';

import { useState } from 'react';
import { useMutation, useQueryClient } from 'react-query';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
import { Comment } from '@/types';
import { postAPI } from '@/lib/api';
import { formatDate } from '@/lib/utils';
import { useAuth } from '@/hooks/useAuth';

interface CommentSectionProps {
  postId: string;
  comments: Comment[];
}

export default function CommentSection({ postId, comments }: CommentSectionProps) {
  const { user, isAuthenticated } = useAuth();
  const router = useRouter();
  const queryClient = useQueryClient();
  const [newComment, setNewComment] = useState('');
  
  const addCommentMutation = useMutation(
    () => postAPI.addComment(postId, newComment),
    {
      onSuccess: () => {
        setNewComment('');
        router.refresh(); // 刷新服务器组件
        queryClient.invalidateQueries(['post', postId]);
      }
    }
  );
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (!isAuthenticated) {
      router.push('/login?redirect=' + encodeURIComponent(window.location.pathname));
      return;
    }
    
    if (newComment.trim()) {
      addCommentMutation.mutate();
    }
  };
  
  return (
    <section className="mt-12">
      <h2 className="text-2xl font-semibold mb-6">评论 ({comments.length})</h2>
      
      <form onSubmit={handleSubmit} className="mb-8">
        <textarea
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder={isAuthenticated ? "写下你的评论..." : "请登录后发表评论"}
          className="w-full p-3 border rounded-md mb-2"
          rows={4}
          disabled={!isAuthenticated}
        ></textarea>
        
        <button
          type="submit"
          className="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 disabled:bg-gray-400"
          disabled={!isAuthenticated || newComment.trim() === '' || addCommentMutation.isLoading}
        >
          {addCommentMutation.isLoading ? '发表中...' : '发表评论'}
        </button>
      </form>
      
      <div className="space-y-6">
        {comments.length === 0 ? (
          <p className="text-gray-500 text-center py-4">暂无评论,来发表第一条吧!</p>
        ) : (
          comments.map(comment => (
            <div key={comment.id} className="border-b pb-4">
              <div className="flex items-center mb-2">
                {comment.author.image ? (
                  <Image
                    src={comment.author.image}
                    alt={comment.author.name}
                    width={36}
                    height={36}
                    className="rounded-full mr-3"
                  />
                ) : (
                  <div className="w-9 h-9 bg-gray-300 rounded-full mr-3"></div>
                )}
                
                <div>
                  <h4 className="font-medium">{comment.author.name}</h4>
                  <p className="text-xs text-gray-500">
                    {formatDate(comment.createdAt)}
                  </p>
                </div>
              </div>
              
              <p className="ml-12">{comment.content}</p>
            </div>
          ))
        )}
      </div>
    </section>
  );
}

4. 认证与状态管理

认证上下文提供者 - contexts/AuthContext.tsx

typescript 复制代码
tsx复制代码
'use client';

import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import { User } from '@/types';
import { authAPI } from '@/lib/api';
import { jwtDecode } from 'jwt-decode';

interface AuthContextType {
  user: User | null;
  isAuthenticated: boolean;
  isLoading: boolean;
  login: (email: string, password: string) => Promise<void>;
  register: (name: string, email: string, password: string) => Promise<void>;
  logout: () => void;
}

const AuthContext = createContext<AuthContextType | undefined>(undefined);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  
  // 初始化用户状态
  useEffect(() => {
    const token = localStorage.getItem('token');
    if (token) {
      try {
        const decoded = jwtDecode<{ id: string, email: string }>(token);
        // 通常这里会调用API获取完整的用户信息
        setUser({
          id: decoded.id,
          email: decoded.email,
          name: '' // 这里假设只有id和email可解析,真实应用中应调用API获取完整

7. 部署与运维技巧

  • Vercel部署Next.js应用
  • Docker容器化Node.js服务
  • CI/CD自动化流程
  • 监控与日志

总结

现代全栈开发将JavaScript的强大生态系统与前沿框架相结合,为开发者提供了前所未有的开发体验和产品可能性。通过Next.js和Node.js的组合,我们可以构建高性能、易维护且用户体验出色的Web应用。

希望本文能为你的全栈开发之旅提供有价值的参考。技术在不断进化,保持学习、保持好奇,才能在这个充满机遇的领域不断成长。


作者简介:资深全栈工程师,拥有多年JavaScript开发经验,热衷于研究前沿Web技术及架构设计。

相关推荐
风继续吹..1 小时前
后台管理系统权限管理:前端实现详解
前端·vue
yuanmenglxb20042 小时前
前端工程化包管理器:从npm基础到nvm多版本管理实战
前端·前端工程化
新手小新2 小时前
C++游戏开发(2)
开发语言·前端·c++
我不吃饼干3 小时前
【TypeScript】三分钟让 Trae、Cursor 用上你自己的 MCP
前端·typescript·trae
Code blocks3 小时前
关于“LoggerFactory is not a Logback LoggerContext but Logback is on ......“的解决方案
java·spring boot·后端
小杨同学yx4 小时前
前端三剑客之Css---day3
前端·css
Mintopia5 小时前
🧱 用三维点亮前端宇宙:构建你自己的 Three.js 组件库
前端·javascript·three.js
04Koi.5 小时前
八股训练--Spring
java·后端·spring
故事与九5 小时前
vue3使用vue-pdf-embed实现前端PDF在线预览
前端·vue.js·pdf
Mintopia6 小时前
🚀 顶点-面碰撞检测之诗:用牛顿法追寻命运的交点
前端·javascript·计算机图形学