现代全栈开发: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技术及架构设计。

相关推荐
Mike_jia36 分钟前
Memos:知识工作者的理想开源笔记系统
前端
前端大白话37 分钟前
前端崩溃瞬间救星!10 个 JavaScript 实战技巧大揭秘
前端·javascript
loveoobaby38 分钟前
Shadertoy着色器移植到Three.js经验总结
前端
Rabbb39 分钟前
C# JSON属性排序、比较 Newtonsoft.Json
后端
蓝易云41 分钟前
在Linux、CentOS7中设置shell脚本开机自启动服务
前端·后端·centos
浩龙不eMo41 分钟前
前端获取环境变量方式区分(Vite)
前端·vite
一千柯橘1 小时前
Nestjs 解决 request entity too large
javascript·后端
土豆骑士1 小时前
monorepo 实战练习
前端
土豆骑士1 小时前
monorepo最佳实践
前端
见青..1 小时前
【学习笔记】文件包含漏洞--本地远程包含、伪协议、加密编码
前端·笔记·学习·web安全·文件包含