回到列表
技术复盘
App技术复盘

App 系统实现技术复盘

复盘 App 系统实现的技术栈、过程、取舍与理由、关键技术、问题、建议等内容。

Claude Opus 4.6Claude Opus 4.6· AI Copilot

以下是正文

一、项目概述

本项目(next-app-04)是一个面向企业客户的商业数据智能(BDI)营销网站,目标是以 SaaS 官网的标准呈现商业服务内容,触及并留存企业主和中高层管理者。技术底座为 Next.js 16 + React 19 + TypeScript 5 + Tailwind CSS 4,辅以 shadcn/ui 组件库、next-intl 国际化和 @next/mdx 内容管理。

本复盘基于对项目全部源码的逐行审阅,对照 Next.js v16.1.6 官方文档、React 19.2 文档及社区最佳实践,识别问题并给出改进建议。


二、技术栈评估

2.1 核心技术栈组合

技术版本角色评价
Next.js16.1.6框架当前最新稳定版,已启用 cacheComponents 和 reactCompiler
React19.2.4UI 运行时最新版,支持 View Transitions、useEffectEvent 等
TypeScript5.9.3类型系统strict 模式已启用
Tailwind CSS4.1.18样式引擎v4 版本,使用 @import 新语法
shadcn/ui + radix-ui3.8.3 / 1.4.3组件库统一使用 radix-ui 单包导入
next-intl4.8.2国际化与 Next.js 16 App Router 深度集成
@next/mdx16.1.6内容管理构建时编译 MDX,配合 remark/rehype 插件链
Biome2.3.14Lint + Format取代 ESLint + Prettier
node:sqliteNode.js 24 内置数据持久化轻量博客交互数据存储
Zod4.3.6数据校验Frontmatter 校验

组合评价:整体选型非常精准。Next.js 16 + React 19 是当前最前沿的全栈框架组合;Tailwind CSS v4 + shadcn/ui 是近两年样式方案的最优解;Biome 替代 ESLint + Prettier 减少了工具链复杂度且性能更优。

2.2 是否有更优替代?

当前选择可能的替代方案取舍理由
@next/mdx 构建时编译Contentlayer / Velite / MDX Remote本地 MDX 适合当前中小规模内容,构建时编译性能最优、依赖最少。当文章 > 100 篇时考虑 Velite 或 CMS
node:sqlitePostgreSQL / Turso / Drizzle博客交互数据量极小,SQLite 零运维。若上生产部署到 Serverless 环境则需换为 Turso 或 PostgreSQL
next-intlParaglide / next-internationalnext-intl v4 是 Next.js App Router i18n 最成熟方案,类型安全好,社区活跃
Zod v4ArkType / ValibotZod v4 生态最丰富,与 next-intl / React Server Actions 集成最好
next-themes自实现 CSS variablesnext-themes 仅 1.5KB,解决了 SSR 闪烁问题,没必要自己写
BiomeoxlintBiome 同时覆盖 lint + format,oxlint 目前仅 lint

三、关键问题清单

3.1 SEO 关键文件缺失(严重)

根据 Next.js v16 官方文档 Project Structure,以下 SEO 关键文件全部缺失:

缺失文件影响优先级
app/robots.ts搜索引擎无法获取爬取规则,影响索引P0
app/sitemap.ts搜索引擎无法发现全部页面,严重影响 SEOP0
app/[locale]/not-found.tsx404 页面无自定义 UI,用户体验差P1
app/[locale]/error.tsx无错误边界,运行时错误直接白屏P1
app/global-error.tsx全局错误无兜底P1
app/[locale]/blog/[slug]/opengraph-image.tsx社交分享无自动 OG 图片P2

对于一个声称"必须对 SEO 友好"的项目,缺少 robots.ts 和 sitemap.ts 是不可接受的

示例修复(app/robots.ts):

import type { MetadataRoute } from "next";
 
export default function robots(): MetadataRoute.Robots {
  const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://bdi.business";
  return {
    rules: { userAgent: "*", allow: "/", disallow: ["/api/", "/_next/"] },
    sitemap: `${baseUrl}/sitemap.xml`,
  };
}

3.2 SQL 注入风险(严重)

lib/actions/blog.ts 中的 toggleReaction 函数使用模板字符串拼接 SQL 列名:

// 危险代码 — column 来自函数参数
const column = current === "approvals" ? "approvals" : "deviations";
db.prepare(`UPDATE interactions SET ${column} = MAX(0, ${column} - 1) ...`);

虽然 column 的值在调用时被限制为 "approvals""deviations" 两个确定值,但这种模式本身是危险的

  1. TypeScript 类型约束并不阻止运行时篡改
  2. 违反了"永远不要在 SQL 中使用字符串拼接"的基本安全原则
  3. 如果未来有人修改调用方,可能引入注入点

建议改为两条独立的预编译语句,用 if/else 分支选择执行。

3.3 "use cache"react.cache() 混用(需关注)

lib/blog.ts 中同一函数同时使用了 React 的 cache() 和 Next.js 16 的 "use cache" 指令:

export const getAllUniqueSlugs = cache(async (): Promise<string[]> => {
  "use cache"; // Next.js 16 Cache Component
  cacheTag("blog-v100");
  cacheLife("days");
  // ...
});

两者的缓存语义不同:

  • react.cache()请求级别的记忆化(同一次渲染中多次调用返回相同结果)
  • "use cache"跨请求的持久化缓存(由 Next.js 管理,支持 revalidation)

二者嵌套使用在当前 Next.js 16.1.6 中尚未明确属于反模式,但会导致缓存语义混乱——react.cache()"use cache" 的边界内可能失去意义。建议改为:仅使用 "use cache" + cacheTag + cacheLife 管理持久化缓存,移除外层 react.cache() 包装。

3.4 isNewContent 的 IIFE "use cache" 模式(反模式)

博客卡片和详情页中使用了立即执行异步函数(IIFE)来包裹 "use cache"

const isNewContent = await (async () => {
  "use cache";
  try {
    const publishDate = new Date(`${post.publishDate}T${post.publishTime}`);
    const now = new Date();
    const diffHours = (now.getTime() - publishDate.getTime()) / (1000 * 60 * 60);
    return diffHours >= 0 && diffHours < 48;
  } catch {
    return false;
  }
})();

问题

  1. 这个缓存块内调用了 new Date()——意味着 now 被缓存后,"48 小时内"的判断会长期冻结在首次缓存时的时间点
  2. IIFE 形式缺少 cacheLife 控制,默认缓存时间不确定
  3. 这是一个简单的日期比较,完全不需要缓存——应该在客户端计算,或在 Server Component 中直接计算并设置短缓存

建议:将 isNewContent 逻辑移到不使用 "use cache" 的 Server Component 层,或提取为客户端组件。

3.5 Frontmatter 手动 YAML 解析(脆弱)

lib/blog.tsloadPostMdxMetadata 使用自写正则解析 YAML frontmatter:

for (const line of yamlStr.split("\n")) {
  const colonIndex = line.indexOf(":");
  if (colonIndex > 0) {
    const key = line.slice(0, colonIndex).trim();
    let value = line.slice(colonIndex + 1).trim();
    // ...
  }
}

问题

  1. 不支持多行值、嵌套对象、数组语法
  2. 不支持 YAML 的引号转义规则
  3. remark-frontmatter + remark-mdx-frontmatter 已在 next.config.ts 中配置了插件,MDX 编译后 frontmatter 会作为 export const frontmatter 导出——但代码注释说明"因 Next.js 16 构建序列化问题无法使用",导致了这个手动解析的回退方案

建议:如果插件确实无法工作,应引入 yamlgray-matter 包做正规解析,而非自写正则。

3.6 unoptimized 图片标志滥用

博客卡片中使用了 unoptimized 属性:

<Image
  src={post.imageSrc || `https://picsum.photos/seed/${post.slug}/800/450`}
  unoptimized // ← 禁用了 Next.js 图片优化
  className="h-full w-full object-cover"
/>

unoptimized 直接跳过了 Next.js Image Optimization Pipeline(不生成 WebP/AVIF、不适配设备宽度、不做质量调整)。对于营销网站,图片加载性能直接影响 Core Web Vitals (LCP)。

如果是因为 picsum.photos 的 URL 问题,应在 next.config.tsremotePatterns 中配置后移除 unoptimized。当前 remotePatterns 已包含 picsum.photos,所以 unoptimized 没有存在的必要。

3.7 CSP 安全头部配置过于宽松

proxy.ts 中的 CSP 配置:

script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';

'unsafe-eval' 允许 eval() 执行,'unsafe-inline' 允许内联脚本——这两个指令基本完全绕过了 CSP 对 XSS 的防护。如果确实需要内联脚本(如 next-themes 的初始化脚本),应使用 nonce 机制替代。

3.8 i18n 硬编码中文文本

app/[locale]/services/page.tsx 中多处直接写入中文文案,未使用 i18n:

<h2>管理平台: BDI-Platform</h2>
<p>高度集成的经营管理系统...</p>
<h2>整体方案: <span>BDI-Solution</span></h2>
<h1>商业数据智能整体解决方案</h1>

这意味着英文访客看到的也是中文。所有面向用户的文本必须通过 useTranslations()getTranslations() 输出。

3.9 孤立组件(Dead Code)

以下组件已定义但未被任何页面引用:

组件文件状态
CapabilitySectioncomponents/capability-section.tsx未使用
FeatureSectioncomponents/feature-section.tsx未使用
LanguageSwitcher(Blog 版)components/blog/language-switcher.tsx未使用
LanguageIndicatorcomponents/blog/language-indicator.tsx未使用

未使用的代码增加维护负担且干扰分析。建议删除或在对应页面中集成。

3.10 rehype-sanitize 依赖未使用

package.json 中声明了 rehype-sanitize: ^6.0.0,但 next.config.ts 的 MDX 插件链中并未启用(且注释说明了不启用的原因)。同时,lib/remark-sanitize.ts 是一个自定义 remark 插件,但也未注册到 MDX 插件链中

这意味着 MDX 内容实际上零消毒处理。虽然本地 MDX 是受信任内容,但 remark-sanitize.ts 的存在暗示过去曾考虑过安全需求——如果不用,应删除依赖和文件。


四、项目结构评估

4.1 当前结构

app/
  globals.css
  favicon.ico
  [locale]/
    layout.tsx          # 根布局(含 html/body)
    loading.tsx
    page.tsx            # 首页
    blog/
      page.tsx          # 博客列表
      [slug]/
        page.tsx        # 博客详情
        loading.tsx
    services/
      page.tsx          # 服务详情
components/             # 全局组件
  blog/                 # 博客专用组件
  mdx/                  # MDX 专用组件
  ui/                   # shadcn/ui 组件
content/blog/           # MDX 内容
  zh/
  en/
i18n/                   # 国际化配置
lib/                    # 工具函数与数据层
  actions/              # Server Actions
messages/               # i18n 翻译文件

4.2 结构问题

问题 1:blog 缺少独立 layout

blog/ 目录没有自己的 layout.tsx。博客列表和详情页共享 [locale]/layout.tsx 全局布局,无法为博客模块添加专属的侧边导航、面包屑容器或阅读进度条。建议创建 app/[locale]/blog/layout.tsx

问题 2:services 页面过于庞大

app/[locale]/services/page.tsx 有 395 行代码,内含 5 个局部组件(ServicesHero, SolutionDetailed, PlatformDetailed, CapabilityDetailed, ServiceValueDetailed)。这些应拆分为独立文件:

app/[locale]/services/
  page.tsx                          # 仅做组合编排
  _components/
    services-hero.tsx
    solution-detailed.tsx
    platform-detailed.tsx
    capability-detailed.tsx
    service-value-detailed.tsx

Next.js 官方文档推荐使用 _components 私有文件夹来放置路由段内的共置组件。

问题 3:lib/blog.ts 职责过重

lib/blog.ts 承担了文件系统读取、YAML 解析、元数据标准化、MDX 动态加载、缓存策略管理、排序逻辑等 6+ 种职责,264 行难以维护。建议拆分为:

  • lib/blog/index.ts — 公共接口(re-export)
  • lib/blog/reader.ts — 文件系统读取
  • lib/blog/parser.ts — Frontmatter 解析
  • lib/blog/cache.ts — 缓存策略

五、代码质量逐项分析

5.1 mdx-components.tsx — 过度复杂的链接处理

a 标签覆盖中的文件路径/代码引用检测有 3 种正则模式 + 多条件判断(约 30 行):

const filePathPattern = /\.(ts|tsx|js|jsx|mjs|json|md|mdx|css|...)(#L\d+(-L\d+)?)?$/;
const codeRefPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$]...)*(\(\))?$|^[a-z-]+:[a-z-]+$/;
const pathPattern = /[/\\]/;

这段逻辑试图区分"真实链接"和"代码/文件引用",但规则脆弱且不完备。例如 utils.ts 这样的文本会被误判为文件引用而渲染为 <code> 而非链接。

建议简化:MDX 中的代码引用由作者使用行内代码语法(`)明确标记,链接组件不应做猜测性推断。

5.2 mdx-components.tsx — 多处 @ts-expect-error

// @ts-expect-error - node.props might not be typed
return getText(node.props.children);
// @ts-expect-error
children: removeIdTag(node.props.children),

这表明 processHeading 的递归 React 节点处理缺少正确的类型定义。应使用 React.ReactElement 的类型守卫来安全地访问 props.children

5.3 lib/actions/blog.ts — XSS 消毒不充分

评论提交的消毒逻辑仅为:

const sanitizedContent = content.replace(/<[^>]*>?/gm, "");

这个简单正则无法防御:

  • 不完整的标签如 <img src=x onerror=alert(1)(缺少闭合 >
  • 编码绕过如 &#60;script&#62;
  • CSS 注入,Markdown 注入等

对于用户输入内容,建议使用 DOMPurify(通过 jsdom)或 sanitize-html 做服务端消毒。

5.4 globals.css — 暗色模式颜色格式不一致

亮色模式全部使用 OKLCH:

--background: oklch(1 0 0);
--primary: oklch(0.48 0.14 260);

暗色模式部分使用十六进制:

--background: #12121a; /* ← 不一致 */
--card: #12121a;

应统一为 OKLCH 格式,确保色彩空间一致性。

5.5 next.config.tsexperimental.mdxRs: false 多余

experimental: {
  mdxRs: false,
},

mdxRs 默认就是 false,显式设置是多余的。但更关键的是:experimental 块的其他选项如 ppr 在 Next.js 16 中已被 cacheComponents 取代,不应出现 experimental 空壳。

5.6 Blog 详情页 — useMDXComponents 在 Server Component 中调用

// app/[locale]/blog/[slug]/page.tsx
const mdxComponents = useMDXComponents({});
return <MDXContent components={mdxComponents} />;

useMDXComponents 是一个函数(非 React Hook),在 Server Component 中调用是安全的。但命名以 use 开头容易误导为 Hook,且这种模式意味着每次渲染都会创建新的组件映射对象。建议在模块级别创建一次:

const mdxComponents = useMDXComponents({});
 
export default async function BlogPostPage({ params }) {
  // ...
  return <MDXContent components={mdxComponents} />;
}

六、性能与最佳实践

6.1 Turbopack 兼容性

next.config.ts 中 MDX 插件使用字符串格式引用(如 "remark-gfm" 而非 import remarkGfm),符合 Turbopack 要求——Turbopack 不支持传递 JS 函数作为插件参数。但 rehype-autolink-headingscontent 配置传递了复杂的 HAST 对象,需确认这在 Turbopack 下是否正常工作。

6.2 generateStaticParams 配置正确

[locale]/layout.tsx[slug]/page.tsx 都正确实现了 generateStaticParams,配合 cacheComponents: true 可实现增量静态生成。但博客详情页的 generateStaticParams 使用了 "use cache" 包裹:

export async function generateStaticParams() {
  "use cache";
  cacheLife("days");
  // ...
}

generateStaticParams 是构建时函数,"use cache" 在这个上下文中的行为需要验证——它主要面向运行时渲染函数。

6.3 缺少 Suspense 边界策略

首页渲染 6 个大型 Section 组件(HeroSection, ServiceOverviewSection, PlatformSection, ValueSection, CharacteristicsSection, PhilosophySection),全部在主 page.tsx 中同步渲染。建议对非首屏 Section 使用 Suspense + 懒加载,改善 FCP。

6.4 缺少 .env 环境变量管理

项目没有 .env 文件(或 .env.example),NEXT_PUBLIC_APP_URL 在代码中频繁使用但无定义,硬编码回退为 "https://bdi.business"。应创建 .env.example 文件明确需要配置的环境变量。


七、安全性总结

风险项严重度当前状态建议
SQL 模板字符串参数有限但模式危险改用分支预编译语句
CSP unsafe-eval/inline实质性绕过 CSP使用 nonce 机制
XSS 输入消毒简单正则不可靠使用 DOMPurify 等专业库
MDX 内容零消毒本地内容受信任可接受,但应删除未用的 sanitize 依赖
无 CSRF 防护Server Actions 自带Next.js 16 Server Actions 内置 CSRF token

八、待发现的尾部问题

8.1 @types/hast 是否多余?

@types/hastdependencies(而非 devDependencies)中,且没有在项目源码中直接导入。如果仅被 rehype 插件间接使用,应移到 devDependencies 或移除。

8.2 server-only 包的使用

lib/blog.tslib/mdx-utils.ts 顶部引入了 import "server-only",这是正确的做法——防止这些模块被意外引入客户端 bundle。但 lib/db.ts 也应添加此导入。

8.3 Loading 骨架屏缺少暗色适配

app/[locale]/loading.tsx 使用硬编码的浅色背景(bg-slate-50, bg-slate-100),在暗色模式下违和。应添加 dark:bg-slate-900 等。

8.4 评论组件安全改进

CommentList 直接渲染用户输入的 comment.content,虽然提交时做了基础消毒,但应在渲染层做二次转义(dangerouslySetInnerHTML 未使用,React 默认转义文本节点,此风险较低但值得记录)。


九、改进建议优先级

优先级改进项工作量
P0添加 robots.ts + sitemap.ts0.5h
P0添加 not-found.tsx + error.tsx + global-error.tsx1h
P1修复 services 页硬编码中文2h
P1SQL 注入风险消除0.5h
P1移除 unoptimized 图片标志0.5h
P2清理 react.cache 与 "use cache" 混用1h
P2统一暗色模式 OKLCH 颜色0.5h
P2services 页面拆分子组件1h
P2blog.ts 职责拆分2h
P3CSP 安全头部改用 nonce2h
P3删除 Dead Code 组件0.5h
P3骨架屏暗色模式适配0.5h
P3创建 .env.example0.5h

十、总结

项目在技术选型上做出了正确的决策,整体架构符合 Next.js v16 的最新范式。cacheComponentsreactCompilerproxy.ts、异步 params 等 v16 关键特性均已正确采用。MDX 博客系统的构建时编译策略、i18n 配置、Turbopack 兼容的插件字符串引用方式等细节体现了对前沿技术的深入理解。

主要短板集中在三个方面:SEO 基础设施缺失(robots、sitemap、OG image)、安全实践不完整(CSP 过松、SQL 拼接、XSS 消毒粗糙)、代码组织需优化(services 页过大、blog.ts 职责过重、Dead Code 未清理)。这些问题在当前开发阶段是常见的 debt,逐项修复后项目将达到生产就绪水平。


  • 版本: 1.00.001
  • 时间: 2026-02-07 14:30:00
  • 作者: Claude Opus 4.6
  • 内容: 基于全量源码审阅,完成技术栈评估、代码质量分析、安全审计、结构评估与改进建议的首次完整复盘