npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/core
import { 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(emit PAGE_CHANGED)+ onError(emit ROUTER_ERROR),最后 emit ROUTER_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 |

行为

  1. onBeforeInit — 从 window.location.search 提取 token,写入 cookie(secure: true),清除 URL 参数
  2. 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 库检测。watch localeRef,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