Skyroc Admin React
架构

启动流程

理解 apps/admin 从 main.tsx 到 bootstrap.tsx 再到 App.tsx 的初始化顺序

这一页解释 apps/admin 如何从浏览器入口初始化到后台应用可用。重点不是背入口文件,而是判断新的启动逻辑应该放在哪一层:入口加载、一次性运行时初始化、React Provider 组合,还是路由后的业务初始化。

当前启动主链路是:

index.html
  -> main.tsx
    -> bootstrap.tsx
      -> setupTheme
      -> setupAdminLayouts
      -> setupAdminPlugins
      -> setupI18n
      -> render App
        -> QueryClientProvider
        -> JotaiProvider
        -> Devtools
        -> AntdProvider
        -> NotificationProvider
        -> LazyAnimate
        -> RouterProvider
        -> GlobalEffect

适用场景

当你要做下面这些改动时,先看这页:

场景重点位置
调整应用启动顺序apps/admin/src/main.tsxapps/admin/src/bootstrap.tsxapps/admin/src/App.tsx
接入新的运行时插件apps/admin/src/plugins/index.ts@skyroc/web-admin-runtime
修改主题、暗色模式或 Ant Design 主题接入setupThemeAppAntdProviderThemeEffect
修改布局、菜单、动态路由和 tabs 初始化setupAdminLayoutsfeatures/menusfeatures/auth/use-auth.ts
修改语言、日期本地化或国际化初始化setupI18nGlobalEffectlocales/sync.ts
排查白屏、devtools、NProgress 或路由守卫问题main.tsxbootstrap.tsxpages/__root.tsxfeatures/router/guard.ts

当前实现位置

文件职责
apps/admin/index.html提供 <div id="app"></div>,并加载 /src/main.tsx
apps/admin/src/main.tsx浏览器入口。开发环境先加载 Jotai devtools,再动态导入 bootstrap.tsx
apps/admin/src/bootstrap.tsx启动编排层。完成主题、布局、插件、i18n 初始化,并挂载 React 根节点。
apps/admin/src/App.tsxProvider 组合层。把 React Query、Jotai、devtools、Ant Design、通知、动画、路由和全局副作用组合起来。
apps/admin/src/config.ts应用级配置入口,提供默认首页、路由模式、菜单图标、语言、主题和 devtools 配置。
apps/admin/src/features/router创建 TanStack Router,并通过 RouterProvider 注入登录态、用户信息和首页上下文。
apps/admin/src/features/auth/use-auth.ts登录态和初始化用户信息的入口。路由守卫会通过它加载用户信息和菜单。
apps/admin/src/plugins全局样式、svg icon 注册、Dayjs、Iconify、NProgress 和更新检测等运行时插件。

核心概念

main.tsx 只负责入口加载

main.tsx 的职责很窄:

async function bootstrap() {
  if (import.meta.env.DEV) {
    await import('@skyroc/web-admin-devtools/jotai');
  }

  await import('./bootstrap');
}

bootstrap();

开发环境会先加载 @skyroc/web-admin-devtools/jotai,再进入真正的应用启动。这样 Jotai devtools 的接入发生在应用 atom 被 Provider 使用之前。生产环境不会加载这段 devtools 入口。

不要把主题、路由、请求、布局、权限等应用初始化直接塞进 main.tsx。这里应该保持为“环境相关入口加载层”。

bootstrap.tsx 是一次性初始化编排层

bootstrap.tsx 先查找 #app 容器,容器不存在时直接返回。容器存在后,它按固定顺序初始化应用运行所需的全局能力。

顺序初始化当前行为
1setupTheme({ buildTime: BUILD_TIME })初始化主题 atom。开发环境使用默认主题;生产环境会读取缓存,并通过构建时间处理主题覆盖。
2setupAdminLayouts(...)把默认首页、默认图标、路由模式、route tree、菜单分类、动态路由加载器、菜单扩展和本地缓存适配器交给布局包。
3setupAdminPlugins()初始化 Dayjs、NProgress、Iconify 离线服务和生产环境应用更新检测。
4await setupI18n()初始化 @skyroc/web-admin-i18n,读取默认语言、fallback 语言、语言选项和本地缓存。
5createRoot(container).render(...)ErrorBoundary 下渲染 App

这个顺序不能随意打乱。主题必须早于任何组件读取主题 atom;布局配置必须早于 WebAdminLayout 和菜单 hooks 使用;NProgress 必须早于根路由调用 globalConfig.nprogress;i18n 必须早于页面和 layout 读取翻译。

bootstrap.tsx 还会静态导入 ./plugins/assets。这个文件注册全局资源:

import 'uno.css';
import 'virtual:svg-icons-register';
import '../styles/css/global.css';

这类样式和资源注册属于应用级副作用,应该继续放在启动编排附近,而不是放进具体页面。

setupAdminLayouts 只接收配置,不渲染布局

setupAdminLayouts 来自 @skyroc/web-admin-layouts。它当前只是保存布局运行时配置,真正的布局渲染发生在 pages/(admin)/layout.tsx 中的 WebAdminLayout

apps/admin 传入的关键配置包括:

配置作用
defaultHomeglobalConfig.defaultHome 读取默认首页。
routeModeVITE_AUTH_ROUTE_MODE 读取静态或动态路由模式。
routeTree使用 TanStack Router 生成的 routeTree.gen.ts
loadDynamicRoutes通过 loadAdminDynamicRoutes() 加载真实后端菜单响应,并适配成布局包动态菜单数据。
menuCategories定义菜单分类和 layout route 的关系,当前 admin 对应 /(admin)
menuNodeCallback为顶层菜单追加 divider 等项目级菜单节点。
extras注册标准 badge 之外的菜单扩展 UI,例如 ReleaseChannel
permissionSuperRoleVITE_STATIC_SUPER_ROLE 读取超级角色标识。
storage使用 localStg 读写布局、菜单和 tabs 相关缓存。

因此,新增菜单分类、菜单额外节点或动态路由加载策略时,优先改 features/menusbootstrap.tsx 的布局配置,不要在 WebAdminLayout 内部硬编码业务规则。

App.tsx 负责 Provider 组合

App.tsx 不做一次性初始化,它只负责把 React 运行时需要的 Provider 按依赖关系包起来。

QueryClientProvider
  -> JotaiProvider
    -> Devtools
    -> AntdProvider
      -> NotificationProvider
        -> LazyAnimate
          -> RouterProvider
          -> GlobalEffect

这里有几个依赖关系需要保持:

Provider 或组件为什么在这里
QueryClientProvideruseAuthAntdProvider、路由守卫和服务 hooks 都依赖同一个 queryClient
JotaiProvider主题、语言、认证等应用状态依赖 Jotai store。
Devtools开发环境加载后台调试面板,并接入 queryClientrouterglobalStore
AntdProvider读取当前语言和用户信息,把 Ant Design locale 与主题能力交给下层 UI。
NotificationProvider提供通知系统能力,并配置通知音效。
RouterProvideruseAuth 返回的登录态、用户信息、首页和初始化函数注入 TanStack Router context。
GlobalEffect挂载主题和语言副作用,例如主题状态同步、语言切换后同步 Dayjs locale。

RouterProvider 注入的 context 会被根路由和后台 layout route 使用。根路由在已登录但未初始化时调用 initAuth();后台 layout route 通过 guardAdminRoute() 做登录、用户信息、权限和外链处理。

路由后的初始化发生在 guard 和 hooks 中

不要把所有业务初始化都提前到 bootstrap.tsx。用户信息、菜单和权限属于“有登录态后才能确定”的运行时数据,当前由路由链路完成:

阶段位置行为
Router context 注入features/router/RouterProvider.tsxuseAuth() 读取 initAuthclearAuthisLoggedInuserInfohomeRoute 等上下文。
根路由预初始化pages/__root.tsx已登录但 auth 未初始化时,调用 context.initAuth()
后台路由守卫pages/(admin)/layout.tsx调用 guardAdminRoute()
用户与菜单初始化features/auth/use-auth.tsinitAuth() 请求用户信息,再调用 initMenus(data) 生成菜单。
权限判断features/router/guard.ts未登录跳登录页;用户初始化失败清理认证;静态权限和已授权路由不通过时跳 403;外链路由打开新窗口。

这条链路保证了应用壳可以先完成基础 Provider 挂载,再根据登录态和用户权限决定可见路由、菜单和默认首页。

最小可用示例

新增一次性运行时初始化

如果要接入一个只需要启动一次、并且不依赖 React Provider 的运行时能力,放在 bootstrap.tsx 更合适。

function setupAppMetrics() {
  if (!import.meta.env.PROD) return;

  // 初始化生产环境埋点、监控或错误上报。
}

async function setupApp() {
  const container = document.getElementById('app');

  if (!container) return;

  setupTheme({ buildTime: BUILD_TIME });
  setupAdminLayouts({
    // 保持现有布局配置
  });
  setupAdminPlugins();
  setupAppMetrics();

  await setupI18n();

  const root = createRoot(container);
  root.render(
    <ErrorBoundary FallbackComponent={FallbackRender}>
      <App />
    </ErrorBoundary>
  );
}

判断标准:它是否需要在 React 渲染前完成,是否只运行一次,是否不依赖组件生命周期。如果答案都是“是”,才考虑放进 bootstrap.tsx

新增一个 Provider

如果能力依赖 React context、hooks 或组件生命周期,放在 App.tsx 的 Provider 组合里。

import type { ReactNode } from 'react';

interface AppFeatureProviderProps {
  /** 需要挂载到功能 Provider 下的应用内容。 */
  children: ReactNode;
}

const AppFeatureProvider = (props: AppFeatureProviderProps) => {
  const { children } = props;

  return <FeatureProvider>{children}</FeatureProvider>;
};

const App = () => (
  <Provider>
    <AntdProvider>
      <AppFeatureProvider>
        <NotificationProvider soundUrl={wechatStyleNotification}>
          <LazyAnimate>
            <RouterProvider />
            <GlobalEffect />
          </LazyAnimate>
        </NotificationProvider>
      </AppFeatureProvider>
    </AntdProvider>
  </Provider>
);

判断标准:它是否必须包住页面组件,是否依赖 hooks,是否需要响应主题、语言、用户、路由等状态。如果答案是“是”,不要放进 bootstrap.tsx

常见误区

误区正确做法
把所有启动逻辑都放进 main.tsxmain.tsx 只做环境相关入口加载;应用初始化放在 bootstrap.tsx
在组件里第一次读取主题时才初始化主题setupTheme 必须在 React 渲染前完成,否则主题 atom 初始值可能不稳定。
WebAdminLayout 里硬编码菜单、路由模式或动态路由加载这些属于布局运行时配置,应通过 setupAdminLayoutsfeatures/menus 注入。
未初始化 i18n 就渲染 AppsetupI18n() 是异步初始化,当前启动链路会先 await 再 render。
把用户信息和菜单请求提前到 bootstrap.tsx用户信息和菜单依赖登录态,应留在 useAuth、根路由和后台路由守卫里。
修改 NProgress 后只看根路由NProgress 实例由 setupAdminPlugins() 通过 initNProgress 写入 globalConfig,根路由只是消费它。
复制旧 Vue 文档的 Pinia、NaiveUI 或 Elegant Router 说明当前实现以 React、Jotai、Ant Design 和 TanStack Router 为准。

排查顺序

启动相关问题可以按下面顺序缩小范围:

现象先检查
页面完全不挂载index.html 是否存在 #appmain.tsx 是否成功导入 bootstrap.tsx
开发环境 devtools 不出现import.meta.env.DEV 是否为 true,main.tsx 是否先加载了 @skyroc/web-admin-devtools/jotaiApp.tsxAdminDevtools 是否被 lazy 加载。
主题或暗色模式初始值不对setupTheme 是否在 render 前执行,生产环境缓存和 BUILD_TIME 是否符合预期。
菜单或 tabs 异常setupAdminLayoutsrouteModerouteTreemenuCategoriesloadDynamicRoutesstorage 是否正确。
登录后菜单没有生成useAuth().initAuth() 是否成功拿到用户信息,initMenus(data) 是否执行。
语言或日期本地化异常setupI18n() 是否完成,GlobalEffect 是否挂载,syncLocales 是否把语言映射到 Dayjs locale。
路由切换进度条异常setupAdminPlugins() 是否初始化 NProgress,根路由 pages/__root.tsx 是否能读取 globalConfig.nprogress

相关位置

主题继续查看
项目目录边界项目结构
运行、构建和环境模式快速开始
布局和菜单配置布局系统菜单与标签页
路由守卫路由守卫
权限和超级角色权限
主题、语言和图标主题系统国际化与图标

On this page