免責聲明:本故事主要講述我的經歷。為了流程和參與度,我跳過了詳細的程式碼解釋和一些技術細節。這裡的目標是分享旅程,而不是提供深入的技術細分。
每個開發人員都會有這樣的時刻——他們以為自己掌握了一些東西,但卻被同事徹底打敗了。這就是我此刻的故事。它始於一個簡單的目標:改善 React 專案中的路由。最終我開始質疑自己身為開發人員的整個存在。 😅
接下來的旅程充滿了興奮、沮喪、純粹由多巴胺推動的突破時刻,最終,對經驗、技能以及成為高級開發人員的真正意義有了令人大開眼界的認識。係好安全帶。 🚀
這又是發展的一天,又是覺得自己像個專業人士的一天。我正在開發一個 React 專案,使用createBrowserRouter
來處理Router.tsx
中的路由。注意到了.tsx副檔名了嗎?是的,這是因為createBrowserRouter
需要element
內部的實際 JSX 元件。讓我給你展示一下路由器的範例:
export const router = createBrowserRouter([
{
path: "/",
element: <AppLayout/>,
children: [
{
path: "",
element: <ProtectedRoute><DashboardLayout/></ProtectedRoute>,
children: [
{
index: true,
path: "projects",
element: <OnlyAdmin fallback="/all-projects"><Projects/></OnlyAdmin>,
},
{
path: "projects/:projectId",
element: <OnlyAdmin><ProjectGuard/></OnlyAdmin>,
children: [
{
path: "groups",
element: <ProjectGroups/>
},
{
id: "project-stats",
path: "groups/:groupId",
element: <GroupGuard/>,
children: [
{
path: "stats",
element: <GroupStats/>
},
{
path: "consents",
element: <Consents/>
}
]
},
{
path: "stats",
element: <ProjectStats/>
}
]
},
fallbackRoute("/projects")
]
}
]
},
{
path: "*",
element: <NoMatch/>,
}
], {
future: {
v7_startTransition: true,
}
});
我喜歡使用createBrowserRouter
。它很簡潔,允許嵌套路由,如果你使用 React Router,我強烈推薦它。但問題隨之而來。
在整個專案中,我們使用<Link>
元件進行導航。很標準,對吧?類似:
<Link to="/projects/123/groups/456">Go to Group</Link>
現在,混亂就開始了。客戶請求改變路由結構。只需對路徑進行一些小的調整,沒有什麼大問題……對嗎?錯誤的。
因為現在,專案中的每個手動編寫的 URL 都必須更新。 🥲
我盯著程式碼庫,心想:好吧,這很糟糕。這真的很糟糕。
一定有更好的方法——一種不依賴硬編碼字串來驗證整個應用程式的連結的方法、一種在編寫路線時自動完成路線的方法、一種確保一致性的方法。
我的腦力激盪模式啟動了。
🔍我怎麼能讓路線變得更聰明?
💡 TypeScript 可以幫助強制執行路線驗證嗎?
🤔如何讓 WebStorm 自動完成路線名稱?
然後,一個最愚蠢但最有趣的想法突然出現在我腦海中:我應該為此製作一個 JetBrains 外掛程式嗎? 😂
在整整五分鐘的時間裡,我都在認真考慮寫一個完整的 JetBrains 外掛來解決我的路由問題。但後來我搖了搖頭。希赫姆,停下來。公司不會付錢給你來開發無用的插件。
相反,我想:讓我們使用 TypeScript。
如果我能以某種方式將路由路徑轉換為TypeScript 強制的 ID ,那麼我可以:
✔ 驗證連結。
✔ 啟用自動完成功能。
✔ 確保路線始終正確,不再有斷開的連結。
我修改了路由器,使每條路由包含唯一的 ID :
const routeObjects: RouteObject[] = [
{
path: "/",
element: <AppLayout/>,
id: "root",
children: [
{
path: "projects",
element: <OnlyAdmin><Projects/></OnlyAdmin>,
id: "admin-projects-list"
}
]
}
];
我覺得自己像個天才。但是TypeScript 並不配合。
我需要 TypeScript動態地辨識這些 ID作為有效值。我嘗試使用typeof routesObject
,但沒有用。經過數小時搜尋 TypeScript 文件(順便說一句,並不是很好的文件)、大量Stack Overflow 頁面,甚至與 ChatGPT 進行腦力激盪後,我還是一無所獲。
這時,一個新的想法出現了:如果我動態產生一個 TypeScript 類型檔案會怎麼樣?
繁榮。新計劃:
將所有路線提取到一個物件中。
動態產生TypeScript類型檔案。
每當路線改變時執行腳本。
我寫了一個腳本:
npm run generate-router-types
輸出結果如下:
export type AppRoute = 'admin-projects-list' | 'home';
💥任務完成!
我推動了 PR,休息了一會兒,然後回來期待能夠盡快獲得批准。
然後我看到了喬的評論。我們叫他喬吧(這不是他的真名,但我們就這樣叫吧)。
“我有更好的方法,只使用 TypeScript”
我的靈魂離開了我的身體。 💀
兄弟,你以為我沒有先嘗試過嗎?
然後他直接傳訊息給我:
“您有 15 分鐘的時間來討論一下您的 PR 嗎?”
偉大的。一次會議。和喬一起。
Joe 是公司新人,但他的履歷很瘋狂——Rust、Go、Node、DevOps,應有盡有。但他也擁有那種活力──那種對任何事總是能找到反駁的開發者。
儘管如此,我還是抱著「隨便」的態度參加了通話。讓我們來聽聽這位天才的看法。
Joe 分享了他的螢幕。
他開始解釋。
我看到了他的程式碼。
就在那一刻,我意識到......事實上,我還只是個大三學生。 😂
他的解決方案?純粹的TypeScript 魔法。
沒有建置腳本。
無需外部工具。
沒有路線提取功能。
僅需幾行 TypeScript。
type MergePath<T extends string | undefined, P extends string> =
T extends ""
? P
: P extends "/"|""
? `${P}${T}`
: `${P}/${T}`;
type ExtractRouteType<T extends RouteObject, P extends string> =
T extends { path: string } ?
{ route: P, paramsKeys: ExtractPathParams<P> }
: never;
type ExtractRoute<T extends RouteObject, P extends string> = (T extends { id: string, path: string } ? { [K in T["id"]]: ExtractRouteType<T, P> } : {}) & ExtractRoutes<T["children"], P>;
type ExtractRoutes<T extends RouteObject[] | undefined, P extends string = ""> = T extends [infer R extends RouteObject, ...infer S extends RouteObject[]] ? ExtractRoute<R, MergePath<R["path"], P>> & ExtractRoutes<S, P> : {};
export type RouteById = ExtractRoutes<typeof routeObjects> extends infer U ? { [K in keyof U]: U[K] } : never;
😭😭😭
我花了幾個小時尋找一個根本不需要的解決方案。
Joe 笑著說: “是的,當你知道如何使用 TypeScript 時,它真的很強大。”
是的,我受教育了。
但我非但沒有生悶氣,反而進行了反擊。我採用了 Joe 的 TypeScript 魔法並對其進行了改進。
我想:如果他可以動態提取路線,為什麼我不能動態驗證參數?
我希望TypeScript 強制執行路由參數,這表示:
✔ 如果路線需要:projectId
,我應該被迫傳遞{ projectId: string }
作為參數。
✔ 如果我忘記了一個參數,TypeScript 應該會拋出一個錯誤。
✔ 如果我新增了不需要的額外參數, TypeScript 會抱怨。
✔ 當然,自動完成功能仍應完美運作。
我回到了我的戰鬥崗位。 💻☕
我介紹了ExtractPathParams
,這是一個 TypeScript 實用程序,用於僅從路由路徑中提取所需的參數:
type ExtractPathParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? [Param, ...ExtractPathParams<`/${Rest}`>]
: Path extends `${string}:${infer Param}`
? [Param]
: [];
然後,我重寫了buildRoute
,以便如果沒有傳遞必需的參數,它就會拒絕編譯:
export function buildRoute<RouteId extends keyof RouteById>(
routeId: RouteId,
...params: ParamsArg<RouteId> extends undefined ? [] : [ParamsArg<RouteId>]
): string {
if (!routes[routeId]) {
throw new Error(`Route ${routeId} not found`);
}
let route = routes[routeId].route as string;
let params_ = params.length > 0 ? params[0] : {};
for (const [key, value] of Object.entries(params_)) {
route = route.replace(`:${key}`, value as string);
}
if (route.includes(':')) {
throw new Error(`Route ${routeId} contains an unresolved parameter`);
}
return route;
}
繁榮。現在,如果我傳遞了錯誤的參數,TypeScript 就會向我尖叫! 🎯
我還重構了AppLink
來整合這個新的驗證:
const AppLink = <RouteId extends keyof RouteById>({
routeId,
...props
}: RouteBuilderProps<RouteId> & Omit<LinkProps, "to"> & React.RefAttributes<HTMLAnchorElement>) => {
return <Link to={buildRoute(routeId, props.params)} {...props}/>;
};
export default AppLink;
我進行了測試。一切正常。
我又開了另一個 PR。這次,喬確實喜歡它了。
“很好,我喜歡這個。確保路線真正是類型安全的。幹得好。”
😭甜蜜的驗證。
現在,不用再這樣寫:
<Link to="/projects/123/groups/456">View Group</Link>
我們可以寫:
<AppLink routeId="admin-project-group-consents" params={{ projectId: "123", groupId: "456" }}>
View Group
</AppLink>
✔不再有錯誤的 URL。
✔ TypeScript 強制驗證。
✔自動完成路線名稱和參數。
✔將來改變路徑時可以減少麻煩。
就這樣,路由系統終於變得堅不可摧了。 🔥
那天,我明白了:經驗確實很重要。
我可能會懷恨在心。我可能已經感到痛苦了。但相反,我學習了,提高了,並累積了知識。
事實是,經驗不僅僅在於編碼所花的時間。這取決於你挑戰自己的深度、你的技能提升程度以及你對工具的真正了解程度。
那次公關對我來說是一個巨大的教訓。這不僅涉及路由、TypeScript 或 Joe 是否是 TypeScript 精靈。這是關於成長的事情。
因此,如果您發現自己被更好的解決方案所折服,請不要抗拒。研究它、在此基礎上建構它、並使之成為你自己的。
這才是真正偉大的開發人員的素質。 😎
結束。
原文出處:https://dev.to/hichemtab-tech/the-day-i-truly-realized-i-was-a-junior-developer-kgp