@shell/devtools

Admin Shell 内置的开发期调试模块,聚合 TanStack Query / Router 与 Jotai 三套 DevTools

概述

模块@shell/devtools
目录packages/web/admin/devtools
分发随 Admin Shell 源码复制进生成项目,不独立发布
依赖@tanstack/react-devtools@tanstack/react-query-devtools@tanstack/react-router-devtoolsjotai-devtools
入口@shell/devtools@shell/devtools/jotai

三套 DevTools 各自都会往页面右下角塞一个浮动按钮。这个模块把它们收进一个 TanStackDevtools 壳里:Query 和 Router 作为壳的两个 plugin 面板,Jotai 单独渲染但共用同一份主题和一套位置配置。

  • Query 面板:React Query 缓存、请求状态、手动 refetch
  • Router 面板:TanStack Router 的路由树、当前匹配、route context
  • Jotai:atom 检视面板 + Redux DevTools 时间线

使用

import { AdminDevtools } from '@shell/devtools';

<AdminDevtools
  config={{
    jotai: {
      name: 'My Admin',
      panel: true,
      position: 'bottom-left',
      timeline: true,
      triggerOffset: { left: 248 }
    },
    position: 'bottom-right',
    query: true,
    router: true,
    theme: 'dark'
  }}
  queryClient={queryClient}
  router={router}
  store={globalStore}
/>;

三个实例参数都是可选的,但不传就没有对应面板queryClient 缺席时 Query 面板不注册,router 缺席时 Router 面板不注册,store 缺席时 Jotai 读的是默认 store。

主入口导出

类别符号
组件AdminDevtools
类型AdminDevtoolsConfigAdminDevtoolsPropsAdminJotaiDevtoolsConfigAdminJotaiDevtoolsTriggerOffset

配置项

interface AdminDevtoolsConfig {
  /** 总开关,只有显式传 false 才整体不渲染 */
  enabled?: boolean;
  /** false 关闭 Jotai;传对象做细粒度配置 */
  jotai?: boolean | AdminJotaiDevtoolsConfig;
  /** TanStack 壳的触发器位置,默认 'bottom-right' */
  position?: TanStackDevtoolsConfig['position'];
  /** 启用 Query 面板,默认开 */
  query?: boolean;
  /** 启用 Router 面板,默认开 */
  router?: boolean;
  /** 三套面板共用的主题 */
  theme?: 'dark' | 'light';
}

interface AdminJotaiDevtoolsConfig {
  /** Redux DevTools 连接名,默认 'skyroc-admin' */
  name?: string;
  /** 内嵌 atom 检视面板,默认开 */
  panel?: boolean;
  /** 面板位置,默认 'bottom-left' */
  position?: DevToolsProps['position'];
  /** Redux DevTools 时间线记录,默认开 */
  timeline?: boolean;
  /** 浮动 trigger 的偏移,避开 sider 等应用外框 */
  triggerOffset?: AdminJotaiDevtoolsTriggerOffset;
}

interface AdminJotaiDevtoolsTriggerOffset {
  bottom?: number | string;
  left?: number | string;
  right?: number | string;
  top?: number | string;
}

几个容易踩的默认值语义:

字段判定方式结果
enabled=== false 才关不传即渲染
query / router!== false 即开但还要对应实例存在
jotai=== false 才关true 等价于传 {}
panel / timeline!== false 即开两个都关掉时整块 Jotai 不渲染

theme 只在根配置上,不在 jotai 里——Jotai 面板的主题是从根配置继承下去的。

triggerOffset 的数字会补成 px,字符串原样写入,最终落到四个 CSS 变量上:

--skyroc-jotai-devtools-trigger-bottom
--skyroc-jotai-devtools-trigger-left
--skyroc-jotai-devtools-trigger-right
--skyroc-jotai-devtools-trigger-top

子入口 @shell/devtools/jotai

import '@shell/devtools/jotai';

纯 side-effect,内容就是 import 'jotai-devtools/utils',作用是让之后每个 atom() 调用自动带上 debugLabel必须在任何 atom 创建之前执行,所以 apps/admin/src/main.tsx 把它放在动态 import ./bootstrap 之前:

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

  await import('./bootstrap');
}

顺序反了的话,先创建的那批 atom 在面板里会显示成 <no debugLabel>

在 apps/admin 的接入

// App.tsx
const AdminDevtools = import.meta.env.DEV
  ? lazy(() => import('@shell/devtools').then(mod => ({ default: mod.AdminDevtools })))
  : (_props: AdminDevtoolsProps) => null;
//                ^^^ 生产环境是个 no-op 组件,整个模块不会进入生产构建

const Devtools = () => {
  const { darkMode } = useSettingsTheme();

  const config = useMemo<AdminDevtoolsProps['config']>(
    () => ({ ...globalConfig.devtools, theme: darkMode ? 'dark' : 'light' }),
    [darkMode]
  );

  return (
    <Suspense fallback={null}>
      <AdminDevtools
        config={config}
        queryClient={queryClient}
        router={router}
        store={globalStore}
      />
    </Suspense>
  );
};

静态部分写在 globalConfig.devtools 里,theme 在运行时跟着 useSettingsThemedarkMode 合并进去:

devtools: {
  jotai: {
    name: import.meta.env.VITE_APP_TITLE,
    panel: true,
    position: 'bottom-left',
    timeline: true,
    triggerOffset: {
      left: defaultThemeSettings.sider.width + 8
    }
  },
  position: 'bottom-right',
  query: true,
  router: true
} satisfies AdminDevtoolsConfig

triggerOffset.left 取的是 sider 宽度加 8px,让 Jotai 的浮动按钮正好落在侧边栏右侧而不是压在上面。

设计要点

  • 统一面板:三套 DevTools 各占一个浮动按钮太挤,收进一个壳里;
  • 可定位:TanStack 壳和 Jotai 面板各有独立位置,triggerOffset 再补一层像素级微调;
  • 零生产成本import.meta.env.DEV 三元 + lazy 动态 import + tree-shaking 三层保护,生产 bundle 里不含任何 devtools 代码;
  • 懒加载到底:模块内部四个 DevTools 组件本身也都是 lazy(),Jotai 的样式表也在 lazy 工厂里才 import。

Last updated on