Communitygithub.com

重構技能

引導系統性重構,將功能與既有模式對齊、消除重複、並提升程式碼重用性。包含 DataGrid/pattern-library、型別、fetch 分頁、view-model 與 override-prop 指引、以及 MUI sx-first(避免已棄用的 Box 簡寫樣式 prop)。適合用於重構功能、整合重複程式碼、提取共用工具、或對齊相似功能間的模式。

重構技能 是什麼?

此重構技能專為在基於 Material-UI 與 Next.js 的程式碼庫中進行系統性重構而設計,尤其是在處理 DataGrid、pattern-library 元件與複雜資料獲取模式時。它提供了一種結構化的方法,通過識別相似功能、尋找重複的元件、hooks、工具函數與型別,來將功能與既有的模式對齊。該技能詳述了一個包含 8 個步驟的重構流程:從提取共享程式碼、合併元件、對齊載入與樣式模式,到驗證模式一致性與清理註解。它還包含了一節「程式碼審查強化」(Code Review Hardening),重點整理來自 PR 回饋的常見議題,例如偏好使用 pattern-library 中的 `DetailPanelToggle`、避免使用匿名物件型別、遵循 Typography 優先於 Box 的慣例、以及正確實作分頁獲取邏輯。支援的平台包括使用 MUI 與 Next.js App Router 的 React 應用程式,並遵循 MUI 關於使用 sx prop(而非傳統系統樣式簡寫)的最新建議。此技能在開發人員重構既有功能、減少技術債務並強制採用一致且可維護的模式時特別有用。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/caseycme/skills/tree/HEAD/skills/engineering/refactoring

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

重構技能 是做什麼的?

當重構功能以與既有模式對齊、消除重複並提升程式碼重用性時,使用此技能。

關於 UI 應屬於 pattern-library 還是區域組合,請使用 design-system 技能。

重構前分析

1. 識別相似功能

  • 在程式碼庫中尋找功能相似的功能
  • 比較目錄結構、元件模式與工具函數使用方式
  • 尋找功能相似但實作不同的功能
  • 使用語意搜尋尋找相關功能,例如:「具有相似表格/DataGrid 模式的功能」

2. 尋找重複機會

搜尋重複的程式碼模式:

  • 元件:尋找跨功能相似的元件檔案(相同名稱或相似功能)
  • Hooks:檢查重複的 hook 邏輯(表格 hooks、篩選 hooks、聚合 hooks)
  • 工具函數:尋找重複的工具函數(聚合、格式化、驗證、轉換)
  • 樣式:識別重複的 sx prop 模式或行內樣式
  • 常數:檢查重複的常數或設定物件
  • 型別:尋找重複的型別定義或介面

3. 檢查共享工具函數

在建立新的工具函數之前,先驗證既有的工具函數:

  • 搜尋 @/util/ 目錄下的領域特定工具函數
  • 檢查 @/util/dates 中的日期格式化函數
  • 檢查 @/util/numbers 中的數字格式化函數
  • 檢查 @/components/ 中的可重用元件
  • 檢查 pattern-library 中的設計系統元件
  • 使用 grep/搜尋在建立新的實作前先找到既有的實作

重構流程

步驟 1:提取共享程式碼

發現重複時:

  1. 在適當的 @/util/ 目錄下建立共享工具函數:

    // 範例:util/[domain]/[category].ts
    export const sharedFunction = () => { ... };
    export const sharedConstant = { ... };
    
  2. 更新所有功能,使其從共享位置匯入:

    // 功能特定檔案 - 重新匯出共享
    export { sharedFunction, sharedConstant } from "@/util/[domain]/[category]";
    
  3. 從功能目錄中移除重複的實作

  4. 更新整個程式碼庫中所有的匯入

步驟 2:合併元件

發現重複元件時:

  1. 識別最完整/功能最豐富的版本(檢查 props、功能、測試)
  2. 提取到共享位置(@/components/ 或功能無關的位置)
  3. 讓 props 更具彈性以支援所有使用案例(使用聯集型別、可選 props 或泛型)
  4. 更新所有使用該元件的功能中的匯入
  5. 刪除重複的元件檔案
  6. 驗證所有使用方式仍然正常運作

步驟 3:對齊載入模式

針對載入狀態:

  1. 檢查 loading.tsx 檔案 - 應該是簡單的骨架螢幕,而非完整的頁面佈局
  2. 移除元件層級的骨架載入器 - 依賴頁面層級的 loading.tsx 來處理初始載入
  3. 重新獲取期間:顯示既有資料,而非骨架(除非 UX 明確需要)
  4. 比對程式碼庫中相似功能的模式
  5. 使用 Next.js App Router 慣例 - loading.tsx 處理初始載入,元件處理重新獲取狀態

步驟 4:合併樣式

針對樣式:

  1. 將可重用的樣式模式提取到共享元件或樣式工具函數
  2. 當共享元件存在時,移除行內 sx props
  3. 使用 pattern-library 中的設計系統元件取代自訂樣式
  4. 檢查可以成為共享工具函數或主題擴展的重複樣式物件
  5. 遵循 Material-UI 模式 - 使用主題值,避免魔術數字

步驟 5:驗證模式一致性

確保與既有功能一致:

  1. 比較目錄結構 - 應遵循相似的模式(_components/、_hooks/、_utils/)
  2. 檢查 hook 模式 - 如果功能需求不同,較簡單的 hook 也可以接受
  3. 驗證資料流 - 應在可能的情況下與相似功能匹配
  4. 檢視元件組合 - 應使用相似的組合模式(封裝、組合等)
  5. 檢查命名慣例 - 遵循既有模式(PascalCase 用於元件,camelCase 用於 hooks)

步驟 6:工具函數合併

針對工具函數:

  1. 先檢查函數是否已存在於共享工具函數中(搜尋程式碼庫)
  2. 比較實作 - 使用較完整/穩健的版本
  3. 合併重複項 - 移動到共享位置,更新所有匯入
  4. 記錄差異 - 如果實作有意的不同,記錄原因
  5. 保留功能 - 確保合併後的版本能處理所有使用案例

步驟 7:清理註解

重構後,移除不必要的註解:

  1. 移除遷移備註 - 解釋程式碼從哪裡移來的註解(例如「isCollecting 現在從 Table/utils.ts 匯出」)
  2. 移除顯而易見的註解 - 只是重述程式碼功能的註解(例如「// 重新匯出共享聚合」,因為程式碼明顯顯示重新匯出)
  3. 保留解釋性註解 - 解釋 WHY(為什麼)的註解(例如「// 使用 setTimeout 以確保表單狀態已更新」)
  4. 保留複雜邏輯註解 - 解釋不明顯的商業邏輯或邊界狀況的註解
  5. 移除 TODO/FIXME 註解 - 除非是合理的未來工作項目

不必要的註解範例:

// ❌ 不必要 - 從程式碼明顯可見
// 從 util 重新匯出共享聚合
export { latest, totalRange } from "@/util/keywords/gridAggregations";

// ❌ 不必要 - 遷移備註,不再需要
// isCollecting 現在從 Table/utils.ts 匯出以避免重複
// 從那裡匯入:import { isCollecting } from "../KeywordListDetail/Table/utils";

良好的註解範例:

// ✅ 良好 - 解釋 WHY
// 使用 setTimeout 以確保表單狀態在 CSV 處理後已更新
setTimeout(() => {
  void handleSubmit(onSubmit)();
}, 100);

// ✅ 良好 - 解釋複雜邏輯
// 關鍵字在以下情況被視為已更新:
// 1. 其 lastUpdated 值發生變更(最可靠的指標),或者
// 2. 它不再處於 collecting 狀態,且我們已輪詢足夠次數(備援方式)

步驟 8:程式碼品質改善

在重構後的程式碼中應用一致的程式碼品質模式:

Null/Undefined 檢查

使用寬鬆相等簡化 null 和 undefined 檢查:

// ❌ 冗長
if (value !== undefined && value !== null) { ... }
if (value !== undefined && value !== null && value !== "") { ... }

// ✅ 簡化
if (value != null) { ... }
if (value != null && value !== "") { ... }

注意:使用 != null(寬鬆相等)在一次檢查中同時檢查 null 和 undefined。這是慣用的 TypeScript/JavaScript,並且更簡潔。

僅型別匯入

對僅型別的匯入使用 import type,以改善建置效能並明確意圖:

// ❌ 僅用於型別的執行時期匯入
import { SomeType } from "app-api/types/domain";

// ✅ 僅型別匯入
import type { SomeType } from "app-api/types/domain";

適用於:

  • 來自 app-api 的型別定義
  • 介面匯入
  • 僅型別的工具型別
  • 僅用於型別註解的任何匯入

移除不必要的型別斷言

當 TypeScript 可以正確推斷型別時,移除型別斷言:

// ❌ 不必要的斷言
const items = data.map((item) => item as SomeType);

// ✅ 讓 TypeScript 推斷
const items = data.map((item) => item);

何時保留斷言:僅在 TypeScript 確實無法推斷型別時,或以 TypeScript 無法驗證的方式縮小型別時。

移除不必要的 Nullish Coalescing

當值已可為 null 或 TypeScript 推斷出正確型別時,移除 ?? null:

// ❌ 不必要 - valueGetter 已回傳可為 null 的值
valueGetter: (value, row) => {
  return row.someField ?? null;
};

// ✅ 簡化
valueGetter: (value, row) => {
  return row.someField;
};

何時保留:當為了 API 相容性明確將 undefined 轉換為 null,或型別系統要求時。

移除未使用的 Props

從元件介面和實作中移除未使用的 props:

// ❌ 未使用的 prop
interface ComponentProps {
  item: Item;
  unusedField: string; // 從未在元件中使用
}

// ✅ 乾淨的介面
interface ComponentProps {
  item: Item;
}

為回呼函數加入明確型別

當 TypeScript 無法推斷回呼參數的型別時,加入明確的型別註解:

// ❌ 隱含 any
const items = ids.map((id) => { ... });

// ✅ 明確型別
const items = ids.map((id: string | number) => { ... });

適用於:陣列方法(map、filter、forEach)、事件處理器,以及其他型別從上下文不明確的回呼函數。

提取重複的 JSX 模式

將重複的 JSX 模式提取為可重用的元件:

// ❌ 重複的模式
{
  isAggregation ? (
    <Typography
      variant="body2"
      fontWeight="medium"
      fontStyle="italic"
      color="text.secondary"
    >
      {formattedValue}
    </Typography>
  ) : (
    <Typography variant="body2" fontWeight="medium">
      {formattedValue}
    </Typography>
  );
}

// ✅ 提取的元件
<AggregationTypography
  formattedValue={formattedValue}
  isAggregation={isAggregation}
/>;

對不同實作使用工廠函數

當邏輯相似但需要處理不同的資料結構時,使用工廠函數:

// ✅ 工廠函數模式
export const createIntentOperators = <T extends { intent?: Intent | Intent[] }>(
  normalizeToArray: (value: T) => Intent[]
) => {
  return {
    // 對正規化後的陣列進行運算的操作
  };
};

// 在不同功能中使用
const operators1 = createIntentOperators<Feature1Type>((item) => [item.intent]);
const operators2 = createIntentOperators<Feature2Type>(
  (item) => item.intents || []
);

優點:重用核心邏輯,同時允許不同的資料結構被適當地處理。

程式碼審查強化(反覆出現的回饋)

在重構觸及 grids、dashboards、fetches 或共享「列 + 指標」形狀時應用這些規則(與重複的 Moz Pro UI PR 回饋一致)。

DataGrid:詳細面板切換

  • 偏好使用 pattern-library 中的 DetailPanelToggle 來處理詳細面板列的展開/折疊,而不是在自訂儲存格中使用臨時 IconButton + @mui/icons。
  • 設定無障礙功能:當儲存格應可透過 Tab 聚焦時,傳入 buttonTabIndex={0}(請參閱 app/(default)/competitive-research/top-competing-content/_module/components/Table/columns.tsx、SerpFeaturesCell)。
  • 佈局/置中:在 Box/Stack 上使用 sx(或使用 Stack 的 direction / spacing,當這是既定模式時)。不要將 sx 替換為 Box 上傳統的 MUI 系統樣式 props(例如 display、alignItems 作為頂層 props)——MUI 正棄用該方法,改為支援 sx。

型別:避免 object / 匿名列形狀

  • 不要對 brand_definitions、列片段或 API 鄰接列表使用 readonly object[],當存在結構型別或可以定義一次時。
  • 定義一個共享的「類似」介面(例如 BrandDefinitionLike)在功能 utils 模組中;匯出它和一個單一的顯示行輔助函數(例如 getBrandDefinitionDisplayLine),而不是在儲存格內部重複帶有轉換的 getBrandName。
  • 在參數型別化後移除多餘的轉換(as BrandDefinitionLike[])。
  • 重用來自 *_module/utils/dataHelpers 的領域輔助函數,當形狀是真正的 BrandDefinition 時;僅在 prompt 列載荷不同時使用較寬鬆的輔助函數。

Typography 與 Box(pattern-library)

  • 對於文字(空狀態、行內「—」、強調的 span),偏好使用 pattern-library 中的 Typography,並在適當的時候使用 component="span"——而不是假裝是文字的 Box。
  • Typography API:在表達語意的地方使用 variant、color、fontWeight、component(這些並未被棄用)。
  • Box / 佈局封裝:對間距、彈性佈局、尺寸和主題值偏好使用 sx。避免進行驅動型重構,將樣式從 sx 移動到 Box 的「樣式」簡寫 props;這與當前的 MUI 指引相衝突。

元件邊界:props 與內部狀態

  • 從元件的 data 內部推導布林值,當上層元件不需要它們來進行佈局或 URL 狀態時(例如從主要品牌的 brand_terms 推導「有品牌詞彙」)。
  • 將僅限 UI 的開關保持在內部(例如在一個卡片中切換兩種圖表模式),除非另一個兄弟元件或路由必須驅動相同的狀態——那麼記錄為什麼需要提升。

控制流程與 JSX

  • 偏好使用早期 return 來處理載入/空分支,而不是在變數中賦值一個大型三元表達式然後立即 return,除非記憶化確實需要一個表示式。
  • 排序比較器:永遠不要硬編碼一個與假設的領域最大值相關的魔術哨兵值(例如 5);對 null/undefined 使用 Number.POSITIVE_INFINITY / 明確的「最後排序」,以便在限制變更時行為保持正確。

Fetches 與分頁

  • 避免對儀表板規模的列表使用固定的較小 limit(例如 100),當後端有分頁且專案可能超過一頁時。
  • 使用穩定的頁面大小進行迴圈,直到一頁回傳少於限制的行數或零行,或者遵循 action 的文件化合約(請參閱 moz-pro-service action 文件 / Bruno 以取得最大 limit)。
  • 如果呼叫端需要計數,一致地提供 total 或推導出的總數。

呈現封裝:不要重複顯而易見的內容

  • 當上層 Card 已經設定了可見標題時,移除多餘的替代狀態欄位(例如避免重複與卡片標題匹配的 cardContentTitle)。
  • Null 檢查:如果 prop 的型別是 string,就不要進行 g 的 null 檢查。

(由於原始內容被截斷,此處的翻譯也有所截斷。)

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

相關技能