发布于 2026-01-05 1 阅读
0

我真正意识到自己只是个初级开发人员的那一天🤡

我真正意识到自己只是个初级开发人员的那一天🤡

免责声明:本文侧重于讲述我的经历。为了行文流畅、引人入胜,我省略了详细的代码解释和一些技术细节。本文旨在分享我的探索历程,而非提供深入的技术分析。

引言:谦逊与 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,
    }
});
Enter fullscreen mode Exit fullscreen mode

非常喜欢使用它createBrowserRouter。它界面简洁,支持嵌套路由,如果你正在使用 React Router,我强烈推荐它。但是后来问题出现了


问题🤯

在整个项目中,我们都使用这个<Link>组件进行导航。很标准,对吧?比如:

<Link to="/projects/123/groups/456">Go to Group</Link>
Enter fullscreen mode Exit fullscreen mode

现在,混乱就此开始。客户要求更改路由结构。只是对路径进行一些小调整,没什么大不了的……对吧?错!

因为现在,项目中每一个手动编写的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"
            }
        ]
    }
];
Enter fullscreen mode Exit fullscreen mode

我当时觉得自己简直是个天才。TypeScript却不配合

我需要 TypeScript动态地将这些 ID 识别为有效值。我尝试使用 `typescript.get_id()` typeof routesObject,但不行,它不起作用。在花了几个小时搜索 TypeScript 文档(顺便说一句,这文档写得真不怎么样)、浏览了无数Stack Overflow 页面,甚至还和 ChatGPT 一起头脑风暴之后,我仍然一无所获。

这时,一个新想法涌上心头:如果我动态生成一个 TypeScript 类型文件呢?

砰!新计划:

  1. 将所有路由提取到一个对象中。
  2. 动态生成 TypeScript 类型文件。
  3. 路由发生变化时运行脚本。

我写了一个脚本:

npm run generate-router-types
Enter fullscreen mode Exit fullscreen mode

输出结果为:

export type AppRoute = 'admin-projects-list' | 'home';
Enter fullscreen mode Exit fullscreen mode

💥任务完成!

我提交了公关稿,喝了杯咖啡休息了一下,回来后期待能很快获得批准


“乔”时刻😐

然后我看到了乔的评论。我们就叫他乔吧(这不是他的真名,但我们就这么叫他吧)。

“我可以用更好的方法,只用 TypeScript 来实现这个功能。”

GitHub PR 评论截图

我的灵魂离开了我的身体。💀

兄弟,你以为我没先试过吗?!

然后他直接给我发了消息:

“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;
Enter fullscreen mode Exit fullscreen mode

😭😭😭

我花了几个小时研究一个根本不需要的解决方案。

乔笑着说:“是啊,如果你知道如何使用 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]
    : [];
Enter fullscreen mode Exit fullscreen mode

然后,我重写了代码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;
}
Enter fullscreen mode Exit fullscreen mode

砰!现在如果我传递了错误的参数,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;
Enter fullscreen mode Exit fullscreen mode

我运行了测试,一切正常。

我又提交了一个PR。这次,乔居然喜欢上了。

“不错,我喜欢这个。它确保了路由真正具有类型安全性。做得好。”

😭真是太好了,得到了认可。

现在,不要再这样写了:

<Link to="/projects/123/groups/456">View Group</Link>
Enter fullscreen mode Exit fullscreen mode

我们可以这样写:

<AppLink routeId="admin-project-group-consents" params={{ projectId: "123", groupId: "456" }}>
    View Group
</AppLink>
Enter fullscreen mode Exit fullscreen mode

不再出现错误的 URL。✔

TypeScript强制验证。✔路由名称和参数自动补全。✔

未来更改路径时更加轻松

就这样,路由系统终于万无一失了。🔥


课程🎓

那天我明白了:经验真的很重要。

我本可以心怀怨恨,可以怨恨不已。但我没有,我选择学习、进步,并在这些知识的基础上不断积累

事实上,经验不仅仅取决于你花多少时间写代码。它还取决于你挑战自我的深度,你提升技能的程度,以及你对工具的真正理解。

那次公关对我来说是一次非常宝贵的经历。它不仅仅关乎路由、TypeScript,或者Joe是一位TypeScript高手,而是关乎成长

所以,如果你发现自己找到了更好的解决方案,不要抗拒它。研究它,在此基础上进行改进,并将其变成你自己的解决方案。

这才是真正优秀开发者的特质。😎

剧终。

文章来源:https://dev.to/hichemtab-tech/the-day-i-truly-realized-i-was-a-junior-developer-kgp