阶段 7:全栈能力 - 拉开差距的关键

4 阅读6分钟

阶段 7:全栈能力 - 拉开差距的关键

全栈不是"什么都会一点",而是能独立完成从数据库到前端的完整链路。这部分会让你在架构设计中更有话语权。


一、Node.js API 开发

1.1 Express + TypeScript 企业级结构

src/
├── config/           # 配置管理
├── controllers/      # 控制器(处理请求/响应)
├── services/         # 业务逻辑层
├── repositories/     # 数据访问层
├── models/           # 数据模型
├── middlewares/      # 中间件
├── routes/           # 路由定义
├── utils/            # 工具函数
├── types/            # TypeScript 类型
└── app.ts            # 应用入口

1.2 完整实现

// ========== types/index.ts ==========
export interface User {
  id: number
  username: string
  email: string
  role: 'admin' | 'user'
  createdAt: Date
}

export interface ApiResponse<T = any> {
  code: number
  message: string
  data?: T
  timestamp: number
}

// ========== config/index.ts ==========
import dotenv from 'dotenv'
dotenv.config()

export const config = {
  port: parseInt(process.env.PORT || '3000'),
  nodeEnv: process.env.NODE_ENV || 'development',
  jwtSecret: process.env.JWT_SECRET!,
  database: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT || '3306'),
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
  },
}

// ========== models/UserModel.ts ==========
import { RowDataPacket } from 'mysql2'

export interface UserRow extends RowDataPacket {
  id: number
  username: string
  email: string
  password_hash: string
  role: 'admin' | 'user'
  created_at: Date
  updated_at: Date
}

// ========== repositories/UserRepository.ts ==========
import pool from '../config/database'
import { UserRow } from '../models/UserModel'
import { User } from '../types'

export class UserRepository {
  async findById(id: number): Promise<User | null> {
    const [rows] = await pool.execute<UserRow[]>(
      'SELECT id, username, email, role, created_at FROM users WHERE id = ?',
      [id]
    )
    if (rows.length === 0) return null
    
    const row = rows[0]
    return {
      id: row.id,
      username: row.username,
      email: row.email,
      role: row.role,
      createdAt: row.created_at,
    }
  }
  
  async findByEmail(email: string): Promise<UserRow | null> {
    const [rows] = await pool.execute<UserRow[]>(
      'SELECT * FROM users WHERE email = ?',
      [email]
    )
    return rows[0] || null
  }
  
  async create(user: Omit<User, 'id' | 'createdAt'> & { passwordHash: string }): Promise<number> {
    const [result] = await pool.execute(
      `INSERT INTO users (username, email, password_hash, role) 
       VALUES (?, ?, ?, ?)`,
      [user.username, user.email, user.passwordHash, user.role]
    )
    return (result as any).insertId
  }
  
  async update(id: number, data: Partial<User>): Promise<boolean> {
    const fields: string[] = []
    const values: any[] = []
    
    if (data.username) {
      fields.push('username = ?')
      values.push(data.username)
    }
    if (data.email) {
      fields.push('email = ?')
      values.push(data.email)
    }
    if (data.role) {
      fields.push('role = ?')
      values.push(data.role)
    }
    
    if (fields.length === 0) return false
    
    values.push(id)
    const [result] = await pool.execute(
      `UPDATE users SET ${fields.join(', ')}, updated_at = NOW() WHERE id = ?`,
      values
    )
    return (result as any).affectedRows > 0
  }
  
  async delete(id: number): Promise<boolean> {
    const [result] = await pool.execute('DELETE FROM users WHERE id = ?', [id])
    return (result as any).affectedRows > 0
  }
}

// ========== services/UserService.ts ==========
import bcrypt from 'bcrypt'
import jwt from 'jsonwebtoken'
import { UserRepository } from '../repositories/UserRepository'
import { config } from '../config'
import { User } from '../types'

export class UserService {
  private userRepository = new UserRepository()
  
  async register(username: string, email: string, password: string): Promise<{ user: User; token: string }> {
    // 检查邮箱是否已存在
    const existing = await this.userRepository.findByEmail(email)
    if (existing) {
      throw new Error('邮箱已被注册')
    }
    
    // 加密密码
    const passwordHash = await bcrypt.hash(password, 10)
    
    // 创建用户
    const userId = await this.userRepository.create({
      username,
      email,
      passwordHash,
      role: 'user',
    })
    
    const user = await this.userRepository.findById(userId)
    if (!user) throw new Error('创建用户失败')
    
    // 生成 token
    const token = this.generateToken(user)
    
    return { user, token }
  }
  
  async login(email: string, password: string): Promise<{ user: User; token: string }> {
    const userRow = await this.userRepository.findByEmail(email)
    if (!userRow) {
      throw new Error('用户不存在')
    }
    
    const isValid = await bcrypt.compare(password, userRow.password_hash)
    if (!isValid) {
      throw new Error('密码错误')
    }
    
    const user: User = {
      id: userRow.id,
      username: userRow.username,
      email: userRow.email,
      role: userRow.role,
      createdAt: userRow.created_at,
    }
    
    const token = this.generateToken(user)
    
    return { user, token }
  }
  
  private generateToken(user: User): string {
    return jwt.sign(
      { userId: user.id, email: user.email, role: user.role },
      config.jwtSecret,
      { expiresIn: '7d' }
    )
  }
  
  async getUserById(id: number): Promise<User | null> {
    return this.userRepository.findById(id)
  }
  
  async updateUser(id: number, data: Partial<User>): Promise<boolean> {
    return this.userRepository.update(id, data)
  }
}

// ========== middlewares/auth.ts ==========
import { Request, Response, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import { config } from '../config'

export interface AuthRequest extends Request {
  userId?: number
  userRole?: string
}

export const authMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
  const token = req.headers.authorization?.replace('Bearer ', '')
  
  if (!token) {
    return res.status(401).json({ code: 401, message: '未提供认证令牌' })
  }
  
  try {
    const decoded = jwt.verify(token, config.jwtSecret) as any
    req.userId = decoded.userId
    req.userRole = decoded.role
    next()
  } catch (error) {
    return res.status(401).json({ code: 401, message: '无效的认证令牌' })
  }
}

export const roleMiddleware = (roles: string[]) => {
  return (req: AuthRequest, res: Response, next: NextFunction) => {
    if (!req.userRole || !roles.includes(req.userRole)) {
      return res.status(403).json({ code: 403, message: '权限不足' })
    }
    next()
  }
}

// ========== middlewares/errorHandler.ts ==========
import { Request, Response, NextFunction } from 'express'

export class AppError extends Error {
  statusCode: number
  constructor(message: string, statusCode: number = 500) {
    super(message)
    this.statusCode = statusCode
  }
}

export const errorHandler = (
  err: Error | AppError,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const statusCode = err instanceof AppError ? err.statusCode : 500
  
  console.error(`[ERROR] ${err.message}`)
  console.error(err.stack)
  
  res.status(statusCode).json({
    code: statusCode,
    message: err.message,
    timestamp: Date.now(),
  })
}

// ========== controllers/UserController.ts ==========
import { Request, Response } from 'express'
import { UserService } from '../services/UserService'
import { AuthRequest } from '../middlewares/auth'
import { AppError } from '../middlewares/errorHandler'

const userService = new UserService()

export class UserController {
  async register(req: Request, res: Response) {
    const { username, email, password } = req.body
    
    // 参数校验
    if (!username || !email || !password) {
      throw new AppError('缺少必要参数', 400)
    }
    
    if (password.length < 6) {
      throw new AppError('密码长度不能少于6位', 400)
    }
    
    const result = await userService.register(username, email, password)
    
    res.json({
      code: 0,
      message: '注册成功',
      data: result,
      timestamp: Date.now(),
    })
  }
  
  async login(req: Request, res: Response) {
    const { email, password } = req.body
    
    if (!email || !password) {
      throw new AppError('缺少必要参数', 400)
    }
    
    const result = await userService.login(email, password)
    
    res.json({
      code: 0,
      message: '登录成功',
      data: result,
      timestamp: Date.now(),
    })
  }
  
  async getProfile(req: AuthRequest, res: Response) {
    const userId = req.userId!
    const user = await userService.getUserById(userId)
    
    if (!user) {
      throw new AppError('用户不存在', 404)
    }
    
    res.json({
      code: 0,
      message: 'success',
      data: user,
      timestamp: Date.now(),
    })
  }
}

// ========== routes/userRoutes.ts ==========
import { Router } from 'express'
import { UserController } from '../controllers/UserController'
import { authMiddleware, roleMiddleware } from '../middlewares/auth'

const router = Router()
const controller = new UserController()

// 公开路由
router.post('/register', controller.register.bind(controller))
router.post('/login', controller.login.bind(controller))

// 需要认证的路由
router.get('/profile', authMiddleware, controller.getProfile.bind(controller))

// 管理员路由
router.delete('/:id', authMiddleware, roleMiddleware(['admin']), async (req, res) => {
  // 管理员删除用户
})

export default router

// ========== app.ts ==========
import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
import compression from 'compression'
import rateLimit from 'express-rate-limit'
import userRoutes from './routes/userRoutes'
import { errorHandler } from './middlewares/errorHandler'
import { config } from './config'

const app = express()

// 安全中间件
app.use(helmet())
app.use(cors())
app.use(compression())

// 限流
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100, // 最多100个请求
  message: '请求过于频繁,请稍后再试',
})
app.use('/api', limiter)

// 解析请求体
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true, limit: '10mb' }))

// 日志
app.use((req, res, next) => {
  console.log(`${req.method} ${req.path} - ${req.ip}`)
  next()
})

// 路由
app.use('/api/users', userRoutes)

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: Date.now() })
})

// 404 处理
app.use((req, res) => {
  res.status(404).json({ code: 404, message: '接口不存在' })
})

// 错误处理
app.use(errorHandler)

// 启动服务
app.listen(config.port, () => {
  console.log(`Server running on http://localhost:${config.port}`)
})

export default app

1.3 数据库连接池(MySQL)

// config/database.ts
import mysql from 'mysql2/promise'
import { config } from '.'

const pool = mysql.createPool({
  host: config.database.host,
  port: config.database.port,
  user: config.database.user,
  password: config.database.password,
  database: config.database.database,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0,
  enableKeepAlive: true,
  keepAliveInitialDelay: 0,
})

// 测试连接
pool.getConnection()
  .then(conn => {
    console.log('Database connected successfully')
    conn.release()
  })
  .catch(err => {
    console.error('Database connection failed:', err)
    process.exit(1)
  })

export default pool

1.4 Redis 集成(缓存 + Session)

// config/redis.ts
import Redis from 'ioredis'
import { config } from '.'

const redis = new Redis({
  host: config.redis.host,
  port: config.redis.port,
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000)
    return delay
  },
})

redis.on('connect', () => {
  console.log('Redis connected')
})

redis.on('error', (err) => {
  console.error('Redis error:', err)
})

// 缓存装饰器
export function cache(ttl: number = 3600) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value
    
    descriptor.value = async function (...args: any[]) {
      const key = `cache:${propertyKey}:${JSON.stringify(args)}`
      
      // 尝试从缓存获取
      const cached = await redis.get(key)
      if (cached) {
        return JSON.parse(cached)
      }
      
      // 执行原方法
      const result = await originalMethod.apply(this, args)
      
      // 存入缓存
      if (result) {
        await redis.setex(key, ttl, JSON.stringify(result))
      }
      
      return result
    }
    
    return descriptor
  }
}

export default redis

二、SSR / 同构应用(Next.js)

2.1 Next.js 14 App Router 架构

// app/layout.tsx
import { Inter } from 'next/font/google'
import './globals.css'
import { Providers } from './providers'

const inter = Inter({ subsets: ['latin'] })

export const metadata = {
  title: 'My App',
  description: 'Generated by Next.js',
  viewport: 'width=device-width, initial-scale=1',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN">
      <body className={inter.className}>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}
// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { useState } from 'react'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: {
            staleTime: 60 * 1000,
            refetchOnWindowFocus: false,
          },
        },
      })
  )

  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  )
}
// app/page.tsx - Server Component
import { Suspense } from 'react'
import { UserList } from '@/components/UserList'
import { ProductList } from '@/components/ProductList'

// Server Component - 可以直接访问数据库
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache', // 静态生成
    // next: { revalidate: 3600 } // ISR
  })
  return res.json()
}

export default async function HomePage() {
  const products = await getProducts()
  
  return (
    <main>
      <h1>首页</h1>
      
      {/* Server Component 渲染 */}
      <section>
        <h2>商品列表</h2>
        <ProductList products={products} />
      </section>
      
      {/* Client Component 懒加载 */}
      <Suspense fallback={<div>加载中...</div>}>
        <UserList />
      </Suspense>
    </main>
  )
}
// components/UserList.tsx - Client Component
'use client'

import { useQuery } from '@tanstack/react-query'
import { useState } from 'react'

async function fetchUsers() {
  const res = await fetch('/api/users')
  if (!res.ok) throw new Error('Failed to fetch users')
  return res.json()
}

export function UserList() {
  const [page, setPage] = useState(1)
  const { data, isLoading, error } = useQuery({
    queryKey: ['users', page],
    queryFn: fetchUsers,
  })
  
  if (isLoading) return <div>加载用户数据...</div>
  if (error) return <div>加载失败</div>
  
  return (
    <ul>
      {data?.users.map((user: any) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

2.2 API Routes(Next.js 后端能力)

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { getServerSession } from 'next-auth'
import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'

// GET /api/users
export async function GET(request: NextRequest) {
  const session = await getServerSession(authOptions)
  
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  const searchParams = request.nextUrl.searchParams
  const page = parseInt(searchParams.get('page') || '1')
  const limit = parseInt(searchParams.get('limit') || '10')
  
  const users = await prisma.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
    select: {
      id: true,
      name: true,
      email: true,
      role: true,
    },
  })
  
  return NextResponse.json({
    users,
    page,
    limit,
    total: await prisma.user.count(),
  })
}

// POST /api/users
export async function POST(request: NextRequest) {
  const body = await request.json()
  const { name, email, password, role } = body
  
  // 参数校验
  if (!name || !email || !password) {
    return NextResponse.json(
      { error: 'Missing required fields' },
      { status: 400 }
    )
  }
  
  // 创建用户
  const user = await prisma.user.create({
    data: {
      name,
      email,
      password: await bcrypt.hash(password, 10),
      role: role || 'user',
    },
    select: {
      id: true,
      name: true,
      email: true,
      role: true,
    },
  })
  
  return NextResponse.json(user, { status: 201 })
}

// PUT /api/users/:id
export async function PUT(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const body = await request.json()
  const id = parseInt(params.id)
  
  const user = await prisma.user.update({
    where: { id },
    data: body,
  })
  
  return NextResponse.json(user)
}

// DELETE /api/users/:id
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const id = parseInt(params.id)
  
  await prisma.user.delete({ where: { id } })
  
  return NextResponse.json({ success: true })
}

2.3 中间件(权限控制、日志)

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { getToken } from 'next-auth/jwt'

// 需要认证的路由
const protectedRoutes = ['/dashboard', '/profile', '/settings']
// 仅管理员可访问
const adminRoutes = ['/admin']

export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  
  // 获取 token
  const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET })
  
  // 检查是否登录
  if (protectedRoutes.some(route => pathname.startsWith(route))) {
    if (!token) {
      const url = new URL('/login', request.url)
      url.searchParams.set('callbackUrl', pathname)
      return NextResponse.redirect(url)
    }
  }
  
  // 检查管理员权限
  if (adminRoutes.some(route => pathname.startsWith(route))) {
    if (token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/403', request.url))
    }
  }
  
  // 记录访问日志(生产环境)
  if (process.env.NODE_ENV === 'production') {
    // 异步记录日志,不阻塞请求
    fetch(`${process.env.LOG_SERVICE_URL}/log`, {
      method: 'POST',
      body: JSON.stringify({
        path: pathname,
        userId: token?.sub,
        timestamp: Date.now(),
        userAgent: request.headers.get('user-agent'),
      }),
      headers: { 'Content-Type': 'application/json' },
    }).catch(console.error)
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/profile/:path*',
    '/settings/:path*',
    '/admin/:path*',
  ],
}

2.4 数据获取模式对比

// 1. SSR - 服务端渲染(每次请求都获取数据)
export default async function SSRPage() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store'  // 禁用缓存,每次都重新获取
  }).then(res => res.json())
  
  return <div>{data}</div>
}

// 2. SSG - 静态生成(构建时获取一次)
export default async function SSGPage() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache'  // 静态生成时获取一次
  }).then(res => res.json())
  
  return <div>{data}</div>
}

// 3. ISR - 增量静态再生(定时重新生成)
export default async function ISRPage() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 }  // 每小时重新生成
  }).then(res => res.json())
  
  return <div>{data}</div>
}

// 4. 动态路由 + SSG
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
  
  return posts.map((post: any) => ({
    id: post.id.toString(),
  }))
}

export default async function PostPage({ params }: { params: { id: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json())
  
  return <div>{post.title}</div>
}

// 5. Client Component - 客户端获取
'use client'

import useSWR from 'swr'

export function ClientPage() {
  const { data, error, isLoading } = useSWR('/api/data', fetcher)
  
  if (isLoading) return <div>Loading...</div>
  if (error) return <div>Error</div>
  
  return <div>{data}</div>
}

三、性能优化对比

渲染模式优点缺点适用场景
SSG最快、CDN 友好数据实时性差博客、文档、营销页
SSRSEO 友好、首屏快服务器压力大电商、社交媒体
ISR兼顾速度与实时性配置复杂商品详情、文章
CSR交互丰富、服务器轻SEO 差、首屏慢后台管理系统

四、全栈面试总结

高频问题

问题回答要点
"Node.js 适合做什么?"BFF、SSR、API 网关、实时应用、工具脚本
"SSR 解决了什么问题?"SEO、首屏加载速度、社交分享预览
"Next.js 和传统 SSR 区别?"零配置、混合渲染、文件路由、API Routes、ISR
"如何优化 Node.js 性能?"集群模式、缓存、异步非阻塞、连接池、PM2 管理
"JWT vs Session?"JWT 无状态适合分布式,Session 可控性好适合单体

全套架构师知识体系已整理完毕。 这些内容覆盖了从底层原理到工程化、从架构设计到全栈能力的完整路径。