首页
/ Slidev 配置 Vue 应用:setup/main.ts 与 defineAppSetup 扩展机制详解

Slidev 配置 Vue 应用:setup/main.ts 与 defineAppSetup 扩展机制详解

2026-09-05 23:55:03作者:丁柯新Fawn

Slidev 使用 Vue 3 渲染客户端应用,而 setup/main.ts 是官方预留的应用级扩展入口:你可以在其中访问 Vue 应用实例(app)和路由实例(router),挂载自定义插件,或在应用挂载前完成各类初始化。本文基于当前仓库源码,完整讲清该机制的文件约定、类型定义与运行时调用链,帮助你在自己的 Slidev 演示项目中安全地扩展 Vue 应用。

基本用法:创建 setup/main.ts

Slidev 遵循约定优于配置的原则,应用扩展入口的约定路径为项目根目录下的 setup/main.ts(也支持 .js.mts.mjs 等扩展名)。

在该文件中,使用 @slidev/types 包导出的 defineAppSetup 定义扩展逻辑:

import type { Plugin } from 'vue'
import { defineAppSetup } from '@slidev/types'

const YourPlugin: Plugin = {
  install(app) {
    app.provide('yourKey', 'yourValue')
  },
}

export default defineAppSetup(({ app, router }) => {
  // Vue App
  app.use(YourPlugin)
})

回调参数是 AppContext,包含两个字段:

  • app:Vue 应用实例(createApp() 返回的对象),可用于 app.use()app.provide()app.component() 等标准 Vue Application API 操作;
  • router:Vue Router 实例,已配置好 Slidev 的路由历史与路由表,可通过它监听导航、查询当前路由等。

defineAppSetup 本质上是一个类型辅助函数。从 类型定义 可以看到,它通过泛型约束了回调签名,但不改变运行时行为:

export type AppSetup = (context: AppContext) => Awaitable<void>

function defineSetup<Fn>(fn: Fn) {
  return fn
}

export const defineAppSetup = defineSetup<AppSetup>

注意类型定义中的 Awaitable<void>该回调支持 async 写法,Slidev 会等待其完成。这意味着你可以执行异步初始化(如动态加载配置、请求远端数据后再 app.use()),而不会与后续流程产生竞态。

此外,原文档指出:setup/main.ts 也可以作为整个 Slidev 应用的主入口,用于在执行任何应用逻辑之前做一些初始化。由于它运行时机极早(详见下文调用链),适合放置埋点 SDK 初始化、全局错误捕获、第三方库配置等"越早越好"的代码。

运行时调用链:setup 何时被加载与执行

理解执行时机,才能正确使用这个扩展点。从当前仓库源码可以还原完整链路:

1. 虚拟模块收集 setup/main.* 文件

Slidev 在构建侧通过 Vite 虚拟模块机制收集用户项目中的 setup 文件。packages/slidev/node/virtual/setups.ts 会为每个 setup 名称生成一个虚拟模块 /@slidev/setups/<name>,其内容按 roots 生成 import 语句:

function createSetupTemplate(name: string): VirtualModuleTemplate {
  const id = `/@slidev/setups/${name}`
  return {
    id,
    getContent({ roots }) {
      const imports: string[] = []
      const globs = roots.map((root) => {
        const glob = join(root, `setup/${name}.{ts,js,mts,mjs}`)
        // ... 生成对 setup/<name> 文件的 import 与导出
      })
      return `${imports.join('\n')}\n\nexport default [${globs.join(', ')}].filter(Boolean)`
    },
  }
}

其中 'main' 是注册在 setup 模块列表中的名称之一(同列表还有 shikicode-runnersmonacomermaidrootroutesshortcutscontext-menu 等)。.filter(Boolean) 保证了文件不存在时不会报错——也就是说 setup/ 目录及其中的文件全部是可选的,这与其他 setup 文件(如 目录结构文档 所述)保持一致的约定。

2. 客户端在挂载前 await 所有 setup

客户端主流程位于 packages/client/main.ts

async function main() {
  const app = createApp(App)
  await setupMain(app)
  app.mount('#app')
}

关键点:app.mount('#app')setupMain 完成之后才执行。而 setupMain 的内部流程(见 packages/client/setup/main.ts)为:

  1. 创建 Vue Router(根据 __SLIDEV_MEMORY_ROUTE__ / __SLIDEV_HASH_ROUTE__ 选择 history 模式),并 app.use(router)
  2. 依次安装 createHead()v-click / v-mark / v-drag / v-motion 指令以及 TwoSlash 浮动容器等内置插件;
  3. 组装 AppContext{ app, router });
  4. 遍历虚拟模块 #slidev/setups/main 收集到的所有 setup,逐个 await setup(context)
const context: AppContext = {
  app,
  router,
}

for (const setup of setups)
  await setup(context)

由此得出几个使用上的实际含义:

  • 执行时机晚于内置插件安装:当你的 defineAppSetup 回调运行时,router 与内置指令都已在 app 上注册完成,你可以安全地访问 router.currentRoute 或调用 router.afterEach
  • 执行时机早于应用挂载:你的初始化代码运行在 app.mount('#app') 之前,因此可以在渲染前完成依赖注入(app.provide)、全局组件注册(app.component)等;
  • 多个 setup 会按序被 await:如果你的初始化是异步的(如 await fetch(...)),框架会等待其 resolve 后才继续,不会出现"应用已挂载但初始化未完成"的竞态。

3. 与 setup/root.ts 的分工

从源码结构看,Slidev 还约定了另一个客户端扩展点 setup/root.{ts,js,mts,mjs},对应类型 RootSetup = () => Awaitable<void>类型定义)。它在根组件(App)内部执行(见 packages/client/setup/root.ts 中的 for (const setup of setups) setup()),能访问 getCurrentInstance(),适合做依赖组件实例或 useHead 的 setup。

两者的分工可以概括为:

扩展点 文件 类型 可用上下文 典型场景
App setup setup/main.ts defineAppSetup approuter(挂载前) 挂载插件、app.provide、全局初始化
Root setup setup/root.ts defineRootSetup 组件实例上下文 需要 getCurrentInstance / 组合式 API 的初始化

如果你只需要操作 approuter 两个对象,setup/main.ts 就是文档所指的入口。

实际示例:全局 provide 与路由钩子

以下示例展示了 setup/main.ts 中两类常见用法(均只依赖文档所述的 app / router 两个上下文对象):

import { defineAppSetup, type AppContext } from '@slidev/types'

export default defineAppSetup((ctx: AppContext) => {
  const { app, router } = ctx

  // 1. 全局依赖注入:在 slides / layouts / 自定义组件中用 inject('api') 获取
  app.provide('api', {
    fetchConfig: async () => (await fetch('/config.json')).json(),
  })

  // 2. 注册全局组件
  app.component('GlobalTimer', {
    template: `<span class="timer">⏱</span>`,
  })

  // 3. 监听导航:每次切页上报(例如埋点)
  router.afterEach((to, from) => {
    console.log(`slide: ${from.path} -> ${to.path}`)
  })
})

几点注意事项:

  • provide 的 key 使用字符串时,请遵循项目的注入键约定,避免与其他库冲突;从源码看,Slidev 自身的注入键以 injection* 常量形式定义在 packages/client/constants.ts 中,自定义时避开这些键即可;
  • 路由钩子(afterEach / beforeEach)在你注册之后才会对后续导航生效,而 Slidev 的路由表由 setupRoutes() 生成,注册时机晚于 setup 执行,因此你的钩子能覆盖到应用运行期的全部导航;
  • 该回调是 Awaitable<void>,若使用 async 写法,请确保不会引入阻塞渲染的长任务——它运行在 app.mount 之前的同步等待链路上。

适用前提与限制

  • 本扩展点属于 client 端 能力(对应文档标注的 Environment: client),只在浏览器侧执行,不能在其中访问 Node API;
  • 文件名必须精确为 setup/main.ts(或 .js / .mts / .mjs),放置位置是 slides 项目根目录下的 setup/ 文件夹,这是虚拟模块 glob 匹配的结果,见 虚拟模块生成逻辑
  • defineAppSetup 来自 @slidev/types,它不引入任何运行时依赖,仅为 TypeScript 提供类型推导;
  • 更多 Vue 应用级操作可参考 Vue 官方 Application API 文档(app.useapp.componentapp.provide 等),Slidev 透传的是标准 Vue 3 应用实例。

小结

setup/main.ts + defineAppSetup 是 Slidev 提供的应用级扩展入口:它在 router 与内置指令安装完成之后、app.mount('#app') 之前执行,且支持异步等待。掌握这一点后,你就能在保持 Slidev 零配置体验的同时,把任意 Vue 插件、依赖注入和初始化逻辑挂进演示应用,而不需要 fork 或改动 Slidev 客户端源码。

登录后查看全文
热门项目推荐
相关项目推荐