最近我在维护一个 Tauri + React + Rust 的桌面项目时,集中做了一轮前端逻辑重构。这个项目不是 demo,而是一个已经有不少真实功能的应用:搜索、剪贴板、收藏、外部文件、设置页、会话记录、笔记、计划、Claude/Cursor 配置等页面都已经跑在生产代码里。

这类项目发展到一定阶段后,最容易出现的问题不是“代码不会跑”,而是“代码越来越难判断是否还对”。

尤其是在 React 页面里,经常会看到这样的代码:

const showList = !isMobile || !selectedFile;
const showDetail = !isMobile || !!selectedFile;

if (isMobile && trimmed && !remoteTokenInput.trim() && !gitRemote) {
  showToast(t("settings.remoteTokenRequired"), true);
  return;
}

const selectedCanDelete = selectedFile
  ? sourceType === "note" || sourceType === "clip" || (isDesktop && sourceType === "conversation")
  : false;

这些代码单独看都不复杂,但当它们散落在十几个页面、几十个事件处理函数、多个 hook 和共享组件里时,维护成本会迅速上升。你每改一个规则,都要在脑子里搜索:还有哪里也写了同样的判断?移动端有没有另一套?删除后应该选中哪一个文件?这个按钮什么时候显示,什么时候禁用?

这次重构我使用了一个专门处理这类问题的 Codex Skill:

它的中文名可以理解为:语义逻辑建模 Skill

核心原则很简单:

不要把业务逻辑藏在裸表达式里。先命名,再复用,再测试,再组合。

它解决的不是“代码风格”,而是“逻辑失控”

很多重构建议会停留在“拆组件”“抽 hook”“减少重复代码”。这些当然有用,但它们经常没有触及真正的问题。

真正的问题是:业务判断没有名字

比如下面这类表达式:

isMobile && trimmed && !remoteTokenInput.trim() && !gitRemote

它到底是什么意思?

从语法上看,它只是几个布尔条件相与。从业务上看,它其实是在表达:

移动端首次配置远端仓库时,如果用户填写了远端地址但没有填写 access token,就不能保存。

如果这个判断只出现一次,也许还能接受。但一旦它和保存逻辑、提示文案、按钮状态、远端检测流程交织在一起,页面代码就会慢慢变成“布尔表达式迷宫”。

Semantic Logic Modeling Skill 的方法不是让你为了抽函数而抽函数,而是要求你按语义层级建模:

  1. 识别外部输入:API、数据库、配置、路由、表单、运行时状态。
  2. 抽取原子语义函数:每个函数只回答一个最小业务问题。
  3. 组合派生语义函数:把多个原子判断组合成中间含义。
  4. 命名复合场景函数:让分支读起来像业务语言。
  5. 输出受控结果函数:按钮状态、流程路由、提示文案、动作开关都由命名函数产生。
  6. 最后再接入 React、Controller、Hook 或其他框架代码。

换句话说,它不是“把 if 搬到另一个文件”,而是把条件逻辑变成一个可以被阅读、复用和测试的模型。

一次真实重构:从页面内判断,到 domain logic

这次我对 GitMemo Desktop 的剩余前端主要页面做了一轮集中重构。最终提交里,涉及 28 个前端源码文件,新增了一批按业务域划分的 logic 模块,例如:

desktop/src/components/domain/settings/settingsLogic.ts
desktop/src/components/domain/search/searchLogic.ts
desktop/src/components/domain/clipboard/clipboardLogic.ts
desktop/src/components/domain/conversations/conversationsLogic.ts
desktop/src/components/domain/editor-home/editorHomeLogic.ts
desktop/src/components/domain/external-files/externalFilesLogic.ts
desktop/src/components/domain/favorites/favoritesLogic.ts
desktop/src/components/domain/files/fileActionsLogic.ts
desktop/src/components/domain/files/fileWorkspaceLogic.ts
desktop/src/components/domain/notes/notesLogic.ts
desktop/src/components/domain/claude-config/claudeConfigLogic.ts
desktop/src/utils/platformLogic.ts

重构前,很多页面同时承担四件事:

  • 调用 Tauri/Rust 命令读取数据;
  • 管理 React state;
  • 判断当前业务状态;
  • 渲染 UI。

重构后,页面仍然负责状态连接和副作用,但大量“条件到结果”的逻辑被移动到纯函数里。

例如设置页里,远端仓库保存逻辑原本是直接写在事件处理函数中的:

const trimmed = remoteInput.trim();
const hasNewMobileToken = isMobile && !!remoteTokenInput.trim();

if (trimmed === gitRemote && !hasNewMobileToken) {
  setEditingRemote(false);
  setRemoteTokenInput("");
  return;
}

if (isMobile && trimmed && !remoteTokenInput.trim() && !gitRemote) {
  showToast(t("settings.remoteTokenRequired"), true);
  return;
}

重构后,页面调用一个语义明确的决策函数:

const decision = getRemoteSaveDecision({
  isMobile,
  remoteInput,
  remoteTokenInput,
  currentRemote: gitRemote,
});

if (decision.kind === "unchanged") {
  setEditingRemote(false);
  setRemoteTokenInput("");
  return;
}

if (decision.kind === "missing_mobile_token") {
  showToast(t("settings.remoteTokenRequired"), true);
  return;
}

对应的业务判断集中在 settingsLogic.ts

export function getRemoteSaveDecision(input: {
  isMobile: boolean;
  remoteInput: string;
  remoteTokenInput: string;
  currentRemote: string;
}) {
  const url = input.remoteInput.trim();
  const token = input.remoteTokenInput.trim();
  const hasNewMobileToken = input.isMobile && Boolean(token);

  if (url === input.currentRemote && !hasNewMobileToken) {
    return { kind: "unchanged", url, accessToken: null };
  }

  if (input.isMobile && url && !token && !input.currentRemote) {
    return { kind: "missing_mobile_token", url, accessToken: null };
  }

  return {
    kind: "save",
    url,
    accessToken: input.isMobile ? (token || null) : null,
  };
}

这个变化看似只是多了一个函数,但维护方式完全不同了。

以前读代码时,你需要自己翻译布尔表达式。现在读函数名就能知道业务意图:这是一个“远端保存决策”。而且它的输出被限制为 unchangedmissing_mobile_tokensave 三种受控结果,页面只需要消费结果。

React 页面最应该瘦身的,是 UI 派生状态

React 里很容易出现一种代码味道:JSX 看起来很正常,但每个 props 后面都藏着一段条件判断。

例如:

showList={!isMobile || !selectedFile}
showDetail={!isMobile || !!selectedFile}
disabled={disabled || !hasTarget || loading}
hidden={editing || !selectedCanDelete}

这些判断本质上都是 UI 派生状态。它们不属于 DOM,也不属于组件样式,而属于业务语义。

这次重构里,我把这类判断抽成了类似下面的函数:

export function getFileWorkspacePaneState(isMobile: boolean, selectedFile: string | null) {
  const hasDetail = selectedFile !== null;
  return {
    hasDetail,
    showList: !isMobile || !hasDetail,
    showDetail: !isMobile || hasDetail,
  };
}

页面里就变成:

const { showList, showDetail } = getFileWorkspacePaneState(isMobile, selectedFile);

这类函数非常小,但价值很大。因为它把“移动端是否显示列表/详情”的规则从页面细节里拿出来了。以后如果移动端交互规则改变,不需要挨个页面找 !isMobile || ...

同样,收藏按钮和更多菜单也被抽出了共享动作逻辑:

export function shouldEnableFavoriteShortcut(input: {
  active: boolean;
  isMobile: boolean;
  disabled?: boolean;
  hasTarget: boolean;
  loading: boolean;
}) {
  return input.active && !input.isMobile && !input.disabled && input.hasTarget && !input.loading;
}

这比在组件里反复写:

if (!active || isMobile || disabled || !hasTarget || loading) return;

更容易维护,也更容易测试。

会话解析:最典型的“业务逻辑不该留在组件里”

这次重构里,我最有感触的是 ConversationsPage

原本页面里直接包含 Markdown frontmatter 解析、消息段落解析、删除后选中下一条记录、列表计数展示等逻辑。它们都和 UI 有关,但并不是 UI 本身。

重构后,会话相关逻辑被收进:

desktop/src/components/domain/conversations/conversationsLogic.ts

例如:

export function parseConversationMarkdown(raw: string) {
  const { meta, body } = parseConversationFrontmatter(raw);
  const parsedBody = parseConversationBody(body);
  return {
    meta,
    body,
    intro: parsedBody.intro,
    messages: parsedBody.messages,
  };
}

页面只负责调用:

const parsed = parseConversationMarkdown(raw);
setCurrentMeta(parsed.meta);
setRawBody(parsed.body);
setIntroContent(parsed.intro);
setMessages(parsed.messages);

这个变化让页面代码的职责更接近“读取文件后更新状态”,而不是“读取文件、解析 Markdown、理解消息结构、再更新状态”。

这也是 Semantic Logic Modeling Skill 很强调的一点:框架代码应该消费语义逻辑,而不是承载语义逻辑。

这类 Skill 和普通提示词有什么区别?

很多人会问:这不就是让 AI “帮我重构一下”吗?

差别很大。

普通提示词容易得到局部优化:AI 可能会帮你拆几个函数、整理几个组件,但它不一定有稳定的方法论。今天这样拆,明天那样拆,最后还是容易变成另一种风格混乱。

Skill 的价值在于给 AI 一个稳定的操作框架。

Semantic Logic Modeling Skill 会持续约束 AI:

  • 不要把业务逻辑藏在裸表达式里;
  • 重复判断必须抽成共享纯函数;
  • 原子判断、派生判断、复合场景、受控结果要分层;
  • React 中先抽纯函数,再考虑 hook,最后组件只负责渲染;
  • 高风险或多分支逻辑优先补测试。

这就像给 AI 配了一套“代码审美和重构准则”。你不是每次都重新解释你的偏好,而是让它默认按同一套模型工作。

适合使用它的场景

我个人认为,下面这些场景非常适合使用这个 Skill:

  • 权限判断:角色、订阅、组织、资源归属共同决定是否可操作;
  • 表单校验:字段之间存在依赖关系,不只是单字段 required;
  • 功能开关:平台、版本、灰度、配置共同决定功能是否显示;
  • 流程路由:不同状态进入不同下一步;
  • 搜索/筛选:类型、平台、来源共同决定结果展示;
  • UI 状态:按钮显隐、禁用、详情页/列表页切换;
  • 删除/保存后行为:删除后选中谁,保存后刷新哪里;
  • 价格/计费/额度:多个条件共同决定最终价格或可用额度。

一句话:只要你的代码里存在大量“条件决定结果”,它就有用。

安装和使用

推荐通过 Codex plugin marketplace 安装:

codex plugin marketplace add sahadev/semantic-logic-modeling-skill --ref main
codex plugin add semantic-logic-modeling-skill@semantic-logic-modeling

也可以直接查看仓库:

使用时可以显式告诉 Codex:

使用 $semantic-logic-modeling 检查这段权限逻辑,并按语义逻辑建模方式重构。

或者:

用 semantic-logic-modeling skill 重构这个 React 页面里的复杂条件判断。

我的实际感受

这次重构之后,我最大的感受是:代码不是“更短了”这么简单,而是“更能解释自己了”。

以前很多判断需要靠人脑翻译:

isMobile && url && !token && !currentRemote

现在变成:

decision.kind === "missing_mobile_token"

以前页面里到处都是:

path.startsWith("clips/")
path.startsWith("plans/")
path.startsWith("notes/")

现在变成:

getSearchSourceTypeFromPath(path)
isPendingPathForFolder(path, "clips/")
getNotesTabForPath(path)

以前 JSX 中散落着按钮是否显示、菜单是否启用、移动端是否进入详情页等判断。现在这些规则有了名字,并且集中在对应的 domain logic 模块里。

这对长期维护非常关键。因为复杂项目最怕的不是一次写错,而是规则改了之后你不知道哪里还藏着同一个判断。

需要注意的边界

当然,它不是银弹。

使用这套方法时,我建议注意三点:

第一,不要为了抽象而抽象。只有当一个判断代表明确业务语义,或者会被复用、测试、演进时,才值得命名。

第二,事件处理和副作用不一定都要抽走。比如 Tauri 调用、toast、state 更新、刷新列表,这些可以留在页面里。Skill 更适合抽取“条件到结果”的部分。

第三,抽出来的逻辑最好逐步补测试。纯函数最大的好处就是容易测试,尤其是设置页、搜索来源、会话解析、权限判断这类多分支逻辑。

结语

AI 编程不应该只停留在“帮我写代码”。更重要的是,让 AI 按稳定的方法论帮助我们治理复杂度。

Semantic Logic Modeling Skill 给我的感觉,就是把“复杂业务逻辑应该怎么写”这件事,变成了一套可以反复执行的工程流程。

如果你的项目里已经出现大量这样的代码:

if (a && !b || c && d) { ... }

或者你的 React 页面越来越像“状态、请求、权限、UI 判断、平台差异”混在一起的集合体,那它值得一试。

项目地址:

https://github.com/sahadev/semantic-logic-modeling-skill

官网:

https://semantic-logic-modeling-skill.vercel.app/

它的核心价值可以用一句话概括:

先命名逻辑,再让它被复用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐