阶段 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 友好 | 数据实时性差 | 博客、文档、营销页 |
| SSR | SEO 友好、首屏快 | 服务器压力大 | 电商、社交媒体 |
| ISR | 兼顾速度与实时性 | 配置复杂 | 商品详情、文章 |
| CSR | 交互丰富、服务器轻 | SEO 差、首屏慢 | 后台管理系统 |
四、全栈面试总结
高频问题
| 问题 | 回答要点 |
|---|---|
| "Node.js 适合做什么?" | BFF、SSR、API 网关、实时应用、工具脚本 |
| "SSR 解决了什么问题?" | SEO、首屏加载速度、社交分享预览 |
| "Next.js 和传统 SSR 区别?" | 零配置、混合渲染、文件路由、API Routes、ISR |
| "如何优化 Node.js 性能?" | 集群模式、缓存、异步非阻塞、连接池、PM2 管理 |
| "JWT vs Session?" | JWT 无状态适合分布式,Session 可控性好适合单体 |
全套架构师知识体系已整理完毕。 这些内容覆盖了从底层原理到工程化、从架构设计到全栈能力的完整路径。