Slidev 配置 Vue 应用:setup/main.ts 与 defineAppSetup 扩展机制详解
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 模块列表中的名称之一(同列表还有 shiki、code-runners、monaco、mermaid、root、routes、shortcuts、context-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)为:
- 创建 Vue Router(根据
__SLIDEV_MEMORY_ROUTE__/__SLIDEV_HASH_ROUTE__选择 history 模式),并app.use(router); - 依次安装
createHead()、v-click/v-mark/v-drag/v-motion指令以及 TwoSlash 浮动容器等内置插件; - 组装
AppContext({ app, router }); - 遍历虚拟模块
#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 |
app、router(挂载前) |
挂载插件、app.provide、全局初始化 |
| Root setup | setup/root.ts |
defineRootSetup |
组件实例上下文 | 需要 getCurrentInstance / 组合式 API 的初始化 |
如果你只需要操作 app 和 router 两个对象,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.use、app.component、app.provide等),Slidev 透传的是标准 Vue 3 应用实例。
小结
setup/main.ts + defineAppSetup 是 Slidev 提供的应用级扩展入口:它在 router 与内置指令安装完成之后、app.mount('#app') 之前执行,且支持异步等待。掌握这一点后,你就能在保持 Slidev 零配置体验的同时,把任意 Vue 插件、依赖注入和初始化逻辑挂进演示应用,而不需要 fork 或改动 Slidev 客户端源码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00