生产环境 QA 校验器
按顺序执行各阶段。修复失败项后再进入下一阶段。
适用场景
- 在全栈 Next.js 应用发布或晋升到生产环境之前使用。
- 在涉及 UI、SEO、鉴权、API、数据库或依赖的大改动之后,需要一次具体的上线就绪度检查时使用。
- 当你需要一份紧凑的、以命令驱动的检查清单,覆盖构建、路由、元数据、性能、安全与清理项时使用。
export PROD_URL="https://yourdomain.com"
export QA_AUTH_HEADER="" # optional: "Bearer eyJ..."
export PAGESPEED_API_KEY="" # optional: for auto PageSpeed API
合并运行器
qa:all() { qa:code && qa:build && qa:routes / /about /contact /privacy /terms /faq /sitemap.xml /robots.txt /api/health && qa:seo && qa:api /api/health /api/tools && qa:git && qa:smoke; }
qa:full() { qa:all && qa:auth && qa:auth:cookies && qa:lazyload && qa:heavyload && qa:vulns && qa:cleanup && qa:ux:cards && qa:ux:boundaries && qa:ux:animation && qa:database && qa:secure; }
阶段 1:代码完整性
qa:code() { npx tsc --noEmit && npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 && npm test -- --runInBand --passWithNoTests; }
阶段 2:构建验证
qa:build() { local log; log="$(mktemp "${TMPDIR:-/tmp}/qa-build.XXXXXX.log")" || return 1; set -o pipefail; npm run build 2>&1 | tee "$log"; local rc=$?; set +o pipefail; [ "$rc" -eq 0 ] && ! grep -qi "error\|failed" "$log"; local ok=$?; rm -f "$log"; return "$ok"; }
| 符号 |
含义 |
○ |
静态 |
● |
SSG |
λ |
动态/无服务器 |
⊕ |
部分预渲染 |
阶段 3:API 会话与鉴权
qa:auth() {
local F=0
for ep in /api/auth/login /api/auth/session /api/auth/logout; do
curl -so /dev/null -w "%{http_code}" "$PROD_URL$ep" | grep -q "200\|401" || { echo " ✗ $ep unreachable"; ((F++)); }
done
curl -so /dev/null -w "%{http_code}" "$PROD_URL/api/protected" | grep -q "401\|403" || echo " ⚠ Protected route not denying unauthenticated"
return $F
}
qa:auth:cookies() {
for ep in /api/auth/session /api/auth/login; do
curl -sI "$PROD_URL$ep" | grep -i "^set-cookie:" | while IFS= read -r c; do
echo " $ep: $(echo "$c" | cut -d= -f1)"
echo "$c" | grep -qi "HttpOnly" || echo " ✗ Missing HttpOnly"
echo "$c" | grep -qi "Secure" || echo " ✗ Missing Secure"
echo "$c" | grep -qi "SameSite" || echo " ⚠ Missing SameSite"
done
done
}
阶段 4:路由回归
qa:routes() { local F=0; for p; do local C=$(curl -so /dev/null -w "%{http_code}" "$PROD_URL$p"); echo "$C $p"; [ "$C" = "200" ] || ((F++)); done; return $F; }
qa:robots() { curl -s "$PROD_URL/robots.txt" | grep -qi "Disallow: /$" && echo " ✗ Blocks all crawlers" || echo " ✓ OK"; }
qa:sitemap() { curl -s "$PROD_URL/sitemap.xml" | python3 -c "import sys,xml.etree.ElementTree as ET; ET.parse(sys.stdin); print('✓ Valid XML')"; }
阶段 5:SEO — 标签、图片、Favicon、Slug
qa:seo() {
local H=$(curl -s "$PROD_URL"); local F=0
for t in "og:title" "og:description" "og:image" "twitter:card" "canonical" "description"; do echo "$H" | grep -qi "$t" || { echo " ✗ $t"; ((F++)); }; done
echo "$H" | grep -qi "<title>" || { echo " ✗ <title>"; ((F++)); }
local T=$(echo "$H" | grep -oP '<title>\K[^<]+'); local L=${#T}; [ $L -ge 30 -a $L -le 60 ] || echo " ⚠ Title ${L}chars (target 30-60)"
curl -so /dev/null -w "%{http_code}" "$PROD_URL/favicon.ico" | grep -q 200 || echo " ⚠ No favicon.ico"
return $F
}
qa:seo:ogimage() {
local I=$(curl -s "$PROD_URL" | grep -oP 'og:image" content="\K[^"]+'); [[ "$I" =~ ^http ]] || I="$PROD_URL$I"
curl -so /dev/null -w "%{http_code}" "$I" | grep -q 200 || { echo " ✗ og:image returns non-200"; return 1; }
command -v identify &>/dev/null && curl -s "$I" | identify -format "%wx%h" - 2>/dev/null | grep -qP "12\d{2}x6\d{2}" && echo " ✓ ≥ 1200x630" || echo " ⚠ Install imagemagick to check dimensions"
}
阶段 6:API 路由行为
qa:api() {
for p; do
local R=$(curl -so /dev/null -w "%{http_code} %{content_type}" "$PROD_URL$p")
echo " $p → $R"
done
local E=$(curl -s "$PROD_URL/api/nonexistent")
echo "$E" | python3 -c "import sys,json; d=json.load(sys.stdin); assert 'error' in d; print('✓ Consistent errors')" 2>/dev/null || echo " ⚠ Inconsistent error shape"
}
阶段 7:Git 卫生
qa:git() {
local S=$(git diff HEAD 2>/dev/null | grep -i "password\|secret\|api_key\|localhost:3000" | grep "^+")
[ -n "$S" ] && { echo " ✗ Secrets in diff!"; echo "$S"; return 1; } || echo " ✓ No secrets"
local A=$(git status --short 2>/dev/null | grep -E "\.next|node_modules" | head -3)
[ -n "$A" ] && echo " ⚠ Build artifacts:" && echo "$A" || echo " ✓ No artifacts"
}
阶段 8:部署后冒烟测试
qa:smoke() {
curl -sI "$PROD_URL" | head -1 | grep -q "200" && echo " ✓ Homepage" || echo " ✗ Homepage"
curl -sI "$PROD_URL/sitemap.xml" | head -1 | grep -q "200" && echo " ✓ Sitemap" || echo " ✗ Sitemap"
}
阶段 9:页面速度、懒加载与包体积
qa:lazyload() {
local N=$(grep -r "loading=" app/ --include="*.tsx" 2>/dev/null | grep -c "lazy" || true)
echo " Lazy images: $N"
grep -rn "next/dynamic\|dynamic((" app/ --include="*.tsx" 2>/dev/null | head -5 | grep . || echo " ⚠ No dynamic imports"
}
qa:heavyload() {
ls -lhS .next/static/chunks/*.js 2>/dev/null | head -5
local W=$(curl -so /dev/null -w "%{size_download}" "$PROD_URL" 2>/dev/null || echo 0)
echo " HTML weight: ~$((W/1024))KB"
echo " ⚠ Run 'npx lighthouse $PROD_URL --view' for full weight analysis"
}
# PageSpeed: open "https://pagespeed.web.dev/?url=$PROD_URL"
阶段 10:清理与漏洞扫描
qa:vulns() {
npm audit 2>/dev/null | grep -E "critical|high" | grep . && echo " ✗ Vulnerabilities!" || echo " ✓ No critical/high vulns"
npm outdated 2>/dev/null | head -5 | grep . || echo " ✓ All up to date"
local D=$(grep -rn "eval(\|new Function(\|document.write(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5)
[ -n "$D" ] && echo " ⚠ Dangerous patterns:" && echo "$D" || echo " ✓ No dangerous patterns"
}
qa:cleanup() {
local D=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "console\.log\|debugger" | head -5)
[ -n "$D" ] && echo " ✗ Debug artifacts:" && echo "$D" || echo " ✓ No debug artifacts"
local T=$(git diff --cached 2>/dev/null | grep "^+" | grep -i "TODO\|FIXME\|HACK" | head -5)
[ -n "$T" ] && echo " ⚠ TODOs remain:" && echo "$T"
}
阶段 11:UI/UX — 卡片、动画、错误边界
qa:ux:cards() {
local E=$(grep -rn "text-overflow\|line-clamp\|truncate" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$E" ] && echo " ✓ Text overflow handling" || echo " ⚠ No text overflow handling"
local A=$(grep -rn "aspect-\|object-fit" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$A" ] && echo " ✓ aspect-ratio/object-fit used" || echo " ⚠ No aspect-ratio set"
}
qa:ux:boundaries() {
for f in app/error.tsx app/global-error.tsx app/not-found.tsx app/loading.tsx; do
[ -f "$f" ] && echo " ✓ $f" || echo " ⚠ Missing $f"
done
}
qa:ux:animation() {
local A=$(grep -rn "animation.*width\|transition.*height\|@keyframes.*top\|@keyframes.*margin" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -5)
[ -n "$A" ] && echo " ⚠ Layout-triggering animations:" && echo "$A" || echo " ✓ No layout-triggering animations"
local P=$(grep -r "@media.*prefers-reduced-motion" app/ --include="*.css" --include="*.tsx" 2>/dev/null | head -3)
[ -n "$P" ] && echo " ✓ prefers-reduced-motion found in CSS" || echo " ⚠ No prefers-reduced-motion in CSS"
}
阶段 12:数据库与数据层
qa:database() {
local H=$(grep -rn "postgres://\|mysql://\|mongodb://" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v ".env" | head -5)
[ -n "$H" ] && { echo " ✗ Hardcoded DB URL:"; echo "$H"; } || echo " ✓ No hardcoded DB URLs"
local R=$(grep -rn "\$queryRaw\|\.raw(" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | head -5)
[ -n "$R" ] && echo " ⚠ Raw SQL:" && echo "$R" || echo " ✓ No raw SQL"
local N=$(grep -rn "\.findMany\|\.findUnique" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v "include:" | head -5)
[ -n "$N" ] && echo " ⚠ Possible N+1:" && echo "$N" || echo " ✓ No N+1 patterns"
}
qa:db:migrations() {
[ -d "prisma/migrations" ] && echo " ✓ Prisma: $(ls prisma/migrations 2>/dev/null | wc -l) migrations" || echo " - No prisma migrations dir"
local M=$(ls db/migrations/*.sql 2>/dev/null | head -5); [ -n "$M" ] && echo " ✓ SQL migrations:" && echo "$M" || echo " - No SQL migration files"
}
阶段 13:安全的数据渲染
qa:secure() {
local S=$(git grep -n "api_key\|API_KEY\|secret_key\|PRIVATE_KEY" -- ':!*.env*' ':!*test*' 2>/dev/null | head -5)
[ -n "$S" ] && echo " ✗ Secrets in source:" && echo "$S" || echo " ✓ No hardcoded secrets"
local D=$(grep -rn "dangerouslySetInnerHTML" app/ src/ --include="*.tsx" 2>/dev/null | head -5)
[ -n "$D" ] && echo " ⚠ XSS risk — use DOMPurify:" && echo "$D" || echo " ✓ No dangerouslySetInnerHTML"
local T=$(grep -rn "localStorage\|sessionStorage" app/ src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -i "token\|jwt\|secret" | head -5)
[ -n "$T" ] && echo " ⚠ Tokens in storage — use httpOnly cookies:" && echo "$T" || echo " ✓ No tokens in storage"
curl -s "$PROD_URL/api/nonexistent" 2>/dev/null | grep -qi "stack\|Error:" && echo " ✗ Stack trace leak" || echo " ✓ No stack leak"
}
Pre-Commit Hook
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/sh
npx tsc --noEmit || exit 1
npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0 || exit 1
EOF
chmod +x .git/hooks/pre-commit
CI/CD(GitHub Actions)
name: QA
on: [push, pull_request]
jobs:
qa:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx tsc --noEmit
- run: npx eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 0
- run: npm test -- --runInBand --passWithNoTests
- run: npm run build
最佳实践
| ✅ 应该 |
❌ 不应该 |
| 部署前运行完整的 13 阶段流程 |
跳过类型检查或 lint |
在 profile/.envrc 中设置 PROD_URL |
在脚本中硬编码 URL |
| OG 图片 ≥ 1200×630 |
使用小尺寸 OG 图片 |
使用 transform+opacity 做动画 |
对 width/height/top 做动画 |
| 展示 loading/error/empty 三种状态 |
让用户停留在空白页面 |
动画使用 prefers-reduced-motion |
对所有用户强制开启动效 |
| token 使用 HttpOnly + Secure Cookie |
把鉴权 token 存入 localStorage |
| 在所有层级设置错误边界 |
崩溃时出现白屏 |
| 数据库加索引 + include/populate |
在循环中执行 N+1 查询 |
部署前执行 npm audit |
在存在已知漏洞时部署 |
常见陷阱
| 问题 |
解决方案 |
| 原始 HTML 中缺少 OG 标签 |
在 Next.js 中使用 export const metadata |
Disallow: / 出现在 robots.txt |
会阻止所有爬虫 — 改为针对具体路径 |
| 网格中卡片高度不一致 |
使用 display: grid 并设置等高行,而非 flex |
| 文本溢出卡片 |
添加 text-overflow: ellipsis + overflow: hidden |
| 动画卡顿 |
动画作用于 transform 而非 width/height |
| 表单重复提交 |
首次点击后立即禁用按钮 |
| 生产环境出现控制台错误 |
添加 no-console ESLint 规则 |
| 数据库连接超时 |
增加连接池(PgBouncer/Prisma Accelerate) |
| API 泄露敏感数据 |
在响应转换层剥离 passwordHash/secret |
| 应用因错误而崩溃 |
添加 app/error.tsx 错误边界 |
| JS 包体积过大 |
对重量级组件使用动态导入,配合 next/bundle-analyzer 分析 |
| 图片加载缓慢 |
添加 loading="lazy",使用 WebP/AVIF,调整到显示尺寸 |
安全注意事项
- 所有
qa:* 函数都是只读的(tsc、lint、test、build、curl、grep)
PROD_URL 与 QA_AUTH_HEADER 仅用于你拥有的环境
git diff 中只做了基础的密钥扫描 — 生产环境请使用 trufflehog/git-secrets
- 在生产环境上使用真实凭据进行鉴权测试具有破坏性 — 请改用预发布环境
局限性
- 通过全部阶段只能降低风险,无法消除生产 bug
- 部分检查依赖项目特定的工具(Prisma、NextAuth 等)
- 关键用户路径仍需手动进行 UX 测试
- SEO 检查仅验证原始 HTML,无法覆盖社交预览渲染
- 路由检查仅验证状态码,不验证内容正确性
主检查清单
阶段 1:代码
阶段 2:构建
阶段 3:鉴权
阶段 4:路由
阶段 5:SEO
阶段 6:API
阶段 7:Git
阶段 8:冒烟
阶段 9:速度
阶段 10:清理
阶段 11:UI/UX
阶段 12:数据库
阶段 13:安全渲染