@nvoo/core
v1.0.0
Published
Vite MPA core runtime — lightweight app bootstrap with tracker, error pages, and auth guard
Readme
@nvoo/core
Vue 3 微内核运行时 —— 生命周期调度 + 插件化基础设施注入。
不依赖任何 UI 框架,仅依赖 Vue 3 + vue-router + Pinia(均为 peerDependencies)。
目录结构
src/
├── kernel.ts # createKernel() 工厂,微内核入口
├── main.ts # 公共导出
├── env.d.ts # 全局类型声明(Window.__APP_BASE__ 等)
├── utils.ts # 公共工具(NOT_FOUND_NAME / FORBIDDEN_NAME / 错误页路由注入)
│
├── core/ # 内核内部组件(不对外暴露)
│ ├── event-bus.ts # EventBus — 类型安全事件总线,跨插件通信唯一通道
│ ├── plugin-manager.ts # PluginManager — 插件注册 + 依赖校验 + 拓扑排序
│ └── stage-runner.ts # StageRunner — 生命周期阶段串行异步调度器
│
├── plugins/ # 内置插件工厂(独立导出,按需引入)
│ ├── pinia.ts # createPiniaPlugin — 状态管理注入
│ ├── router.ts # createRouterPlugin — MPA 路由 + ROUTER_READY 事件
│ ├── auth.ts # createAuthPlugin — 鉴权守卫 + 动态路由 + AUTH_TOKEN_CHANGED 事件
│ ├── url-token.ts # createUrlTokenPlugin — URL Token 提取 + Cookie 存储
│ ├── page-style.ts # createPageStylePlugin — 页面级原生样式管理 + usePageStyle 运行时 Hook
│ └── i18n.ts # createI18nPlugin — 国际化插件,动态加载 vue-i18n,与 virtual:i18n 配合使用
│
├── utils/ # 工具与基础设施
│ ├── cookie-storage.ts # createDefaultCookieStorage — 基于 document.cookie 的默认实现
│ └── (详见 src/utils.ts)
│
├── types/ # 类型定义(契约层)
│ ├── plugin.ts # CorePlugin / CoreContext — 插件接口 + 上下文
│ ├── events.ts # CORE_EVENTS 常量 + CoreEvents 类型映射(typeof 提取)
│ ├── kernel.ts # AppKernel / CreateKernelOptions — 内核接口
│ ├── storage.ts # CookieStorage / CookieSetOptions — 存储抽象
│ └── global.d.ts # Window 全局类型增强
│
└── pages/ # 内置页面组件(按需加载,构建时自动 code-split)
├── NotFound.vue # 404 catch-all 兜底页
└── Forbidden.vue # 403 无权限页快速开始
pnpm add @nvoo/coreimport { createKernel, createPiniaPlugin, createRouterPlugin } from "@nvoo/core";
import App from "./App.vue";
const kernel = createKernel({ rootComponent: App });
kernel.use(createPiniaPlugin()).use(createRouterPlugin({ routes }));
await kernel.start();架构
┌──────────────────────────────────────┐
│ AppKernel (微内核) │
│ use(plugin) → 注册插件 + 依赖排序 │
│ start() → 调度生命周期 │
└────────────┬─────────────────────────┘
│ .use() 注入
┌────────┼────────┬────────┐
│ │ │ │
┌───▼───┐ ┌──▼──┐ ┌───▼───┐ ┌──▼─────┐
│ Pinia │ │Router│ │ Auth │ │Tracker │
│Plugin │ │Plugin│ │Plugin │ │Plugin │
└───────┘ └─────┘ └───────┘ └────────┘
插件间通过 CoreContext.emit/on/off 事件总线通信,严禁私有引用。生命周期
onBeforeInit → onInited → onBeforeMount → app.mount() → onMounted → (运行中) → onUnmounted所有钩子均为 void | Promise<void>(支持异步),串行执行。单个插件钩子失败不中断调度链,错误通过 error:occurred 事件上报。
API
createKernel(options): AppKernel
创建微内核实例。
import { createKernel } from "@nvoo/core";
const kernel = createKernel({
rootComponent: App, // 必填:Vue 根组件
mountTo: "#app" // 可选:挂载目标选择器,默认 "#app"
});AppKernel
| 成员 | 类型 | 说明 |
| ------------- | ------------------------------ | -------------------------------------------------------- |
| use(plugin) | (plugin: CorePlugin) => this | 注册插件,支持链式调用。必须在 start() 前调用 |
| start() | () => Promise<void> | 启动内核,按生命周期阶段依次执行所有插件钩子,最后 mount |
| plugins | ReadonlyArray<CorePlugin> | 只读,已注册插件列表 |
| app | App | 只读,Vue 应用实例(start() 之后可用) |
CorePlugin
所有插件必须实现此接口。
import type { CorePlugin, CoreContext } from "@nvoo/core";
const myPlugin: CorePlugin = {
name: "my-plugin", // 唯一标识
deps: ["pinia", "router"], // 可选:声明依赖,内核确保安装顺序
onBeforeInit(ctx) {
/* Vue App 已创建,插件尚未全部安装 */
},
onInited(ctx) {
/* 所有插件 onBeforeInit 执行完毕 */
},
onBeforeMount(ctx) {
/* 即将 app.mount() */
},
onMounted(ctx) {
/* app.mount() 已执行,DOM 可用 */
},
onUnmounted(ctx) {
/* 应用销毁,清理资源 */
}
};CoreContext
插件通过此上下文访问核心能力。严禁跨插件私有引用。
| 成员 | 类型 | 说明 |
| ---------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| app | App | 只读,Vue 应用实例 |
| router | Router \| undefined | Vue Router 实例。由 createRouterPlugin 在 onBeforeInit 写入;依赖 router 的插件可在自身 onBeforeInit 读取(拓扑序保证 router 在前) |
| emit(event, payload) | <K>(event: K, payload: CoreEvents[K]) => void | 发送事件 |
| on(event, handler) | <K>(event: K, handler: (payload: CoreEvents[K]) => void) => void | 监听事件 |
| off(event, handler) | <K>(event: K, handler: (payload: CoreEvents[K]) => void) => void | 取消监听 |
⚠️ 守卫注册时机:要在
beforeEach拦截首次/初始 navigation,必须在 router 插件的onInited(即app.use(router))之前注册 ——vue-router 4在app.use(router)同步段会冻结beforeGuards列表快照。详见下方"路由守卫"章节的时机陷阱说明。
CORE_EVENTS + CoreEvents
事件名常量及载荷类型映射。emit / on / off 通过 typeof CORE_EVENTS 提取字面量类型,实现全自动类型推导。
import { CORE_EVENTS } from "@nvoo/core";
// ctx.emit(CORE_EVENTS.ROUTER_READY, { router })
// ↑ K 推断为 "router:ready",payload 自动约束为 RouterReadyPayload| 常量 | 事件字符串 | 载荷类型 | 触发时机 |
| -------------------- | ---------------------- | --------------------------------- | ----------------------- |
| ROUTER_READY | "router:ready" | { router: Router } | RouterPlugin onInited |
| AUTH_TOKEN_CHANGED | "auth:token-changed" | { token: string \| null } | AuthPlugin token 变化 |
| PAGE_CHANGED | "page:changed" | { path, name?, query, meta } | 每次路由切换后自动触发 |
| ERROR_OCCURRED | "error:occurred" | { message, path, detail?, ... } | 任何插件/应用异常 |
| ROUTER_ERROR | "router:error" | { error: unknown } | 路由导航出错时自动触发 |
扩展事件:在 CORE_EVENTS 常量中新增 key → 定义载荷 interface → 在 CoreEvents 映射类型中添加条件分支,三步完成全链路类型安全。
createPiniaPlugin(options?): CorePlugin
状态管理注入。在 onBeforeInit 创建并安装 Pinia。
import { createPiniaPlugin } from "@nvoo/core";
kernel.use(createPiniaPlugin());
// 或传入自定义实例
kernel.use(createPiniaPlugin({ instance: myPinia }));| 选项 | 类型 | 说明 |
| ---------- | ------- | ---------------------------------------------------- |
| instance | Pinia | 可选,自定义 Pinia 实例,不传则 createPinia() 新建 |
createRouterPlugin(options): CorePlugin
MPA 路由插件。声明 deps: ["pinia"]。
onBeforeInit— 创建 Router、注入错误页路由、写入ctx.router(不调用app.use(router)),让依赖 router 的插件(如 auth)能在自身onBeforeInit注册beforeEach守卫onInited— 调ctx.app.use(router)触发 install 与初始 navigation(此时守卫已就位),随后注册afterEach(emitPAGE_CHANGED)+onError(emitROUTER_ERROR),最后 emitROUTER_READY
拆分两阶段是为了解决 vue-router 4 的初始 navigation 守卫时机问题:install 同步段会冻结
beforeGuards列表,提前到onBeforeInit注册才能被捕获。
import { createRouterPlugin } from "@nvoo/core";
kernel.use(
createRouterPlugin({
routes, // 必填:路由表
base: "/my-app/", // 可选:HTML5 History base,默认取 window.__APP_BASE__,兜底 "/"
scrollBehavior: () => ({ top: 0 }), // 可选:每次导航回到顶部
pages: {
// 可选:自定义错误页面
notFound: () => import("./404.vue"),
forbidden: () => import("./403.vue")
}
})
);| 选项 | 类型 | 默认值 | 说明 |
| ----------------- | ----------------------------------------- | ------------------------------ | ----------------------------------- |
| routes | RouteRecordRaw[] | 必填 | 初始路由表 |
| base | string | window.__APP_BASE__ \|\| "/" | HTML5 History base 路径 |
| scrollBehavior | RouterOptions["scrollBehavior"] | vue-router 默认行为 | 导航后滚动行为,透传给 createRouter |
| pages.notFound | Component \| (() => Promise<Component>) | 内置 NotFound.vue | 404 catch-all 页面 |
| pages.forbidden | Component \| (() => Promise<Component>) | 内置 Forbidden.vue | 403 无权限页面 |
createAuthPlugin(options): CorePlugin
鉴权插件。声明 deps: ["router"],在 onBeforeInit 直接读 ctx.router 注册 beforeEach 守卫(早于 router install,确保被初始 navigation 捕获)。Token 变化时 emit AUTH_TOKEN_CHANGED 并支持动态路由重建。
import { createAuthPlugin } from "@nvoo/core";
kernel.use(
createAuthPlugin({
getToken: () => useAuthStore().token, // 必填:Token 获取器(响应式源头)
loginPath: "/login", // 可选:登录页路径,默认 "/login"
forbiddenPath: "/forbidden", // 可选:无权限跳转路径,默认 "/forbidden"
hasPermission: permissions => {
// 可选:权限判定
return useAuthStore().hasPermission(permissions);
},
createRoutes: hasPermission => {
// 可选:Token 变化时重建路由
return generateRoutes(pages, hasPermission);
}
})
);| 选项 | 类型 | 默认值 | 说明 |
| --------------- | ------------------------------------- | -------------- | ------------------------------------------------------------------------------------------- |
| getToken | () => string \| null | 必填 | Token 获取器,返回 null 表示未登录。内部用 watch(getToken, ...) 响应式监听 |
| loginPath | string | "/login" | 未登录时重定向路径 |
| forbiddenPath | string | "/forbidden" | 无权限时重定向路径 |
| hasPermission | (permissions: string[]) => boolean | — | 权限判定函数。当路由 meta.permissions 非空时触发,返回 false 则跳转 forbiddenPath |
| createRoutes | (hasPermission) => RouteRecordRaw[] | — | Token 变化时重新生成路由表。不提供则 Token 变化仅 emit AUTH_TOKEN_CHANGED,不触发路由重建 |
守卫行为
- 路由
meta.requiresAuth为true时要求登录(默认值即为true,所有路由默认需要鉴权) - 未登录重定向:
loginPath?redirect=<原始路径> - Token 存在且路由
meta.permissions非空时,调用hasPermission校验,失败则跳转forbiddenPath
生命周期清理
onUnmounted 自动执行:移除 beforeEach 守卫 → 停止 watch 监听 → 清空 Router 引用。
路由守卫
路由守卫通过 router.beforeEach 实现,内核提供三种使用方式,按需选择。
方式一:auth 插件内联权限校验(推荐——仅权限场景)
如果只需要登录态 + 权限码校验,直接传入 hasPermission 回调即可,不需要写自定义插件。
kernel.use(
createAuthPlugin({
getToken: () => localStorage.getItem("token"),
loginPath: "/login",
forbiddenPath: "/403",
hasPermission: permissions => {
const userPerms = JSON.parse(localStorage.getItem("permissions") || "[]");
return permissions.some(p => userPerms.includes(p));
}
})
);路由通过 meta 声明权限需求:
// 路由定义中
{ path: "/admin", component: Admin, meta: { permissions: ["admin:read"] } }方式二:自定义插件注册路由守卫(推荐——通用场景)
当守卫逻辑超出鉴权/权限范围时(如埋点、动态标题、灰度路由、动态重定向),编写自定义 CorePlugin。
推荐使用工厂函数模式,通过闭包持有守卫的移除引用,在 onUnmounted 中释放资源。
import type { CorePlugin } from "@nvoo/core";
function createAnalyticsGuard(): CorePlugin {
let removeBeforeEach: (() => void) | null = null;
let removeAfterEach: (() => void) | null = null;
return {
name: "analytics-guard",
deps: ["router"],
onBeforeInit(ctx) {
// deps: ["router"] 保证此时 ctx.router 已由 router 插件写入(但尚未 install)
const router = ctx.router;
if (!router) return;
// beforeEach:导航确认前触发,可阻断或重定向
removeBeforeEach = router.beforeEach((_to, _from) => {
return true; // true = 放行
});
// afterEach:导航完成后触发,适合埋点、标题更新
removeAfterEach = router.afterEach(to => {
document.title = (to.meta.title as string) ?? "默认标题";
console.log(`[analytics] page view: ${to.fullPath}`);
});
},
onUnmounted() {
// 清理守卫,避免内存泄漏
removeBeforeEach?.();
removeAfterEach?.();
}
};
}
// main.ts
kernel.use(createRouterPlugin({ routes: createRoutes() }));
kernel.use(createAnalyticsGuard());关键点:
deps: ["router"]+onBeforeInit读ctx.router—— Router 实例由 router 插件在同阶段写入(拓扑序在前),但尚未 install- ⚠️ 必须在
onBeforeInit注册beforeEach—— 此时早于app.use(router),守卫会被初始 navigation 的 beforeGuards 快照捕获;用ROUTER_READY注册会错过首次导航(详见下方"时机陷阱") afterEach不受上述限制(每次导航都重新收集),但放onBeforeInit同样可行,代码更聚合router.beforeEach()/router.afterEach()返回值为移除函数,保存后在onUnmounted调用释放return true放行;return { path: "/other" }重定向;return false取消导航- 多个
beforeEach按注册顺序执行,任一返回重定向即中断后续
⚠️ 时机陷阱:不要用 ROUTER_READY 注册 beforeEach
ROUTER_READY 事件在 router 插件 onInited 阶段 emit,晚于 ctx.app.use(router)。而 app.use(router) 的同步段里,vue-router 4 已经触发初始 navigation 并把 beforeGuards.list() 快照进了闭包:
onBeforeInit ← router 写 ctx.router(不 install)
← auth / 自定义插件读 ctx.router 注册 beforeEach ✓
onInited ← router 调 app.use(router)
└─ install 同步段:navigate() 快照 beforeGuards.list()
└─ 异步段:await runGuardQueue(快照) ← 守卫在此执行
← router emit ROUTER_READY
└─ 此时再注册 beforeEach 已经太晚 ❌(快照已冻结)症状:SPA 内部跳转正常,硬刷新时守卫不触发(典型表现:登录态/权限过滤在 F5 刷新后失效)。
afterEach 和"只读路由元数据"的场景不受此限制 —— 它们在每次 navigation 都会重新收集,或者通过 PAGE_CHANGED 事件订阅即可。
方式三:auth 插件 + 自定义插件组合
若既需要鉴权守卫又需要自定义逻辑,两个插件并存即可。不同的 beforeEach 按注册顺序执行:
kernel.use(createRouterPlugin({ routes: createRoutes() }));
kernel.use(
createAuthPlugin({
// 鉴权守卫先注册
getToken: () => localStorage.getItem("token"),
loginPath: "/login"
})
);
kernel.use(createAnalyticsGuard()); // 自定义守卫后注册
await kernel.start();执行顺序:auth 的 beforeEach 先执行 → 放行后 → 自定义 beforeEach 再执行。auth 重定向时会阻止后续守卫执行。
守卫逻辑决策树
router.beforeEach(to, from)
│
├─ meta.requiresAuth === false ? ───→ 放行(公开页面)
│
├─ 未登录(getToken() 返回 null) ───→ 重定向到 loginPath?redirect=原路径
│
├─ 已登录 + meta.permissions 非空 ───→ hasPermission(permissions)
│ ├─ true → 放行
│ └─ false → 重定向到 forbiddenPath
│
└─ 已登录 + 无额外权限要求 ──────────→ 放行createUrlTokenPlugin(options?): CorePlugin
URL Token 提取插件。
onBeforeInit:从 URL query 提取 token 写入 cookie,并清除 URL 参数。 onInited:调用 onToken 回调(Pinia 已就绪),将 token 注入 Auth Store。
必须在 auth 之前注册。
import { createUrlTokenPlugin } from "@nvoo/core";
kernel.use(
createUrlTokenPlugin({
tokenKey: "token", // 可选:URL 参数 key,默认 "token"
storageKey: "token", // 可选:cookie key,默认同 tokenKey
onToken: token => {
const auth = useAuth();
auth.login(token);
}
})
);| 选项 | 类型 | 默认值 | 说明 |
| --------------- | ----------------------------------------- | ------------------------------------------------------------- | -------------------------------------- |
| tokenKey | string | "token" | URL 查询参数 key 名 |
| storageKey | string | 同 tokenKey | 写入 cookie 的 key 名 |
| storage | CookieStorage | createDefaultCookieStorage() | Cookie 存储实现(支持测试/自定义) |
| cookieOptions | Partial<CookieSetOptions> & { expires } | { expires: 7200, path: "/", secure: true, sameSite: "Lax" } | Cookie 属性覆盖 |
| onToken | (token: string) => void | — | Token 提取后回调,用于注入 Pinia Store |
行为
- onBeforeInit — 从
window.location.search提取 token,写入 cookie(secure: true),清除 URL 参数 - onInited — 调用
onToken(token)回调(此时 Pinia 已安装),将 token 注入 Auth Store
自定义存储
import { createUrlTokenPlugin, type CookieStorage } from "@nvoo/core";
const memoryStorage: CookieStorage = {
get: name => cache[name] ?? null,
set: (name, value, opts) => {
cache[name] = value;
},
remove: name => {
delete cache[name];
}
};
kernel.use(createUrlTokenPlugin({ storage: memoryStorage }));createPageStylePlugin(options): CorePlugin
页面级原生样式管理插件。声明 deps: ["router"],监听 PAGE_CHANGED 事件,读取 route.meta.pageStyle 并通过回调分发给 bridge。
字段级 diff:内部逐字段比较,只有实际变化的属性才触发回调,避免重复调用原生接口。
通过 usePageStyle() 运行时 Hook 支持页面内动态修改样式(如滚动时切换导航栏颜色),变化时自动触发 diff + 回调。
import { createPageStylePlugin } from "@nvoo/core";
// bridge: 项目自定义的 App 原生桥接对象(如 uni-app 的 uni、Taro 的 Taro)
kernel.use(
createPageStylePlugin({
globalStyle: {
navigationBarBackgroundColor: "#ffffff",
navigationBarTextStyle: "black"
},
onStyleChange: style => {
bridge.setNavStyle(style);
}
})
);| 选项 | 类型 | 说明 |
| --------------- | ---------------------------- | ---------------------------------------------------------------- |
| globalStyle | PageStyle | 可选。全局默认样式,作为所有页面的合并基值(页面级配置覆盖全局) |
| onStyleChange | (style: PageStyle) => void | 可选。样式变化回调,传入完整 PageStyle 对象 |
globalStyle与页面级pageStyle通过{ ...globalStyle, ...pageStyle }合并,页面配置覆盖全局默认。
usePageStyle()
运行时 Hook,页面组件内调用,动态修改当前页面样式。插件未安装时调用 setStyle 仅更新内部 ref(不触发原生回调)。
import { usePageStyle } from "@nvoo/core";
const { style, setStyle } = usePageStyle();
// 只读:当前生效样式
watch(style, s => {
console.log("当前状态栏颜色:", s.statusBarColor);
});
// 动态覆盖(合并模式,只改传入的字段,自动触发 diff + 回调)
setStyle({ statusBarStyle: "dark", navigationBarBackgroundColor: "#ffffff" });| 成员 | 类型 | 说明 |
| ----------------- | ---------------------------- | ----------------------------------------------- |
| style | Readonly<Ref<PageStyle>> | 当前生效的页面样式(只读 ref) |
| setStyle(style) | (style: PageStyle) => void | 合并覆盖当前样式,自动触发 diff → onStyleChange |
PageStyle
页面级原生样式配置接口。
import type { PageStyle } from "@nvoo/core";| 字段 | 类型 | 说明 |
| ------------------------------ | ----------------------- | ------------------------------------------------- |
| statusBarColor | string | 状态栏背景色,支持 #RRGGBB |
| statusBarStyle | "light" \| "dark" | 状态栏文字样式 |
| navigationStyle | "default" \| "custom" | 导航栏样式:default=原生导航栏,custom=沉浸式全屏 |
| navigationBarBackgroundColor | string | 导航栏背景色 |
| navigationBarTitleText | string | 导航栏标题文字 |
| navigationBarTextStyle | "black" \| "white" | 导航栏前景色(按钮/标题) |
在 .vue 文件中声明
<script setup lang="ts">
defineOptions({
title: "用户管理",
pageStyle: {
navigationBarBackgroundColor: "#1989fa",
navigationBarTextStyle: "white",
statusBarColor: "#1989fa",
statusBarStyle: "light"
}
});
</script>
pageStyle享受完整的 TypeScript 智能提示——插件内置了vue和vue-router的模块增强。 构建时自动从defineOptions提取pageStyle,透传到route.meta.pageStyle,无需修改任何构建配置。
动态修改示例
<script setup lang="ts">
import { usePageStyle } from "@nvoo/core";
const { setStyle } = usePageStyle();
onMounted(() => {
const onScroll = () => {
if (window.scrollY > 50) {
setStyle({
navigationBarBackgroundColor: "#ffffff",
navigationBarTextStyle: "black"
});
} else {
setStyle({
navigationBarBackgroundColor: "transparent",
navigationBarTextStyle: "white"
});
}
};
window.addEventListener("scroll", onScroll);
onUnmounted(() => window.removeEventListener("scroll", onScroll));
});
</script>createI18nPlugin(options): CorePlugin
国际化插件。声明 deps: ["router"]。
onBeforeInit— 动态加载vue-i18n,创建 i18n 实例,app.use(i18n)安装。显式暴露$t到globalProperties供 UI 库检测。watchlocaleRef,locale 变化时自动同步到 vue-i18n。onInited— 订阅PAGE_CHANGED,使用$t(meta.i18nKey)设置document.title。onUnmounted— 清理事件订阅和 locale watch。
通常通过 @nvoo/vite-plugin-i18n 的 virtual:i18n 使用,无需手动传入 options。如果需要独立使用:
import { ref } from "vue";
import { createI18nPlugin } from "@nvoo/core";
const locale = ref("zh-CN");
kernel.use(
createI18nPlugin({
messages: {
"zh-CN": { "route.index": "首页" },
en: { "route.index": "Home" }
},
localeRef: locale
})
);| 选项 | 类型 | 说明 |
| ----------- | ----------------------------------------- | ------------------------------------------------------------------- |
| messages | Record<string, Record<string, unknown>> | 必填。语言包,key 为 locale(如 "zh-CN"、"en"),value 为翻译表 |
| localeRef | Ref<string> | 必填。响应式 locale 引用,由 virtual:i18n 管理持久化和跨标签同步 |
设计要点:locale 的持久化和跨标签同步由 virtual:i18n 管理,core 插件只负责把变更同步到 vue-i18n 实例。
vue-i18n通过动态import()加载,未使用 i18n 的项目不会打包该依赖。
消费方完整示例
import {
createAuthPlugin,
createI18nPlugin,
createKernel,
createPageStylePlugin,
createPiniaPlugin,
createRouterPlugin,
createUrlTokenPlugin
} from "@nvoo/core";
import { createRoutes } from "virtual:app-routes";
import { createI18nPlugin as createI18nVPlugin } from "virtual:i18n";
import { createAnalyticsGuard } from "./plugins/analytics-guard";
import { useAuth } from "./stores/auth";
import App from "./App.vue";
const kernel = createKernel({ rootComponent: App });
// ① URL Token 提取(最先注册,提取 URL 参数中的 token)
kernel.use(
createUrlTokenPlugin({
onToken: token => {
const auth = useAuth();
auth.login(token);
}
})
);
// ② Pinia 状态管理
kernel.use(createPiniaPlugin());
// ③ 路由(创建 Router、发出 ROUTER_READY)
kernel.use(
createRouterPlugin({
routes: createRoutes(),
scrollBehavior: () => ({ top: 0 }) // 每次导航回到顶部
})
);
// ④ 鉴权守卫(依赖 router)
kernel.use(
createAuthPlugin({
getToken: () => {
const auth = useAuth();
return auth.token;
},
loginPath: "/login",
forbiddenPath: "/forbidden",
hasPermission: (permissions: string[]) => {
const auth = useAuth();
return auth.hasPermission(permissions);
},
createRoutes: hasPermission => createRoutes(hasPermission)
})
);
// ⑤ 自定义守卫:埋点 + 标题(依赖 router)
kernel.use(createAnalyticsGuard());
// ⑥ 国际化:扫描 locales/ 自动生成 virtual:i18n,提供 $t / locale / setLocale
kernel.use(createI18nVPlugin());
// ⑦ 页面样式管理:defineOptions({ pageStyle }) → 原生导航栏/状态栏
kernel.use(
createPageStylePlugin({
globalStyle: {
navigationBarBackgroundColor: "#ffffff",
navigationBarTextStyle: "black"
},
onStyleChange: style => {
bridge.setNavStyle(style);
}
})
);
await kernel.start();插件注册顺序:
url-token → pinia → router → auth → analytics → i18n → page-style。 顺序由deps依赖声明保证 —— PluginManager 自动拓扑排序,声明了deps: ["router"]的插件一定排在 router 之后。useAuth()必须在 Pinia 安装之后调用,getToken/hasPermission/createRoutes等回调执行时 Pinia 已就绪。createI18nPlugin和createPageStylePlugin通过事件总线和回调解耦,不直接依赖其他插件内部实现,符合架构契约。
自定义插件示例
场景一:路由埋点 + 动态标题(完整生命周期)
import type { CorePlugin } from "@nvoo/core";
function createAnalyticsGuard(): CorePlugin {
let removeBeforeEach: (() => void) | null = null;
let removeAfterEach: (() => void) | null = null;
return {
name: "analytics-guard",
deps: ["router"],
onBeforeInit(ctx) {
const router = ctx.router;
if (!router) return;
removeBeforeEach = router.beforeEach((_to, _from) => {
return true;
});
removeAfterEach = router.afterEach(to => {
document.title = (to.meta.title as string) ?? "默认标题";
console.log(`[analytics] page view: ${to.fullPath}`);
});
},
onUnmounted() {
removeBeforeEach?.();
removeAfterEach?.();
}
};
}💡 埋点推荐用事件而非守卫:如果只是想做 page view 上报,不需要拦截导航,订阅
PAGE_CHANGED事件更干净 —— 见场景二。
完整源码参考:
apps/example-page/plugins/analytics-guard.ts
场景二:监听 PAGE_CHANGED 做全局追踪
PAGE_CHANGED 由 RouterPlugin 在每次导航完成时自动发出,无需手动注册 afterEach。
import { CORE_EVENTS } from "@nvoo/core";
import type { CorePlugin } from "@nvoo/core";
function createPageTrackerPlugin(): CorePlugin {
return {
name: "page-tracker",
onInited(ctx) {
ctx.on(CORE_EVENTS.PAGE_CHANGED, ({ path, name, meta }) => {
// 上报到自建埋点服务
fetch("/api/report/pageview", {
method: "POST",
body: JSON.stringify({ path, name, meta })
});
});
}
};
}场景三:全局错误统一处理
import { CORE_EVENTS } from "@nvoo/core";
import type { CorePlugin } from "@nvoo/core";
function createErrorReportPlugin(): CorePlugin {
return {
name: "error-report",
onInited(ctx) {
// 业务异常
ctx.on(CORE_EVENTS.ERROR_OCCURRED, ({ message, path }) => {
console.error(`[error] ${path}: ${message}`);
});
// 路由导航异常(懒加载失败、guard 异常等)
ctx.on(CORE_EVENTS.ROUTER_ERROR, ({ error }) => {
console.error("[router-error]", error);
});
}
};
}
// main.ts
kernel.use(createErrorReportPlugin());场景四:第三方 UI 库或插件安装(零依赖)
在 onBeforeInit 通过 ctx.app.use() 安装,此时 Vue App 已创建、组件和指令可全局注册。
import type { CorePlugin } from "@nvoo/core";
function createUIPlugin(): CorePlugin {
return {
name: "ui-lib",
onBeforeInit(ctx) {
// 全局组件
ctx.app.component("MyIcon", MyIcon);
// 全局指令
ctx.app.directive("focus", {
mounted(el: HTMLElement) {
el.focus();
}
});
}
};
}如果有 i18n、Element Plus 等第三方库,同理:
// import ElementPlus from "element-plus";
// onBeforeInit(ctx) {
// ctx.app.use(ElementPlus);
// }场景五:启动时预加载全局配置(依赖 pinia)
声明 deps: ["pinia"],在 onInited 调用 store action。此时 Pinia 已安装,可安全使用 useXxxStore()。
import type { CorePlugin } from "@nvoo/core";
import { useConfigStore } from "./stores/config";
function createConfigLoaderPlugin(): CorePlugin {
return {
name: "config-loader",
deps: ["pinia"],
async onInited() {
const config = useConfigStore();
await config.fetchGlobalConfig(); // 拉取系统配置、字典数据等
await config.fetchUserInfo(); // 加载当前用户信息
}
};
}场景六:全局未捕获异常兜底(零依赖)
在 onInited 注册全局 JavaScript 错误监听,将未捕获异常统一上报。
import type { CorePlugin } from "@nvoo/core";
function createGlobalErrorPlugin(): CorePlugin {
let onError: ((e: ErrorEvent) => void) | null = null;
let onRejection: ((e: PromiseRejectionEvent) => void) | null = null;
return {
name: "global-error",
onInited() {
onError = (e: ErrorEvent) => {
console.error("[global-error]", e.message);
// 上报到错误监控平台
};
onRejection = (e: PromiseRejectionEvent) => {
console.error("[global-rejection]", e.reason);
};
window.addEventListener("error", onError);
window.addEventListener("unhandledrejection", onRejection);
},
onUnmounted() {
if (onError) window.removeEventListener("error", onError);
if (onRejection) window.removeEventListener("unhandledrejection", onRejection);
}
};
}场景七:页面级导航栏/状态栏样式管理(依赖 router + bridge)
利用 createPageStylePlugin 实现类似微信小程序的页面级原生样式配置。静态配置写在 defineOptions({ pageStyle }),运行时通过 usePageStyle().setStyle() 动态覆盖。
页面声明(静态配置):
<script setup lang="ts">
defineOptions({
title: "用户管理",
pageStyle: {
navigationBarBackgroundColor: "#1989fa",
navigationBarTextStyle: "white",
statusBarStyle: "light"
},
permissions: ["user#view"]
});
</script>运行时动态修改:
import { usePageStyle } from "@nvoo/core";
const { setStyle } = usePageStyle();
// 页面滚动时切换导航栏样式
window.addEventListener("scroll", () => {
if (window.scrollY > 100) {
setStyle({ statusBarStyle: "dark" });
}
});插件注册(main.ts):
import { createPageStylePlugin } from "@nvoo/core";
// bridge: 项目自定义的 App 原生桥接对象(如 uni-app 的 uni、Taro 的 Taro)
kernel.use(
createPageStylePlugin({
globalStyle: {
navigationBarBackgroundColor: "#ffffff",
navigationBarTextStyle: "black"
},
onStyleChange: style => {
bridge.setNavStyle(style);
}
})
);场景速查表
| 依赖 | 典型钩子 | 场景举例 |
| ------------------- | --------------- | ---------------------------------------------------------- |
| 无 | onBeforeInit | UI 库安装、全局组件/指令注册 |
| 无 | onInited | 全局错误监听、性能监控初始化 |
| 无 | onBeforeMount | 暗色主题、Loading 屏移除前准备 |
| 无 | onMounted | DOM 操作、Loading 屏移除 |
| pinia | onInited | 预加载配置、拉取用户信息、Token 过期监听 |
| pinia + router | onInited | 路由级权限过滤(addRoute/removeRoute) |
| router | onBeforeInit | 自定义路由守卫(beforeEach/beforeResolve/afterEach) |
| router + bridge | 内置 | 页面级导航栏/状态栏样式(createPageStylePlugin) |
| router | onBeforeInit | 国际化(createI18nPlugin),配合 virtual:i18n 使用 |
| 任意 | onUnmounted | 资源释放(移除事件、取消监听、停止定时器) |
协议
MIT
