架构

启动流程

理解 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
        -> AppAntdProvider
        -> 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 主题接入setupThemefeatures/antd/AntdProvider.tsxThemeEffect(来自 @skyroc/web-admin-theme,挂在 GlobalEffect 里)
修改布局、菜单、动态路由和 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('@shell/devtools/jotai');
  }

  await import('./bootstrap');
}

bootstrap();

开发环境会先加载 @shell/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。这个文件注册全局资源:

// apps/admin/src/plugins/assets.ts
import 'uno.css';
import 'virtual:svg-icons-register';
import '@skyroc/web-admin-styles/global.css';

注意第三行来自共享包 @skyroc/web-admin-styles,不是应用本地样式文件——全局 CSS 资产是沉淀在包里的,应用只负责 import。

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

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 等项目级菜单节点。apps/admin 里返回空数组,apps/admin-example 用它插了两条分隔线。
extras注册标准 badge 之外的菜单扩展 UI。apps/adminmenuExtras{}apps/admin-example 注册了 NewFeatureReleaseChannel
permissionSuperRoleVITE_STATIC_SUPER_ROLE 读取超级角色标识。
storage使用 localStg 读写布局、菜单和 tabs 相关缓存。

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

App.tsx 负责 Provider 组合

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

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

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

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

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

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

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

阶段位置行为
Router context 注入features/router/RouterProvider.tsxuseAuth() 读取并注入 7 个字段:initAuthisAuthInitializedisLoggedInuserInfologoutgetHomeRoutehomeRoute
根路由预初始化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 是否先加载了 @shell/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

相关位置

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

Last updated on