重構技能 是做什麼的?
當重構功能以與既有模式對齊、消除重複並提升程式碼重用性時,使用此技能。
關於 UI 應屬於 pattern-library 還是區域組合,請使用 design-system 技能。
重構前分析
1. 識別相似功能
- 在程式碼庫中尋找功能相似的功能
- 比較目錄結構、元件模式與工具函數使用方式
- 尋找功能相似但實作不同的功能
- 使用語意搜尋尋找相關功能,例如:「具有相似表格/DataGrid 模式的功能」
2. 尋找重複機會
搜尋重複的程式碼模式:
- 元件:尋找跨功能相似的元件檔案(相同名稱或相似功能)
- Hooks:檢查重複的 hook 邏輯(表格 hooks、篩選 hooks、聚合 hooks)
- 工具函數:尋找重複的工具函數(聚合、格式化、驗證、轉換)
- 樣式:識別重複的
sxprop 模式或行內樣式 - 常數:檢查重複的常數或設定物件
- 型別:尋找重複的型別定義或介面
3. 檢查共享工具函數
在建立新的工具函數之前,先驗證既有的工具函數:
- 搜尋
@/util/目錄下的領域特定工具函數 - 檢查
@/util/dates中的日期格式化函數 - 檢查
@/util/numbers中的數字格式化函數 - 檢查
@/components/中的可重用元件 - 檢查
pattern-library中的設計系統元件 - 使用 grep/搜尋在建立新的實作前先找到既有的實作
重構流程
步驟 1:提取共享程式碼
發現重複時:
-
在適當的
@/util/目錄下建立共享工具函數:// 範例:util/[domain]/[category].ts export const sharedFunction = () => { ... }; export const sharedConstant = { ... }; -
更新所有功能,使其從共享位置匯入:
// 功能特定檔案 - 重新匯出共享 export { sharedFunction, sharedConstant } from "@/util/[domain]/[category]"; -
從功能目錄中移除重複的實作
-
更新整個程式碼庫中所有的匯入
步驟 2:合併元件
發現重複元件時:
- 識別最完整/功能最豐富的版本(檢查 props、功能、測試)
- 提取到共享位置(
@/components/或功能無關的位置) - 讓 props 更具彈性以支援所有使用案例(使用聯集型別、可選 props 或泛型)
- 更新所有使用該元件的功能中的匯入
- 刪除重複的元件檔案
- 驗證所有使用方式仍然正常運作
步驟 3:對齊載入模式
針對載入狀態:
- 檢查
loading.tsx檔案 - 應該是簡單的骨架螢幕,而非完整的頁面佈局 - 移除元件層級的骨架載入器 - 依賴頁面層級的
loading.tsx來處理初始載入 - 重新獲取期間:顯示既有資料,而非骨架(除非 UX 明確需要)
- 比對程式碼庫中相似功能的模式
- 使用 Next.js App Router 慣例 -
loading.tsx處理初始載入,元件處理重新獲取狀態
步驟 4:合併樣式
針對樣式:
- 將可重用的樣式模式提取到共享元件或樣式工具函數
- 當共享元件存在時,移除行內
sxprops - 使用
pattern-library中的設計系統元件取代自訂樣式 - 檢查可以成為共享工具函數或主題擴展的重複樣式物件
- 遵循 Material-UI 模式 - 使用主題值,避免魔術數字
步驟 5:驗證模式一致性
確保與既有功能一致:
- 比較目錄結構 - 應遵循相似的模式(
_components/、_hooks/、_utils/) - 檢查 hook 模式 - 如果功能需求不同,較簡單的 hook 也可以接受
- 驗證資料流 - 應在可能的情況下與相似功能匹配
- 檢視元件組合 - 應使用相似的組合模式(封裝、組合等)
- 檢查命名慣例 - 遵循既有模式(PascalCase 用於元件,camelCase 用於 hooks)
步驟 6:工具函數合併
針對工具函數:
- 先檢查函數是否已存在於共享工具函數中(搜尋程式碼庫)
- 比較實作 - 使用較完整/穩健的版本
- 合併重複項 - 移動到共享位置,更新所有匯入
- 記錄差異 - 如果實作有意的不同,記錄原因
- 保留功能 - 確保合併後的版本能處理所有使用案例
步驟 7:清理註解
重構後,移除不必要的註解:
- 移除遷移備註 - 解釋程式碼從哪裡移來的註解(例如「isCollecting 現在從 Table/utils.ts 匯出」)
- 移除顯而易見的註解 - 只是重述程式碼功能的註解(例如「// 重新匯出共享聚合」,因為程式碼明顯顯示重新匯出)
- 保留解釋性註解 - 解釋 WHY(為什麼)的註解(例如「// 使用 setTimeout 以確保表單狀態已更新」)
- 保留複雜邏輯註解 - 解釋不明顯的商業邏輯或邊界狀況的註解
- 移除 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。 TypographyAPI:在表達語意的地方使用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-serviceaction 文件 / Bruno 以取得最大limit)。 - 如果呼叫端需要計數,一致地提供
total或推導出的總數。
呈現封裝:不要重複顯而易見的內容
- 當上層
Card已經設定了可見標題時,移除多餘的替代狀態欄位(例如避免重複與卡片標題匹配的cardContentTitle)。 - Null 檢查:如果 prop 的型別是
string,就不要進行g的 null 檢查。
(由於原始內容被截斷,此處的翻譯也有所截斷。)