Skyroc Web Kit

@skyroc/web-admin-runtime

Skyroc Admin 的运行时启动助手包,统一 Dayjs、Iconify offline provider、NProgress 和应用版本更新检测的可配置初始化

概述

@skyroc/web-admin-runtime 是 Skyroc Admin 应用的运行时启动助手包。它负责沉淀那些在每个后台应用启动阶段都会出现、但又不应该散落在业务入口里的初始化逻辑。

它目前覆盖四类运行时能力:

  • Dayjs 初始化:注册 localeData 插件,并支持宿主应用传入一次性的初始语言同步回调。
  • Iconify offline provider 初始化:把宿主应用的 Iconify API 地址注册到 @iconify/react
  • NProgress 初始化:统一进度条容器、动画速度和实例回传。
  • 应用版本更新检测:定时读取最新 index.html 中的 buildTime meta,发现新版本后交给宿主应用展示更新提示。

这个包的目标不是接管整个应用启动流程。它只提供可复用的运行时原子能力和一个聚合入口。真正的启动顺序、环境变量读取、通知 UI、router 行为、i18n 初始化和样式副作用,仍然由宿主应用负责。

适用场景

推荐在下面场景使用:

  • 多个后台应用都需要相同的 Dayjs、Iconify、NProgress 和版本更新检测初始化。
  • 应用希望保留自己的 bootstrap.tsx,但不想在里面逐个调用底层初始化函数。
  • 应用需要像 @skyroc/web-admin-vite 一样,通过 false | options 控制运行时插件是否启用和如何配置。
  • 运行时能力需要复用,但宿主应用的通知组件、路由跳转、环境变量和文案仍然不同。

不推荐把下面内容放进这个包:

  • uno.cssvirtual:svg-icons-registerglobal.css 等样式或构建虚拟模块副作用。
  • 宿主应用的 router 实例。
  • Ant Design notification 的具体渲染内容。
  • i18n 资源加载和语言状态初始化。
  • 业务配置对象,如 globalConfiglocalStg、菜单配置和路由树。

这些内容属于应用启动编排,不属于可复用 runtime helper。

分层关系

apps/admin
  ├─ bootstrap.tsx                 组织启动顺序
  ├─ plugins/index.ts              把应用 env / UI / router 适配成 runtime options
  ├─ plugins/app.tsx               应用版本更新提示的通知 UI 与路由行为
  └─ plugins/assets.ts             样式和 Vite 虚拟模块副作用入口

@skyroc/web-admin-runtime
  ├─ setupAdminRuntimePlugins()    运行时插件聚合入口
  ├─ setupDayjs()                  Dayjs localeData 与可选初始语言同步
  ├─ setupIconifyOffline()         Iconify API provider 注册
  ├─ setupNProgress()              NProgress 配置与实例回传
  ├─ setupAppVersionNotification() index.html buildTime 更新检测
  └─ getHtmlBuildTime()            从 HTML meta 中读取 buildTime

@skyroc/web-admin-vite
  └─ 注入 BUILD_TIME,并在 index.html 写入 buildTime meta

这三个层次的关系很重要:

  • @skyroc/web-admin-vite 负责构建期,把 BUILD_TIME 注入 JS,并把构建时间写进 HTML。
  • @skyroc/web-admin-runtime 负责运行期,读取最新 HTML 的构建时间并触发回调。
  • apps/admin 负责应用语义,决定什么时候初始化、如何提示用户、点击确认后怎么刷新。

快速开始

宿主应用通常只需要在自己的 plugins/index.ts 中集中配置:

import { setupAdminRuntimePlugins } from '@skyroc/web-admin-runtime';
import type { SetupDayjsOptions, SetupIconifyOfflineOptions, SetupNProgressOptions } from '@skyroc/web-admin-runtime';

import { initNProgress } from '@/config';

import { createAdminAppVersionNotificationPluginOptions } from './app';

const adminDayjsPluginOptions = {
  withLocaleData: true
} satisfies SetupDayjsOptions;

const adminIconifyOfflinePluginOptions = {
  apiUrl: import.meta.env.VITE_ICONIFY_URL
} satisfies SetupIconifyOfflineOptions;

const adminNProgressPluginOptions = {
  easing: 'ease',
  onReady: initNProgress,
  parent: '.root',
  speed: 500
} satisfies SetupNProgressOptions;

export function setupAdminPlugins() {
  return setupAdminRuntimePlugins({
    dayjs: adminDayjsPluginOptions,
    appVersionNotification: createAdminAppVersionNotificationPluginOptions(),
    iconifyOffline: adminIconifyOfflinePluginOptions,
    nprogress: adminNProgressPluginOptions
  });
}

然后在应用入口中调用:

import { createRoot } from 'react-dom/client';

import App from './App';
import { setupI18n } from './locales';
import { setupAdminPlugins } from './plugins';
import './plugins/assets';

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

  if (!container) return;

  setupAdminPlugins();

  await setupI18n();

  createRoot(container).render(<App />);
}

setupApp();

setupI18n() 仍然单独保留。它是异步初始化,并且通常要在 React render 前完成。语言切换后的第三方同步由宿主应用的 LangEffect.onLocaleChange 承接,runtime 包不应该把这个时序吞进一个看似通用的 helper。

更完整的多语言接入、资源维护和语言副作用说明见 @skyroc/web-admin-i18n

聚合 API

setupAdminRuntimePlugins

setupAdminRuntimePlugins 是推荐的公共入口。它接收一组运行时插件配置:

import { setupAdminRuntimePlugins } from '@skyroc/web-admin-runtime';

setupAdminRuntimePlugins({
  dayjs: {},
  iconifyOffline: {},
  nprogress: {},
  appVersionNotification: {
    currentBuildTime: BUILD_TIME,
    enabled: import.meta.env.PROD,
    onUpdateAvailable(context) {
      // host app notification
    }
  }
});

每一项都支持两种形态:

配置值含义
false关闭该运行时插件。
options启用该运行时插件,并把 options 传给对应的 setupXxx 函数。

例如:

setupAdminRuntimePlugins({
  dayjs: {
    withLocaleData: false
  },
  iconifyOffline: false,
  nprogress: {
    parent: '#app',
    speed: 300
  },
  appVersionNotification: false
});

默认行为:

插件默认行为
dayjs默认启用,等价于 setupDayjs()
nprogress默认启用,等价于 setupNProgress()
iconifyOffline默认启用,但没有 apiUrl 时不会注册 provider。
appVersionNotification默认不启用,必须显式传入完整 options。

appVersionNotification 默认不启用,是因为它必须知道当前构建时间、是否启用、错误处理和更新提示回调。runtime 包无法替宿主应用决定这些语义。

清理函数

setupAdminRuntimePlugins 会返回一个清理函数:

const cleanup = setupAdminRuntimePlugins({
  appVersionNotification: {
    currentBuildTime: BUILD_TIME,
    enabled: import.meta.env.PROD,
    onUpdateAvailable() {}
  }
});

cleanup();

当前主要用于清理应用更新检测创建的 interval 和 visibilitychange 监听。普通 SPA 启动只会执行一次,通常不需要手动调用;测试、微前端卸载或重复挂载场景可以调用它。

单项 API

聚合入口适合应用启动。单项 API 适合测试、定制启动器或只需要某一个 runtime 能力的应用。

setupDayjs

import { setupDayjs } from '@skyroc/web-admin-runtime';

setupDayjs({
  withLocaleData: true
});

配置项:

字段默认值说明
withLocaleDatatrue是否注册 dayjs/plugin/localeData
syncLocaleundefined宿主应用在 runtime 初始化时同步当前语言到 Dayjs 的回调。

setupDayjs 只注册 Dayjs 插件和调用一次初始化同步回调。syncLocale 不接收语言参数,也不处理后续语言切换。具体支持哪些语言、语言 key 如何映射到 Dayjs locale,以及语言切换后的响应式同步,仍然由宿主应用控制。当前 admin 应用把响应式同步集中在 LangEffect.onLocaleChange

示例:

import { locale } from 'dayjs';
import 'dayjs/locale/en';
import 'dayjs/locale/zh-cn';

function syncLocales(lang: 'zh-CN' | 'en-US') {
  const localeMap = {
    'zh-CN': 'zh-cn',
    'en-US': 'en'
  };

  locale(localeMap[lang]);
}

setupIconifyOffline

import { setupIconifyOffline } from '@skyroc/web-admin-runtime';

setupIconifyOffline({
  apiUrl: import.meta.env.VITE_ICONIFY_URL,
  provider: ''
});

配置项:

字段默认值说明
apiUrlundefinedIconify API 地址。为空时不注册 provider。
provider''Iconify provider 名称。空字符串表示默认 provider。

这个函数只调用 @iconify/reactaddAPIProvider。它不处理本地 SVG sprite,也不导入 virtual:svg-icons-register

本地 SVG sprite 仍然应该留在应用侧:

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

setupNProgress

import { setupNProgress } from '@skyroc/web-admin-runtime';

setupNProgress({
  easing: 'ease',
  parent: '.root',
  speed: 500,
  onReady(nprogress) {
    // save instance to host config if needed
  }
});

配置项:

字段默认值说明
easing'ease'NProgress 动画曲线。
parent'.root'进度条挂载容器选择器。
speed500动画速度,单位毫秒。
onReadyundefined接收初始化后的 NProgress 实例。

onReady 常用于把实例写回应用配置:

import type { NProgress } from 'nprogress';

const progress = {
  instance: null as NProgress | null
};

export function initNProgress(nprogress: NProgress) {
  progress.instance = nprogress;
}

setupAppVersionNotification

import { setupAppVersionNotification } from '@skyroc/web-admin-runtime';

setupAppVersionNotification({
  baseUrl: import.meta.env.VITE_BASE_URL || '/',
  currentBuildTime: BUILD_TIME,
  enabled: import.meta.env.PROD,
  interval: 3 * 60 * 1000,
  onError(error) {
    console.error('Failed to check app version:', error);
  },
  onUpdateAvailable({ latestBuildTime, markPromptClosed }) {
    // show host app notification
  }
});

配置项:

字段默认值说明
baseUrl'/'读取 HTML 入口的 base url。
currentBuildTime必填当前 JS bundle 中注入的构建时间。
enabled必填是否启用更新检测。通常只在生产环境启用。
interval180000检测间隔,默认 3 分钟。
onErrorundefined读取 HTML 或解析失败时的错误回调。
onUpdateAvailable必填检测到新版本时由宿主应用展示提示。

onUpdateAvailable 收到的参数:

字段说明
currentBuildTime当前运行中的 bundle 构建时间。
latestBuildTime最新 index.html 中读取到的构建时间。
markPromptClosed宿主应用关闭提示后必须调用,用来允许后续检测再次提示。

典型宿主应用实现:

import type { SetupAppVersionNotificationOptions } from '@skyroc/web-admin-runtime';

import { destroyNotification, globalConfig, showNotification } from '@/config';
import { router } from '@/features/router';

import { $t } from '../locales';

const UPDATE_NOTIFICATION_KEY = 'update-notification';

export function createAdminAppVersionNotificationPluginOptions(): SetupAppVersionNotificationOptions {
  return {
    baseUrl: import.meta.env.VITE_BASE_URL || '/',
    currentBuildTime: BUILD_TIME,
    enabled: globalConfig.automaticallyDetectUpdate && import.meta.env.PROD,
    onError(error) {
      console.error('Failed to get HTML build time:', error);
    },
    onUpdateAvailable({ markPromptClosed }) {
      function handleCancel() {
        destroyNotification(UPDATE_NOTIFICATION_KEY);
        markPromptClosed();
      }

      function handleOk() {
        router.navigate({ to: '.' });
      }

      showNotification({
        actions: (
          <div className="w-325px flex justify-end gap-3">
            <AButton key="cancel" onClick={handleCancel}>
              {$t('system.updateCancel')}
            </AButton>
            <AButton key="ok" type="primary" onClick={handleOk}>
              {$t('system.updateConfirm')}
            </AButton>
          </div>
        ),
        description: $t('system.updateContent'),
        key: UPDATE_NOTIFICATION_KEY,
        onClose: markPromptClosed,
        title: $t('system.updateTitle')
      });
    }
  };
}

注意事项:

  • enabledfalse 时不会注册任何 interval 或事件监听。
  • 非浏览器环境没有 document 时会直接 no-op,适合 SSR 或测试环境。
  • 当前 helper 读取的是 <meta name="buildTime" content="...">。如果 Vite HTML 插件改了 meta 名称,更新检测也需要同步调整。
  • markPromptClosed 必须在用户关闭提示时调用,否则 runtime 会认为提示仍然打开,后续检测不会重复弹出。

getHtmlBuildTime

getHtmlBuildTime 是底层读取函数,适合测试或自定义更新策略:

import { getHtmlBuildTime } from '@skyroc/web-admin-runtime';

const latestBuildTime = await getHtmlBuildTime({
  baseUrl: '/',
  fetcher: window.fetch.bind(window)
});

它会请求:

${baseUrl}index.html?time=${Date.now()}

然后从 HTML 中读取:

<meta name="buildTime" content="2026-05-28 10:00:00" />

返回值:

返回值说明
string成功读取到 buildTime。
null没有 fetch、响应失败、或 HTML 中没有 buildTime meta。

推荐启动顺序

在完整后台应用中,推荐把 runtime 初始化放在主题和 layout 配置之后、i18n 初始化之前:

setupTheme({
  buildTime: BUILD_TIME
});

setupAdminLayouts({
  routeTree,
  storage
  // ...
});

setupAdminPlugins();

await setupI18n();

createRoot(container).render(<App />);

这个顺序的考虑:

  • setupTheme() 要在任何组件读取主题 atom 前完成。
  • setupAdminLayouts() 要在路由、菜单和 layout hooks 读取配置前完成。
  • setupAdminPlugins() 注册 Dayjs、Iconify、NProgress 和应用更新检测等同步 runtime 能力。
  • setupI18n() 是异步初始化,仍然需要在 render 前完成。
  • GlobalEffect 中的 LangEffect.onLocaleChange 负责在 React 挂载后统一同步 Dayjs、document.lang 等第三方语言副作用。
  • 应用更新检测虽然在 setupAdminPlugins() 中注册,但真正展示通知发生在后续 interval 中,此时 React Provider 已经挂载。

如果某个宿主应用的通知实现要求 Provider 必须已经初始化,仍然可以把 setupAdminRuntimePlugins 拆成应用内部的两个调用点。这个拆分属于应用编排,不需要暴露成 runtime 包的公共 API。

与其他包的关系

责任
@skyroc/web-admin-runtime运行时初始化 helper:Dayjs、Iconify provider、NProgress、应用更新检测。
@skyroc/web-admin-i18ni18next 初始化、语言状态、语言切换 UI 和语言变化副作用入口。
@skyroc/web-admin-vite构建和开发配置:注入 BUILD_TIME、HTML meta、Vite 插件、代理、别名、产物规则。
@skyroc/web-admin-theme主题状态、Ant Design Provider、暗色模式、主题副作用。
@skyroc/web-admin-layouts后台应用壳、菜单、tabs、权限、布局状态和业务插槽。
apps/admin宿主应用编排:env、router、通知 UI、i18n、assets、业务配置。

与 admin-i18n 的边界

@skyroc/web-admin-runtime@skyroc/web-admin-i18n 都会出现在启动链路里,但它们处理的是两类不同问题。

问题归属原因
注册 Dayjs localeData 插件@skyroc/web-admin-runtime这是同步 runtime 初始化能力,和当前语言无关。
解析当前语言、加载翻译资源@skyroc/web-admin-i18n这是异步 i18next 初始化流程。
语言切换按钮和当前语言状态@skyroc/web-admin-i18n依赖 React/Jotai/i18next 状态。
Dayjs 的 zh-CN -> zh-cn 映射apps/admin不同第三方库的 locale key 是应用级约定。
语言切换后同步 DayjsLangEffect.onLocaleChange这是 React 挂载后的响应式副作用。

因此,setupAdminPlugins() 可以在 render 前同步注册 Dayjs 插件,但不要试图在 runtime 包里读取当前语言或监听语言变化。语言状态由 setupI18n() 初始化,后续变化由 LangEffect 统一同步。

一个常见误区是把 apps/admin/src/plugins 整个目录都搬进 runtime 包。这个目录里混合了多种不同边界:

文件类型应放位置原因
Dayjs / Iconify / NProgress 初始化@skyroc/web-admin-runtime可复用,和具体业务无关。
应用更新检测算法@skyroc/web-admin-runtime可复用,但 UI 回调由宿主注入。
更新提示 JSX、router 跳转、文案apps/admin强依赖宿主 UI、路由和 i18n。
uno.cssvirtual:svg-icons-register、全局样式apps/admin构建副作用和应用样式入口。
setupI18n()apps/admin / @skyroc/web-admin-i18n异步语言资源初始化,不属于 runtime 插件聚合器。

配置参考

SetupAdminRuntimePluginsOptions

interface SetupAdminRuntimePluginsOptions {
  appVersionNotification?: false | SetupAppVersionNotificationOptions;
  dayjs?: false | SetupDayjsOptions;
  iconifyOffline?: false | SetupIconifyOfflineOptions;
  nprogress?: false | SetupNProgressOptions;
}

SetupDayjsOptions

interface SetupDayjsOptions {
  syncLocale?: () => void;
  withLocaleData?: boolean;
}

SetupIconifyOfflineOptions

interface SetupIconifyOfflineOptions {
  apiUrl?: string;
  provider?: string;
}

SetupNProgressOptions

interface SetupNProgressOptions {
  easing?: string;
  onReady?: (nprogress: NProgress) => void;
  parent?: string;
  speed?: number;
}

SetupAppVersionNotificationOptions

interface SetupAppVersionNotificationOptions {
  baseUrl?: string;
  currentBuildTime: string;
  enabled: boolean;
  interval?: number;
  onError?: (error: unknown) => void;
  onUpdateAvailable: (context: AppUpdateAvailableContext) => void;
}

interface AppUpdateAvailableContext {
  currentBuildTime: string;
  latestBuildTime: string;
  markPromptClosed: () => void;
}

测试与验证

修改 @skyroc/web-admin-runtime 后,建议至少执行:

pnpm --filter @skyroc/web-admin-runtime test
pnpm --filter @skyroc/web-admin-runtime typecheck
pnpm --filter @skyroc/web-admin-runtime build
pnpm --filter skyroc-admin typecheck
pnpm --filter skyroc-admin build:test

修改 docs/web-kit-docs 文档后,建议执行:

pnpm --dir docs/web-kit-docs types:check
pnpm --dir docs/web-kit-docs build

如果只改文案,也可以先做更轻量的检查:

pnpm exec oxfmt --check docs/web-kit-docs/content/docs/admin-runtime.mdx docs/web-kit-docs/content/docs/meta.json

常见问题

为什么 app update 不默认启用?

它需要 currentBuildTimeenabledonUpdateAvailable。这些值都来自宿主应用,runtime 包不能替应用决定生产环境开关、通知样式、跳转行为和文案。

为什么 setupI18n() 不放进 setupAdminRuntimePlugins()

setupI18n() 是异步流程,会加载语言资源、初始化 i18next,并写入语言状态。第三方语言同步属于 React 挂载后的响应式副作用,应集中放在 LangEffect.onLocaleChange。它属于应用启动编排,不能被一个同步 runtime helper 隐藏。

为什么 assets.ts 不属于 runtime 包?

assets.ts 通常包含样式和 Vite 虚拟模块:

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

这些导入依赖宿主应用的构建配置和样式入口。把它们放进 runtime 包会让包依赖具体应用目录和 Vite 插件存在性,边界会变脏。

为什么还要保留 app 侧 plugins/index.ts

@skyroc/web-admin-runtime 提供通用能力,apps/admin/src/plugins/index.ts 负责把本应用的 env、router、通知 UI 和 NProgress 实例回传组合成具体配置。

这层适配很薄,但它是必要边界。没有它,runtime 包就会被迫读取 import.meta.env、引用应用 router、依赖 Ant Design notification,最终变成另一个 app-local 包。

更新提示点击确认应该做什么?

runtime 包只告诉宿主应用有新版本。确认动作由宿主应用决定,常见策略有:

  • window.location.reload(),强制刷新整个页面。
  • router.navigate({ to: '.' }),让当前路由重新加载。
  • 跳转到首页或登录页。

如果当前应用有路由缓存、tab 缓存或权限状态,建议在宿主应用中统一处理,而不是放到 runtime 包里。

On this page