npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vlian/router

v0.1.2

Published

Business-friendly router wrapper built on react-router-dom

Readme

@vlian/router

基于 react-router-dom 的业务路由封装包。目标不是暴露更多路由细节,而是把静态路由标准化、动态补丁注入、导航阶段异步加载、基础鉴权包装这些复杂性收口到包内部,让业务方只维护两类输入:

  • 静态路由配置
  • 动态路由加载函数

1. 设计目标与边界

解决的问题

  • 统一业务侧路由输入模型,业务方只写声明式静态路由
  • 内部完成 StaticRouteConfig -> react-router-dom RouteObject 的标准化
  • 在导航发生时通过 patchRoutesOnNavigation 按需注入动态路由
  • 支持动态路由来源于本地模块、接口请求、权限码集合或混合来源
  • 内置动态加载去重、导航级缓存、错误回调和基础降级
  • layoutauthredirectmeta 等业务字段做统一消费

不解决的问题

  • 不替代业务权限系统本身
  • 不负责服务端菜单接口协议设计
  • 不强行规定业务的菜单模型、埋点模型、面包屑模型
  • 不试图绕过 react-router-dom 能力边界,仍以 data router 为底座
  • 不内置复杂页面级权限请求流转,异步鉴权建议放在动态路由加载或 loader 中完成

2. 推荐 API 设计

主入口

import { createAppRouter, defineRoutes } from "@vlian/router";

根入口只暴露核心构建 API,用于尽量降低消费端打包时被额外带入的运行时代码。

如果需要直接渲染 provider,请显式从子路径导入:

import { AppRouterProvider } from "@vlian/router/provider";

核心 API

const routes = defineRoutes([
  {
    id: "root",
    path: "/",
    component: RootPage,
    children: [
      {
        path: "dashboard",
        component: DashboardPage,
        meta: { title: "Dashboard" },
      },
    ],
  },
]);

const router = createAppRouter({
  mode: "browser",
  routes,
  dynamic: {
    defaultParentId: "root",
    loader: async ({ path, signal }) => {
      if (!path.startsWith("/system")) {
        return;
      }

      const response = await fetch("/api/routes", { signal });
      const payload = await response.json();

      return payload.routes.map((item: any) => ({
        id: item.code,
        path: item.path,
        lazy: async () => {
          const mod = await import(`./pages/${item.component}.tsx`);
          return { Component: mod.default };
        },
        meta: { title: item.title, code: item.code },
      }));
    },
  },
});

对外 API 说明

  • defineRoutes(routes) 仅做类型收敛,帮助业务侧保留推断结果

  • createAppRouter(options) 包主工厂。内部完成:

    • 静态路由 normalize
    • 动态 patch 创建
    • createBrowserRouter/createHashRouter/createMemoryRouter 适配
  • AppRouterProvider 针对“只想直接传 options 渲染”的场景,内部 useMemo(createAppRouter)。 为避免默认入口增重,请从 @vlian/router/provider@vlian/router/runtime 单独导入

3. TypeScript 类型设计

StaticRouteConfig

export interface StaticRouteConfig<Meta, Extra> extends Extra {
  id?: string;
  path?: string;
  index?: boolean;
  caseSensitive?: boolean;
  component?: RouteComponent;
  Component?: RouteComponent;
  element?: ReactNode;
  lazy?: RouteLazyLoader;
  loader?: RouteObject["loader"];
  action?: RouteObject["action"];
  shouldRevalidate?: RouteObject["shouldRevalidate"];
  errorElement?: ReactNode;
  ErrorBoundary?: RouteObject["ErrorBoundary"];
  hydrateFallbackElement?: ReactNode;
  HydrateFallback?: RouteObject["HydrateFallback"];
  handle?: RouteObject["handle"];
  meta?: Meta;
  layout?: RouteLayout<Meta, Extra>;
  auth?: RouteAuthConfig;
  redirect?: RouteRedirect;
  children?: StaticRouteConfig<Meta, Extra>[];
}

设计要点:

  • 业务优先写 component,不必理解 Componentelement 差异
  • 保留 lazyloaderaction 等 data router 原生能力
  • meta 泛型可扩展
  • Extra 泛型用于承接业务扩展字段

DynamicRouteLoader

export type DynamicRouteLoader<Meta, Extra> = (
  context: DynamicRouteLoaderContext<Meta, Extra>,
) => Promise<
  | void
  | StaticRouteConfig<Meta, Extra>[]
  | DynamicRoutePatch<Meta, Extra>
  | DynamicRoutePatch<Meta, Extra>[]
>;

DynamicRouteLoaderContext

export interface DynamicRouteLoaderContext<Meta, Extra> {
  path: string;
  signal: AbortSignal;
  matches: RouteMatch<string, AppRouteObject<Meta, Extra>>[];
  router: DataRouter;
  knownRouteIds: Set<string>;
}

DynamicRoutePatch

export interface DynamicRoutePatch<Meta, Extra> {
  parentId?: string | null;
  routes: StaticRouteConfig<Meta, Extra>[];
  cacheKey?: string | false;
}

RouterPluginOptions

export interface RouterPluginOptions<Meta, Extra> {
  routes: StaticRouteConfig<Meta, Extra>[];
  mode?: "browser" | "hash" | "memory";
  basename?: string;
  future?: CreateMemoryRouterOptions["future"];
  hydrationData?: CreateMemoryRouterOptions["hydrationData"];
  getContext?: CreateMemoryRouterOptions["getContext"];
  dataStrategy?: CreateMemoryRouterOptions["dataStrategy"];
  initialEntries?: CreateMemoryRouterOptions["initialEntries"];
  initialIndex?: CreateMemoryRouterOptions["initialIndex"];
  window?: Window;
  dynamic?: DynamicRoutingOptions<Meta, Extra>;
  authorization?: RouteAuthorizationOptions<Meta, Extra>;
  onError?: RouterErrorHandler<Meta, Extra>;
}

4. 内部架构设计

目录按职责拆分,不把 normalize、patch、runtime 混在一个文件:

src
├── adapters
│   ├── create-app-router.ts
│   └── index.ts
├── core
│   ├── guards
│   │   └── route-shell.tsx
│   ├── normalize
│   │   ├── normalize-routes.ts
│   │   └── route-id.ts
│   ├── patcher
│   │   └── create-navigation-patcher.ts
│   ├── runtime
│   │   └── route-registry.ts
│   └── index.ts
├── runtime
│   ├── AppRouterProvider.tsx
│   └── index.ts
├── types
│   ├── index.ts
│   └── route.ts
└── index.ts

各层职责

  • adapters 面向 react-router-dom,负责组装最终 router

  • core/normalize 负责静态路由标准化、自动 id 生成、业务字段转换

  • core/guardslayout/auth/redirect 包装成统一的 route shell

  • core/patcher 封装导航期间动态注入逻辑,支持 await、缓存、错误处理和去重

  • core/runtime 维护已注册路由集合,避免重复 patch

  • runtime 提供 AppRouterProvider

  • types 只放公开类型,不掺杂实现

5. 关键实现思路

静态路由输入

业务方只关心:

  • path
  • component/lazy
  • children
  • meta
  • layout
  • auth
  • redirect

包内部负责:

  • 自动生成稳定 id
  • component -> Component
  • redirect -> <Navigate />
  • layout -> RouteShell
  • auth + authorization -> RouteShell
  • 补齐 handle.meta/auth/source

动态补丁

使用 react-router-dom@7patchRoutesOnNavigation,内部封装:

  • loader(context) 支持异步
  • await fetch(...)
  • 接口成功后返回路由数组或 patch 描述
  • 自动 normalize 为 RouteObject
  • 自动过滤已注入 id
  • parentId 打补丁
  • 支持 cacheKey

错误处理机制

动态路由加载失败时:

  • 默认走 onError(context) 回调
  • errorMode: "silent" 时只降级,不中断导航
  • errorMode: "throw" 时向上抛出,让业务自己接管

6. 最小可用示例

6.1 定义静态路由

import { Outlet } from "react-router-dom";
import { defineRoutes } from "@vlian/router";

function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="app-shell">
      <aside>menu</aside>
      <main>{children}</main>
    </div>
  );
}

function RootPage() {
  return <Outlet />;
}

function LoginPage() {
  return <div>login</div>;
}

export const routes = defineRoutes([
  {
    id: "root",
    path: "/",
    component: RootPage,
    layout: RootLayout,
    children: [
      {
        index: true,
        redirect: "/dashboard",
      },
    ],
  },
  {
    id: "login",
    path: "/login",
    component: LoginPage,
    meta: { title: "登录" },
  },
]);

6.2 定义动态路由加载方法

import type { DynamicRouteLoader } from "@vlian/router";

type AppMeta = {
  title?: string;
  permissionCode?: string;
};

export const loadDynamicRoutes: DynamicRouteLoader<AppMeta> = async ({
  path,
  signal,
}) => {
  if (!path.startsWith("/system")) {
    return;
  }

  const response = await fetch("/api/auth/routes", { signal });
  if (!response.ok) {
    throw new Error(`route api failed: ${response.status}`);
  }

  const data = await response.json();

  return {
    parentId: "root",
    cacheKey: `permission:${data.authzVersion}`,
    routes: data.routes.map((item: any) => ({
      id: item.code,
      path: item.path.replace(/^\//, ""),
      meta: {
        title: item.title,
        permissionCode: item.code,
      },
      auth: {
        required: true,
        permissionCode: item.code,
      },
      lazy: async () => {
        const mod = await import(`./pages/${item.component}.tsx`);
        return {
          Component: mod.default,
          loader: mod.loader,
        };
      },
    })),
  };
};

6.3 创建 router

import { createAppRouter } from "@vlian/router";
import { routes } from "./routes";
import { loadDynamicRoutes } from "./dynamic-routes";
import { authStore } from "./state/auth-store";

export const router = createAppRouter({
  mode: "browser",
  routes,
  authorization: {
    resolve: ({ auth }) => {
      if (!auth || auth === false) {
        return true;
      }

      const currentPermissions = authStore.getState().permissions;
      const permissionCode =
        typeof auth === "object" ? auth.permissionCode : undefined;

      return !permissionCode || currentPermissions.includes(permissionCode);
    },
    redirectTo: "/login",
  },
  dynamic: {
    defaultParentId: "root",
    loader: loadDynamicRoutes,
    getNavigationCacheKey: ({ path }) =>
      path.startsWith("/system") ? path : false,
    errorMode: "silent",
  },
  onError: ({ error, path }) => {
    console.error("dynamic route load failed", path, error);
  },
});

这里的 authStore 可以是 Redux、Zustand、Pinia 风格的前端状态容器,关键点是:

  • 路由层只消费当前权限状态
  • 真正的权限来源由登录态接口或用户信息接口提供
  • 前端权限只用于体验控制,不作为最终安全边界

例如可以先通过接口拉取当前用户权限,再写入 store:

type AuthState = {
  permissions: string[];
  setPermissions: (permissions: string[]) => void;
};

export const authStore = create<AuthState>((set) => ({
  permissions: [],
  setPermissions: (permissions) => set({ permissions }),
}));

export const bootstrapAuth = async () => {
  const response = await fetch("/api/auth/me");
  if (!response.ok) {
    throw new Error(`auth bootstrap failed: ${response.status}`);
  }

  const data = await response.json();
  authStore.getState().setPermissions(data.permissions ?? []);
};

如果你的权限系统完全以后端接口为准,也可以在 authorization.resolve 里只做“前端展示控制”,而把真正的安全校验继续留在服务端接口。

6.4 在应用中使用

import { RouterProvider } from "react-router-dom";
import { router } from "./router";

export function App() {
  return <RouterProvider router={router} />;
}

6.5 使用 AppRouterProvider

import { AppRouterProvider } from "@vlian/router/provider";
import { routes } from "./routes";
import { loadDynamicRoutes } from "./dynamic-routes";

export function App() {
  return (
    <AppRouterProvider
      options={{
        mode: "browser",
        routes,
        dynamic: {
          defaultParentId: "root",
          loader: loadDynamicRoutes,
        },
      }}
    />
  );
}

6.6 懒加载用法

当前版本已经原生支持懒加载,直接在 StaticRouteConfig.lazy 里返回 react-router-dom 的 lazy route module 即可。

推荐把 import() 提取成独立函数,这样后面做预加载时可以复用同一份模块加载逻辑。

import { defineRoutes } from "@vlian/router";

const loadDashboardModule = () => import("./pages/dashboard");
const loadUserListModule = () => import("./pages/system/users");

export const routes = defineRoutes([
  {
    id: "root",
    path: "/",
    children: [
      {
        path: "dashboard",
        lazy: async () => {
          const mod = await loadDashboardModule();
          return {
            Component: mod.default,
            loader: mod.loader,
          };
        },
        meta: {
          title: "Dashboard",
        },
      },
      {
        path: "system/users",
        lazy: async () => {
          const mod = await loadUserListModule();
          return {
            Component: mod.default,
            loader: mod.loader,
            ErrorBoundary: mod.ErrorBoundary,
          };
        },
        meta: {
          title: "用户管理",
        },
      },
    ],
  },
]);

适用场景:

  • 页面体积较大,希望按需分包
  • 页面 loader 和页面组件需要一起延迟加载
  • 权限页、后台页、低频页不希望在首屏打包

6.7 预加载用法

当前版本没有单独暴露 preloadRoute() 这一类官方 API,因此推荐两种预加载方式:

  • 对静态懒加载页面,手动提前执行同一个 import() 函数
  • 对动态路由接口,手动预热接口请求缓存,让真正导航时直接复用结果

6.7.1 预加载静态懒加载页面

const loadUserListModule = () => import("./pages/system/users");

export const routes = defineRoutes([
  {
    id: "root",
    path: "/",
    children: [
      {
        path: "system/users",
        lazy: async () => {
          const mod = await loadUserListModule();
          return {
            Component: mod.default,
            loader: mod.loader,
          };
        },
      },
    ],
  },
]);

export const preloadUserListPage = () => loadUserListModule();

在菜单 hover、按钮曝光、空闲时机里预热模块:

import { Link } from "react-router-dom";
import { preloadUserListPage } from "./routes";

export function UserMenuLink() {
  return (
    <Link
      onFocus={() => {
        void preloadUserListPage();
      }}
      onMouseEnter={() => {
        void preloadUserListPage();
      }}
      to="/system/users"
    >
      用户管理
    </Link>
  );
}

6.7.2 预加载动态路由接口

对于动态路由,推荐把“请求路由清单”的逻辑提成可复用缓存函数。这样:

  • 预加载阶段可以先请求一次
  • 真正导航进入时,dynamic.loader 直接复用缓存结果
type DynamicRouteResponse = {
  authzVersion: number;
  routes: Array<{
    code: string;
    path: string;
    title: string;
    component: string;
  }>;
};

let routeManifestPromise: Promise<DynamicRouteResponse> | null = null;

const fetchRouteManifest = async (
  signal?: AbortSignal,
): Promise<DynamicRouteResponse> => {
  if (!routeManifestPromise) {
    routeManifestPromise = fetch("/api/auth/routes", { signal }).then(
      async (response) => {
        if (!response.ok) {
          routeManifestPromise = null;
          throw new Error(`route api failed: ${response.status}`);
        }

        return response.json();
      },
    );
  }

  return routeManifestPromise;
};

export const preloadDynamicRouteManifest = () => fetchRouteManifest();

export const loadDynamicRoutes: DynamicRouteLoader = async ({
  path,
  signal,
}) => {
  if (!path.startsWith("/system")) {
    return;
  }

  const data = await fetchRouteManifest(signal);

  return {
    parentId: "root",
    cacheKey: `permission:${data.authzVersion}`,
    routes: data.routes.map((item) => ({
      id: item.code,
      path: item.path.replace(/^\//, ""),
      lazy: async () => {
        const mod = await import(`./pages/${item.component}.tsx`);
        return {
          Component: mod.default,
          loader: mod.loader,
        };
      },
      meta: {
        title: item.title,
      },
    })),
  };
};

在用户即将进入系统管理区之前,先预热动态路由接口:

import { preloadDynamicRouteManifest } from "./dynamic-routes";

export function SystemEntryButton() {
  return (
    <button
      onFocus={() => {
        void preloadDynamicRouteManifest();
      }}
      onMouseEnter={() => {
        void preloadDynamicRouteManifest();
      }}
      type="button"
    >
      进入系统管理
    </button>
  );
}

6.7.3 预加载建议

  • 预加载触发点优先放在 onMouseEnteronFocus、首屏空闲时机
  • 不要对全站所有路由无差别预加载,否则会抵消懒加载收益
  • 动态路由预加载建议只预热“路由清单接口”,页面模块仍由实际访问时再 lazy import
  • 如果后续需要统一 API,可以在此包上继续增加 preloadRoute / prefetchDynamicRoutes 官方封装

7. 与参考源码的关系

参考目录里的思路本质上是:

  • 静态路由先创建
  • patchRoutesOnNavigation 在导航时补权限路由
  • 动态请求和路由去重放在一个收口函数内

这个包保留了那套思想,但做了三点抽象:

  1. 业务不再直接操作 RouteObject
  2. 业务不再自己写 patch 去重逻辑
  3. 业务可以直接返回“路由数组”或“patch 描述”

8. 方案权衡

易用性

  • 优先让业务写声明式配置
  • patchRoutesOnNavigation、注册去重、导航缓存隐藏在包内
  • 保留 component 这种业务更容易理解的字段

可扩展性

  • meta 泛型可扩展
  • Extra 泛型可扩展业务自定义字段
  • dynamic.loader 支持多种动态来源
  • authorization 可替换成任何业务权限判定逻辑

可维护性

  • normalize、patcher、registry、runtime 分文件
  • 不把适配器逻辑和业务字段转换揉成一个文件
  • 后续接菜单、埋点、面包屑时只需要在 normalize 层或 handle 层扩展

性能

  • 只在需要的导航上触发动态加载
  • 同一路径支持缓存
  • 路由 id 去重避免重复 patch
  • lazy 仍然由 react-router-dom 原生消费

动态注入时机

  • 放在导航期,而不是应用启动期
  • 更适合权限路由、租户能力路由、插件路由
  • 可以在拿到服务端返回后再决定注入哪些页面

与权限系统结合

  • 简单鉴权可放 authorization.resolve
  • 异步权限拉取建议放 dynamic.loader
  • 页面数据级权限建议继续放页面 loader 或业务请求层

9. npm 发布与交付建议

package.json 关键字段

当前包已采用:

  • main: ./dist/index.cjs
  • module: ./dist/index.js
  • types: ./dist/index.d.ts
  • exports 同时暴露根入口、./core./runtime./types
  • peerDependencies
    • react
    • react-dom
    • react-router-dom
  • files
    • dist
    • README.md

构建产物策略

  • tsc --emitDeclarationOnly 生成类型声明
  • swc 生成 ESM
  • swc + rename-cjs.js 生成 CJS

ESM / CJS / d.ts 策略

  • ESM 产物:dist/**/*.js
  • CJS 产物:dist/**/*.cjs
  • 类型声明:dist/**/*.d.ts

README 最小内容

至少包含:

  • 安装方式
  • 快速开始
  • StaticRouteConfig 字段说明
  • dynamic.loader 示例
  • 错误处理方式
  • 版本与 peer dependency 说明

用户安装方式

pnpm add @vlian/router react-router-dom

React + Vite 最小接入示例

下面给出一个可以直接放进 React + Vite 项目的最小示例,目录大致如下:

src
├── main.tsx
├── router.ts
├── routes.ts
├── dynamic-routes.ts
├── state
│   └── auth-store.ts
└── pages
    ├── home.tsx
    ├── login.tsx
    └── system
        └── users.tsx

1. 安装依赖

pnpm add react-router-dom @vlian/router zustand

2. 定义权限 store

// src/state/auth-store.ts
import { create } from "zustand";

type AuthState = {
  permissions: string[];
  setPermissions: (permissions: string[]) => void;
};

export const authStore = create<AuthState>((set) => ({
  permissions: [],
  setPermissions: (permissions) => set({ permissions }),
}));

export const bootstrapAuth = async () => {
  const response = await fetch("/api/auth/me");
  if (!response.ok) {
    throw new Error(`auth bootstrap failed: ${response.status}`);
  }

  const data = await response.json();
  authStore.getState().setPermissions(data.permissions ?? []);
};

3. 定义静态路由

// src/routes.ts
import { Outlet } from "react-router-dom";
import { defineRoutes } from "@vlian/router";

function RootLayout({ children }: { children?: React.ReactNode }) {
  return (
    <div>
      <header>Demo App</header>
      <main>{children}</main>
    </div>
  );
}

const loadHomeModule = () => import("./pages/home");
const loadLoginModule = () => import("./pages/login");

export const routes = defineRoutes([
  {
    id: "root",
    path: "/",
    component: Outlet,
    layout: RootLayout,
    children: [
      {
        index: true,
        lazy: async () => {
          const mod = await loadHomeModule();
          return {
            Component: mod.default,
          };
        },
        meta: {
          title: "首页",
        },
      },
    ],
  },
  {
    id: "login",
    path: "/login",
    lazy: async () => {
      const mod = await loadLoginModule();
      return {
        Component: mod.default,
      };
    },
    meta: {
      title: "登录",
    },
  },
]);

4. 定义动态路由加载器

// src/dynamic-routes.ts
import type { DynamicRouteLoader } from "@vlian/router";

type AppMeta = {
  title?: string;
  permissionCode?: string;
};

const pageModuleLoaders: Record<string, () => Promise<any>> = {
  "system/users": () => import("./pages/system/users"),
};

export const loadDynamicRoutes: DynamicRouteLoader<AppMeta> = async ({
  path,
  signal,
}) => {
  if (!path.startsWith("/system")) {
    return;
  }

  const response = await fetch("/api/auth/routes", { signal });
  if (!response.ok) {
    throw new Error(`route api failed: ${response.status}`);
  }

  const data = await response.json();

  return {
    parentId: "root",
    cacheKey: `permission:${data.authzVersion}`,
    routes: data.routes.map((item: any) => ({
      id: item.code,
      path: item.path.replace(/^\//, ""),
      auth: {
        required: true,
        permissionCode: item.code,
      },
      meta: {
        title: item.title,
        permissionCode: item.code,
      },
      lazy: async () => {
        const loadModule = pageModuleLoaders[item.component];
        if (!loadModule) {
          throw new Error(`unknown page module: ${item.component}`);
        }

        const mod = await loadModule();

        return {
          Component: mod.default,
          loader: mod.loader,
        };
      },
    })),
  };
};

5. 创建 router

// src/router.ts
import { createAppRouter } from "@vlian/router";
import { routes } from "./routes";
import { loadDynamicRoutes } from "./dynamic-routes";
import { authStore } from "./state/auth-store";

export const router = createAppRouter({
  mode: "browser",
  routes,
  authorization: {
    resolve: ({ auth }) => {
      if (!auth || auth === false) {
        return true;
      }

      const permissionCode =
        typeof auth === "object" ? auth.permissionCode : undefined;

      return (
        !permissionCode ||
        authStore.getState().permissions.includes(String(permissionCode))
      );
    },
    redirectTo: "/login",
  },
  dynamic: {
    defaultParentId: "root",
    loader: loadDynamicRoutes,
    getNavigationCacheKey: ({ path }) =>
      path.startsWith("/system") ? path : false,
    errorMode: "silent",
  },
  onError: ({ error, path }) => {
    console.error("dynamic route load failed", path, error);
  },
});

6. 在 Vite 入口挂载 RouterProvider

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import { bootstrapAuth } from "./state/auth-store";

const start = async () => {
  await bootstrapAuth();

  ReactDOM.createRoot(document.getElementById("root")!).render(
    <React.StrictMode>
      <RouterProvider router={router} />
    </React.StrictMode>,
  );
};

void start();

7. 页面文件示例

// src/pages/home.tsx
export default function HomePage() {
  return <div>home page</div>;
}
// src/pages/login.tsx
export default function LoginPage() {
  return <div>login page</div>;
}
// src/pages/system/users.tsx
export async function loader() {
  return null;
}

export default function UsersPage() {
  return <div>users page</div>;
}

这个示例的运行过程是:

  • 启动时先调用 bootstrapAuth() 初始化权限到 store
  • 创建 browser router,并注册静态路由
  • 当用户导航到 /system/** 时,dynamic.loader 发起接口请求
  • 接口返回后按 parentId: "root" 把动态路由补丁进当前路由树
  • 路由命中后再执行页面级 lazy import

用户引入方式

import { createAppRouter, defineRoutes } from "@vlian/router";

初始化方式

const router = createAppRouter({
  mode: "browser",
  routes,
});

产出可供下载使用的构建物

pnpm --filter @vlian/router build

打包后发布:

pnpm --filter @vlian/router publish --access public

10. 推荐后续增强项

  • 增加 beforePatch / afterPatch 生命周期钩子
  • 支持多路动态 loader 组合
  • 把菜单树和路由树的映射收口成官方 helper
  • 增加 prefetchDynamicRoutes(paths[])
  • 增加测试覆盖:
    • normalize
    • dynamic patch 去重
    • cacheKey
    • redirect/layout/auth 包装

11. 当前落地状态

packages/router 已包含:

  • 独立 npm 包骨架
  • 类型定义
  • 主工厂 createAppRouter
  • 静态路由 normalize
  • 动态路由 patcher
  • route registry
  • AppRouterProvider 子路径导出
  • npm 发布和构建配置

如果要继续落到生产可用版本,下一步建议补:

  • 单元测试
  • 真实业务示例
  • changelog
  • 发布流水线