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 初始化和样式副作用,仍然由宿主应用负责。
推荐在下面场景使用:
bootstrap.tsx,但不想在里面逐个调用底层初始化函数。@skyroc/web-admin-vite 一样,通过 false | options 控制运行时插件是否启用和如何配置。不推荐把下面内容放进这个包:
uno.css、virtual:svg-icons-register、global.css 等样式或构建虚拟模块副作用。router 实例。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。
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 适合测试、定制启动器或只需要某一个 runtime 能力的应用。
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]);
}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';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;
}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 或测试环境。<meta name="buildTime" content="...">。如果 Vite HTML 插件改了 meta 名称,更新检测也需要同步调整。markPromptClosed 必须在用户关闭提示时调用,否则 runtime 会认为提示仍然打开,后续检测不会重复弹出。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、业务配置。 |
@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 插件聚合器。 |
interface SetupAdminRuntimePluginsOptions {
appVersionNotification?: false | SetupAppVersionNotificationOptions;
dayjs?: false | SetupDayjsOptions;
iconifyOffline?: false | SetupIconifyOfflineOptions;
nprogress?: false | SetupNProgressOptions;
}interface SetupDayjsOptions {
syncLocale?: () => void;
withLocaleData?: boolean;
}interface SetupIconifyOfflineOptions {
apiUrl?: string;
provider?: string;
}interface SetupNProgressOptions {
easing?: string;
onReady?: (nprogress: NProgress) => void;
parent?: string;
speed?: number;
}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/docs 文档后,建议执行:
pnpm --dir docs/docs types:check
pnpm --dir docs/docs build如果只改文案,也可以先做更轻量的检查:
pnpm exec oxfmt --check docs/docs/content/docs/web/admin-runtime.mdx docs/docs/content/docs/web/meta.json它需要 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 插件存在性,边界会变脏。
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 包里。
Last updated on