我真正意识到自己只是个初级开发人员的那一天🤡
免责声明:本文侧重于讲述我的经历。为了行文流畅、引人入胜,我省略了详细的代码解释和一些技术细节。本文旨在分享我的探索历程,而非提供深入的技术分析。
引言:谦逊与 TypeScript 魔法的一课
每个开发者都会经历这样的时刻——自以为掌握了某个技能,却被同事彻底击败。这就是我的“高光时刻”。一切始于一个简单的目标:改进 React 项目中的路由。最终,我却开始质疑自己作为开发者的意义。😅
接下来是一段充满刺激、挫折、多巴胺飙升的突破时刻,以及最终对经验、技能和成为一名资深开发人员的真正意义的深刻领悟的旅程。准备好了吗?🚀
设置🎬
又是一天的开发,又是一天感觉自己像个专业人士。我正在做一个 React 项目,使用createBrowserRouterJSX 来处理路由Router.tsx。注意到.tsx扩展名了吗?没错,那是因为JSXcreateBrowserRouter需要在 JSX 组件内部使用实际的 JSX 组件element。让我给你展示一下路由的示例:
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 插件来解决我的路由问题。但最后,我摇了摇头。Hichem ,别这样。公司付钱给你可不是为了让你写没用的插件。
“绝妙”计划🚀
我当时想:不如用 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 识别为有效值。我尝试使用 `typescript.get_id()` typeof routesObject,但不行,它不起作用。在花了几个小时搜索 TypeScript 文档(顺便说一句,这文档写得真不怎么样)、浏览了无数Stack Overflow 页面,甚至还和 ChatGPT 一起头脑风暴之后,我仍然一无所获。
这时,一个新想法涌上心头:如果我动态生成一个 TypeScript 类型文件呢?
砰!新计划:
- 将所有路由提取到一个对象中。
- 动态生成 TypeScript 类型文件。
- 路由发生变化时运行脚本。
我写了一个脚本:
npm run generate-router-types
输出结果为:
export type AppRoute = 'admin-projects-list' | 'home';
💥任务完成!
我提交了公关稿,喝了杯咖啡休息了一下,回来后期待能很快获得批准。
“乔”时刻😐
然后我看到了乔的评论。我们就叫他乔吧(这不是他的真名,但我们就这么叫他吧)。
“我可以用更好的方法,只用 TypeScript 来实现这个功能。”
我的灵魂离开了我的身体。💀
兄弟,你以为我没先试过吗?!
然后他直接给我发了消息:
“As-tu 15min pour discuter ta PR ?”
太好了。和乔见面。
乔是公司新人,但他履历惊人——Rust、Go、Node、DevOps,样样精通。而且他精力充沛——那种总能对所有事情提出不同意见的开发者。
不过,我还是抱着一种“随便吧”的态度加入了通话。就听听这位天才怎么说吧。
羞辱🤡
乔分享了他的屏幕。
他开始解释起来。
我看过他的代码。
那一刻,我意识到……我其实是大三学生。😂
他的解决方案?纯粹的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;
😭😭😭
我花了几个小时研究一个根本不需要的解决方案。
乔笑着说:“是啊,如果你知道如何使用 TypeScript,它确实非常强大。”
是的,我被好好教训了一顿。
救赎之路🦸♂️
但我没有垂头丧气,而是奋起反击。我借鉴了乔的TypeScript绝技,并在此基础上进行了改进。
我当时想:如果他可以动态提取路由,为什么我不能动态验证参数呢?
我希望TypeScript 强制执行路由参数,这意味着:
✔ 如果路由需要参数:projectId,我应该被强制传递{ projectId: string }参数。✔
如果我忘记传递参数,TypeScript 应该抛出错误。✔
如果我添加了不需要的额外参数,TypeScript 应该发出警告。✔
当然,自动补全功能也应该完美运行。
我回到了我的战斗岗位。💻☕
我引入了一个 TypeScript 工具,用于从路由路径中ExtractPathParams提取所需的参数:
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
