@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中的buildTimemeta,发现新版本后交给宿主应用展示更新提示。
这个包的目标不是接管整个应用启动流程。它只提供可复用的运行时原子能力和一个聚合入口。真正的启动顺序、环境变量读取、通知 UI、router 行为、i18n 初始化和样式副作用,仍然由宿主应用负责。
适用场景
推荐在下面场景使用:
- 多个后台应用都需要相同的 Dayjs、Iconify、NProgress 和版本更新检测初始化。
- 应用希望保留自己的
bootstrap.tsx,但不想在里面逐个调用底层初始化函数。 - 应用需要像
@skyroc/web-admin-vite一样,通过false | options控制运行时插件是否启用和如何配置。 - 运行时能力需要复用,但宿主应用的通知组件、路由跳转、环境变量和文案仍然不同。
不推荐把下面内容放进这个包:
uno.css、virtual:svg-icons-register、global.css等样式或构建虚拟模块副作用。- 宿主应用的
router实例。 - Ant Design notification 的具体渲染内容。
- i18n 资源加载和语言状态初始化。
- 业务配置对象,如
globalConfig、localStg、菜单配置和路由树。
这些内容属于应用启动编排,不属于可复用 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
});配置项:
| 字段 | 默认值 | 说明 |
|---|---|---|
withLocaleData | true | 是否注册 dayjs/plugin/localeData。 |
syncLocale | undefined | 宿主应用在 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: ''
});配置项:
| 字段 | 默认值 | 说明 |
|---|---|---|
apiUrl | undefined | Iconify API 地址。为空时不注册 provider。 |
provider | '' | Iconify provider 名称。空字符串表示默认 provider。 |
这个函数只调用 @iconify/react 的 addAPIProvider。它不处理本地 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' | 进度条挂载容器选择器。 |
speed | 500 | 动画速度,单位毫秒。 |
onReady | undefined | 接收初始化后的 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 | 必填 | 是否启用更新检测。通常只在生产环境启用。 |
interval | 180000 | 检测间隔,默认 3 分钟。 |
onError | undefined | 读取 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')
});
}
};
}注意事项:
enabled为false时不会注册任何 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-i18n | i18next 初始化、语言状态、语言切换 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 是应用级约定。 |
| 语言切换后同步 Dayjs | LangEffect.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.css、virtual: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 不默认启用?
它需要 currentBuildTime、enabled 和 onUpdateAvailable。这些值都来自宿主应用,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 包里。