第 13 章:性能优化与规范回顾
约 3652 字大约 12 分钟
2025-12-20
本章学习目标
- 掌握代码规范工具(ESLint、Prettier)
- 理解前后端性能优化策略
- 重点掌握异步优化技巧
- 了解安全最佳实践
- 🎉 成果:代码更规范,网站更快!
项目功能完成了,看起来也不错。但真正的专业开发者还会关心:代码质量和性能!
本章结束后你会得到什么
一套规范化的代码、更快的响应速度、更安全的系统!
13.1 代码规范化
13.1.1 为什么需要代码规范?
历史趣事:Tab vs Space 之争
程序员界有一场著名的"圣战"——用 Tab 还是 Space 缩进代码?
这场争论从 1970 年代持续至今。2016 年,Google 的一项分析显示:使用 Space 的开发者平均薪资比使用 Tab 的高 8%(当然这只是相关性,不是因果关系)。
最搞笑的是《硅谷》电视剧还专门为此做了一集——主角因为这个问题和女朋友分手了!
其实真正重要的不是用哪个,而是团队统一。现代工具让这不再是问题:配置一次,自动格式化。
代码规范的好处:
- 团队协作:每个人写的代码风格一致
- 减少错误:工具可以发现潜在问题
- 代码审查:不用争论风格问题
- 可维护性:几个月后自己也能看懂
13.1.2 ESLint:代码检查器
ESLint 是 JavaScript/TypeScript 最流行的代码检查工具。
在前端项目中安装:
cd frontend
npm install -D eslint @nuxtjs/eslint-config-typescript创建配置文件 .eslintrc.cjs:
// frontend/.eslintrc.cjs
module.exports = {
root: true,
extends: [
'@nuxtjs/eslint-config-typescript'
],
rules: {
// 自定义规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
// Vue 相关规则
'vue/multi-word-component-names': 'off',
'vue/no-multiple-template-root': 'off',
// TypeScript 相关
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
}
}添加脚本到 package.json:
{
"scripts": {
"lint": "eslint --ext .ts,.js,.vue .",
"lint:fix": "eslint --ext .ts,.js,.vue . --fix"
}
}13.1.3 Prettier:代码格式化器
Prettier 专注于代码格式化,与 ESLint 配合使用效果最佳。
安装 Prettier:
npm install -D prettier eslint-config-prettier eslint-plugin-prettier创建配置文件 .prettierrc:
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100,
"vueIndentScriptAndStyle": true
}更新 ESLint 配置以集成 Prettier:
// frontend/.eslintrc.cjs
module.exports = {
root: true,
extends: [
'@nuxtjs/eslint-config-typescript',
'prettier' // 添加 prettier,放在最后
],
plugins: ['prettier'],
rules: {
'prettier/prettier': 'warn',
// ... 其他规则
}
}13.1.4 Python 代码规范
后端项目也需要代码规范!
安装工具:
cd backend
pip install black isort flake8配置文件 pyproject.toml:
# backend/pyproject.toml
[tool.black]
line-length = 88
target-version = ['py311']
include = '\.pyi?$'
[tool.isort]
profile = "black"
line_length = 88
[tool.flake8]
max-line-length = 88
extend-ignore = ["E203", "E501"]使用方法:
# 格式化代码
black .
# 排序 import
isort .
# 检查代码
flake8 .关于ruff
ruff 是一个 Python 代码格式化工具,与 black 类似,但它是由Rust写的。它的优势在于:
- 速度快(比 black 快 2-3 倍)
- 配置简单(无需 pyproject.toml)
- 支持插件系统(如 flake8 插件)
你可以选择使用 ruff 替代 black 和 flake8,具体用法请参考 ruff 官方文档。
VS Code 自动格式化
在 VS Code 中安装 ESLint、Prettier 和 Python 扩展,设置保存时自动格式化:
// .vscode/settings.json
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
}
}这样每次保存文件时,代码都会自动格式化!
13.2 项目结构最佳实践
13.2.1 前端项目结构
良好的项目结构让代码更易维护:
frontend/
├── app/
│ ├── assets/ # 静态资源(CSS、图片)
│ │ ├── css/
│ │ │ └── variables.css
│ │ └── images/
│ │
│ ├── components/ # 可复用组件
│ │ ├── common/ # 通用组件
│ │ │ ├── Button.vue
│ │ │ ├── Card.vue
│ │ │ └── Modal.vue
│ │ ├── article/ # 文章相关组件
│ │ │ ├── ArticleCard.vue
│ │ │ └── ArticleContent.vue
│ │ └── layout/ # 布局组件
│ │ ├── NavBar.vue
│ │ └── Footer.vue
│ │
│ ├── composables/ # 可组合函数
│ │ ├── useAuth.ts
│ │ ├── useApi.ts
│ │ └── useFeature.ts
│ │
│ ├── middleware/ # 路由中间件
│ │ └── auth.ts
│ │
│ ├── pages/ # 页面组件
│ │ ├── index.vue
│ │ ├── blog/
│ │ └── user/
│ │
│ ├── types/ # TypeScript 类型定义
│ │ ├── api.ts
│ │ └── models.ts
│ │
│ └── utils/ # 工具函数
│ ├── format.ts
│ └── validation.ts
│
├── nuxt.config.ts
└── package.json13.2.2 后端项目结构
backend/
├── app/
│ ├── __init__.py
│ ├── main.py # 应用入口
│ │
│ ├── api/ # API 路由
│ │ ├── __init__.py
│ │ ├── articles.py
│ │ ├── auth.py
│ │ └── comments.py
│ │
│ ├── core/ # 核心配置
│ │ ├── __init__.py
│ │ ├── config.py # 配置管理
│ │ ├── security.py # 安全相关
│ │ └── database.py # 数据库配置
│ │
│ ├── models/ # 数据模型
│ │ ├── __init__.py
│ │ ├── article.py
│ │ └── user.py
│ │
│ ├── schemas/ # Pydantic 模型
│ │ ├── __init__.py
│ │ └── article.py
│ │
│ ├── services/ # 业务逻辑层
│ │ ├── __init__.py
│ │ ├── article_service.py
│ │ └── user_service.py
│ │
│ └── utils/ # 工具函数
│ ├── __init__.py
│ └── helpers.py
│
├── tests/ # 测试目录
│ ├── __init__.py
│ └── test_api.py
│
├── requirements.txt
└── pyproject.toml13.2.3 命名规范
文件命名:
- Vue 组件:
PascalCase.vue(如ArticleCard.vue) - JavaScript/TypeScript:
camelCase.ts(如useAuth.ts) - Python:
snake_case.py(如article_service.py) - CSS/样式:
kebab-case.css(如main-style.css)
代码命名:
| 类型 | JavaScript/TypeScript | Python |
|---|---|---|
| 变量/函数 | camelCase | snake_case |
| 类 | PascalCase | PascalCase |
| 常量 | UPPER_SNAKE_CASE | UPPER_SNAKE_CASE |
| 私有变量 | _camelCase | _snake_case |
13.3 前端性能优化
13.3.1 懒加载与代码分割
Nuxt 默认按路由进行代码分割,但我们还可以进一步优化:
组件懒加载:
<script setup>
// 懒加载组件(不会包含在初始 bundle 中)
const HeavyChart = defineAsyncComponent(() =>
import('~/components/HeavyChart.vue')
)
// 带加载状态的懒加载
const CommentList = defineAsyncComponent({
loader: () => import('~/components/CommentList.vue'),
loadingComponent: SkeletonComment,
delay: 200, // 延迟显示加载状态
timeout: 5000
})
</script>路由懒加载(Nuxt 已自动支持):
// Nuxt 自动将每个 pages/ 下的文件作为独立 chunk
// 只有访问该路由时才会加载对应代码13.3.2 图片优化
<template>
<!-- 使用 Nuxt Image 组件 -->
<NuxtImg
src="/images/hero.jpg"
width="800"
height="400"
loading="lazy"
format="webp"
sizes="(max-width: 768px) 100vw, 800px"
/>
</template>安装 Nuxt Image:
npm install @nuxt/image// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
quality: 80,
format: ['webp', 'jpg']
}
})13.3.3 防抖与节流
什么是防抖和节流?
- 防抖(Debounce):等用户停止操作后再执行。比如搜索框输入,等停止输入 300ms 后才发请求。
- 节流(Throttle):限制执行频率。比如滚动事件,每 100ms 最多执行一次。
实现防抖函数:
// frontend/app/utils/debounce.ts
export function debounce<T extends (...args: any[]) => any>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null
return function (...args: Parameters<T>) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn(...args)
timer = null
}, delay)
}
}
// 使用示例:搜索框
const searchInput = ref('')
const searchResults = ref([])
const doSearch = debounce(async (keyword: string) => {
const { data } = await useFetch(`/api/search?q=${keyword}`)
searchResults.value = data.value
}, 300)
watch(searchInput, (newValue) => {
if (newValue.length >= 2) {
doSearch(newValue)
}
})实现节流函数:
// frontend/app/utils/throttle.ts
export function throttle<T extends (...args: any[]) => any>(
fn: T,
interval: number
): (...args: Parameters<T>) => void {
let lastTime = 0
return function (...args: Parameters<T>) {
const now = Date.now()
if (now - lastTime >= interval) {
fn(...args)
lastTime = now
}
}
}
// 使用示例:滚动加载
const handleScroll = throttle(() => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement
if (scrollTop + clientHeight >= scrollHeight - 100) {
loadMoreArticles()
}
}, 100)
onMounted(() => {
window.addEventListener('scroll', handleScroll)
})
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll)
})13.4 后端性能优化
13.4.1 数据库查询优化
问题:N+1 查询
# ❌ 糟糕的代码:N+1 查询问题
@router.get("/articles")
def get_articles(db: Session = Depends(get_db)):
articles = db.query(Article).all() # 1 次查询
result = []
for article in articles:
author = db.query(User).get(article.author_id) # N 次查询!
result.append({
"title": article.title,
"author": author.username
})
return result# ✅ 优化后:使用 joinedload 预加载
from sqlalchemy.orm import joinedload
@router.get("/articles")
def get_articles(db: Session = Depends(get_db)):
articles = db.query(Article)\
.options(joinedload(Article.author))\
.all() # 只有 1-2 次查询
return [
{
"title": a.title,
"author": a.author.username
}
for a in articles
]添加数据库索引:
# models/article.py
class Article(Base):
__tablename__ = "articles"
id = Column(Integer, primary_key=True)
title = Column(String(200), nullable=False)
author_id = Column(Integer, ForeignKey("users.id"), index=True) # 添加索引
created_at = Column(DateTime, default=datetime.utcnow, index=True) # 添加索引
status = Column(String(20), index=True)
# 复合索引
__table_args__ = (
Index('idx_author_status', 'author_id', 'status'),
)13.4.2 分页查询
不要一次返回所有数据!
# api/articles.py
from fastapi import Query
@router.get("/articles")
def get_articles(
page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)
):
# 计算偏移量
offset = (page - 1) * page_size
# 获取总数
total = db.query(Article).filter(Article.status == "published").count()
# 分页查询
articles = db.query(Article)\
.filter(Article.status == "published")\
.order_by(Article.created_at.desc())\
.offset(offset)\
.limit(page_size)\
.all()
return {
"items": articles,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": (total + page_size - 1) // page_size
}前端分页组件:
<!-- frontend/app/components/Pagination.vue -->
<template>
<div class="pagination">
<button
@click="$emit('change', currentPage - 1)"
:disabled="currentPage <= 1"
class="page-btn"
>
上一页
</button>
<span class="page-info">
第 {{ currentPage }} / {{ totalPages }} 页
</span>
<button
@click="$emit('change', currentPage + 1)"
:disabled="currentPage >= totalPages"
class="page-btn"
>
下一页
</button>
</div>
</template>
<script setup>
defineProps<{
currentPage: number
totalPages: number
}>()
defineEmits<{
(e: 'change', page: number): void
}>()
</script>13.5 异步优化:让程序更快
🔥 重点章节
异步编程是现代 Web 开发的核心技能。掌握好异步优化,可以让你的应用响应速度提升数倍!
13.5.1 理解 Python 异步编程
历史趣事:Python async/await 的诞生
Python 的异步编程经历了漫长的演进:
- 2001:generator 生成器诞生
- 2014:
asyncio库加入标准库 - 2015:Python 3.5 引入
async/await语法
在此之前,Python 程序员用回调地狱处理并发,代码可读性极差。async/await 让异步代码看起来和同步代码一样清晰!
如今 FastAPI 等现代框架都基于 asyncio 构建,充分利用异步的优势。
同步 vs 异步:一个比喻
想象你在餐厅点餐:
- 同步方式:服务员站在你桌边等你吃完,才去服务下一桌。
- 异步方式:服务员给你上菜后就去服务其他桌,你吃完叫他再来。
FastAPI 中的异步路由:
# ❌ 同步方式:会阻塞其他请求
@router.get("/slow")
def slow_endpoint():
import time
time.sleep(3) # 假设这是一个耗时操作
return {"message": "done"}
# ✅ 异步方式:不会阻塞
@router.get("/fast")
async def fast_endpoint():
import asyncio
await asyncio.sleep(3) # 异步等待,期间可以处理其他请求
return {"message": "done"}13.5.2 asyncio.gather:并行执行多个任务
当你需要同时执行多个独立的异步操作时,asyncio.gather 是你的好帮手:
import asyncio
from httpx import AsyncClient
# ❌ 串行请求:3个请求总共需要 3 秒
async def fetch_data_serial():
async with AsyncClient() as client:
user = await client.get("http://api.example.com/user") # 1秒
posts = await client.get("http://api.example.com/posts") # 1秒
comments = await client.get("http://api.example.com/comments") # 1秒
return user, posts, comments # 总共 3 秒
# ✅ 并行请求:3个请求只需要 1 秒
async def fetch_data_parallel():
async with AsyncClient() as client:
user, posts, comments = await asyncio.gather(
client.get("http://api.example.com/user"),
client.get("http://api.example.com/posts"),
client.get("http://api.example.com/comments")
)
return user, posts, comments # 总共约 1 秒实际应用:用户详情页聚合接口
# api/user_detail.py
import asyncio
from httpx import AsyncClient
@router.get("/users/{user_id}/detail")
async def get_user_detail(user_id: int, db: Session = Depends(get_db)):
"""
获取用户详情,包括基本信息、文章数、评论数、关注数
使用 asyncio.gather 并行查询,大幅提升响应速度
"""
# 定义各个查询任务
async def get_user_info():
return db.query(User).get(user_id)
async def get_article_count():
return db.query(Article).filter(Article.author_id == user_id).count()
async def get_comment_count():
return db.query(Comment).filter(Comment.user_id == user_id).count()
async def get_recent_articles():
return db.query(Article)\
.filter(Article.author_id == user_id)\
.order_by(Article.created_at.desc())\
.limit(5)\
.all()
# 并行执行所有查询
user, article_count, comment_count, recent_articles = await asyncio.gather(
asyncio.to_thread(get_user_info), # 在线程中运行同步代码
asyncio.to_thread(get_article_count),
asyncio.to_thread(get_comment_count),
asyncio.to_thread(get_recent_articles)
)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {
"user": {
"id": user.id,
"username": user.username,
"avatar": user.avatar_url
},
"stats": {
"articles": article_count,
"comments": comment_count
},
"recent_articles": recent_articles
}asyncio.to_thread 的作用
asyncio.to_thread() 把同步代码(如 SQLAlchemy 查询)放到线程池中执行,避免阻塞事件循环。
如果你使用的是原生支持异步的库(如 asyncpg、httpx),可以直接 await,不需要 to_thread。
13.5.3 错误处理:gather 的安全使用
# 默认情况下,任何一个任务失败都会抛出异常
try:
results = await asyncio.gather(
task1(),
task2(),
task3()
)
except Exception as e:
print(f"有任务失败了: {e}")
# 使用 return_exceptions=True,失败的任务返回异常对象而不是抛出
results = await asyncio.gather(
task1(),
task2(),
task3(),
return_exceptions=True
)
for i, result in enumerate(results):
if isinstance(result, Exception):
print(f"任务 {i} 失败: {result}")
else:
print(f"任务 {i} 成功: {result}")13.5.4 前端异步优化:Promise.all
JavaScript 中对应的是 Promise.all:
// frontend/app/composables/useUserDetail.ts
export const useUserDetail = (userId: number) => {
const user = ref(null)
const stats = ref(null)
const recentArticles = ref([])
const loading = ref(true)
const error = ref(null)
const fetchData = async () => {
loading.value = true
try {
// ❌ 串行请求(慢)
// const userRes = await $fetch(`/api/users/${userId}`)
// const statsRes = await $fetch(`/api/users/${userId}/stats`)
// const articlesRes = await $fetch(`/api/users/${userId}/articles`)
// ✅ 并行请求(快)
const [userRes, statsRes, articlesRes] = await Promise.all([
$fetch(`/api/users/${userId}`),
$fetch(`/api/users/${userId}/stats`),
$fetch(`/api/users/${userId}/articles?limit=5`)
])
user.value = userRes
stats.value = statsRes
recentArticles.value = articlesRes
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}
return {
user,
stats,
recentArticles,
loading,
error,
fetchData
}
}Promise.allSettled:等待所有完成(无论成功失败)
// 即使部分请求失败,也能获取其他请求的结果
const results = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/comments')
])
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`请求 ${index} 成功:`, result.value)
} else {
console.log(`请求 ${index} 失败:`, result.reason)
}
})13.5.5 超时控制
Python:
import asyncio
async def fetch_with_timeout():
try:
result = await asyncio.wait_for(
slow_operation(),
timeout=5.0 # 5秒超时
)
return result
except asyncio.TimeoutError:
print("操作超时")
return NoneJavaScript:
// 自定义超时函数
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error('Timeout')), ms)
})
return Promise.race([promise, timeout])
}
// 使用
try {
const data = await withTimeout(
fetch('/api/slow-endpoint'),
5000 // 5秒超时
)
} catch (e) {
if (e.message === 'Timeout') {
console.log('请求超时')
}
}13.6 安全最佳实践
13.6.1 输入验证
永远不要信任用户输入!
# ❌ 危险:SQL 注入
@router.get("/search")
def search(keyword: str, db: Session = Depends(get_db)):
# 永远不要这样做!
query = f"SELECT * FROM articles WHERE title LIKE '%{keyword}%'"
return db.execute(query).fetchall()
# ✅ 安全:使用参数化查询
@router.get("/search")
def search(keyword: str, db: Session = Depends(get_db)):
return db.query(Article)\
.filter(Article.title.contains(keyword))\
.all()使用 Pydantic 验证输入:
from pydantic import BaseModel, validator, EmailStr
from typing import Optional
class UserCreate(BaseModel):
username: str
email: EmailStr
password: str
@validator('username')
def username_valid(cls, v):
if len(v) < 3 or len(v) > 20:
raise ValueError('用户名长度必须在 3-20 之间')
if not v.isalnum():
raise ValueError('用户名只能包含字母和数字')
return v
@validator('password')
def password_strong(cls, v):
if len(v) < 8:
raise ValueError('密码至少 8 位')
if not any(c.isupper() for c in v):
raise ValueError('密码必须包含大写字母')
if not any(c.isdigit() for c in v):
raise ValueError('密码必须包含数字')
return v13.6.2 XSS 防护
前端显示用户内容时要防止 XSS 攻击:
<template>
<!-- ❌ 危险:直接渲染 HTML -->
<div v-html="userComment"></div>
<!-- ✅ 安全:使用文本插值(自动转义) -->
<div>{{ userComment }}</div>
<!-- ✅ 如果必须渲染 HTML,先进行清理 -->
<div v-html="sanitizedHtml"></div>
</template>
<script setup>
import DOMPurify from 'dompurify'
const sanitizedHtml = computed(() =>
DOMPurify.sanitize(userComment.value)
)
</script>13.6.3 敏感信息处理
# .env 文件(不要提交到 Git)
DATABASE_URL=sqlite:///./blog.db
SECRET_KEY=your-super-secret-key-change-this-in-production
JWT_ALGORITHM=HS256
# .gitignore
.env
*.db
__pycache__/# core/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
secret_key: str
jwt_algorithm: str = "HS256"
class Config:
env_file = ".env"
settings = Settings()13.7 小结与练习
🎉 恭喜你完成了性能优化与规范回顾!
你现在掌握了:
- ✅ 代码规范:ESLint + Prettier 自动格式化
- ✅ 项目结构:清晰的目录组织
- ✅ 前端优化:懒加载、防抖节流
- ✅ 后端优化:数据库查询、分页
- ✅ 异步优化:asyncio.gather、Promise.all
- ✅ 安全实践:输入验证、XSS 防护
你的项目现在更专业了!
本章回顾
| 概念 | 说明 |
|---|---|
| ESLint | JavaScript 代码检查工具 |
| Prettier | 代码格式化工具 |
| 防抖/节流 | 控制函数执行频率 |
| asyncio.gather | Python 并行异步执行 |
| Promise.all | JavaScript 并行异步执行 |
| N+1 问题 | 数据库查询性能陷阱 |
动手练习
练习 1:实现分页组件 ⭐⭐
完善文章列表页的分页功能:
- 前端分页组件
- 后端分页 API
- 页码和页面大小可配置
练习 2:异步优化实战 ⭐⭐⭐
优化首页加载:
- 使用
asyncio.gather并行获取最新文章、热门文章、统计数据 - 使用
Promise.all在前端并行请求 - 对比优化前后的响应时间
提示:可以用浏览器开发者工具的 Network 面板查看请求时间。
下一章预告
项目开发完成了!但它只能在你的电脑上运行...
下一章我们将学习项目部署,把博客发布到互联网上,让全世界都能访问!