Slidev 幻灯片布局系统全解析:内置布局、加载优先级与自定义 Layout 编写
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
Slidev(Presentation Slides for Developers)中,布局(Layout)决定了每一页幻灯片的页面结构与视觉风格:它本质上是一个包裹该页幻灯片内容的 Vue 组件。本指南围绕官方 docs/guide/layout.md 展开,系统讲解如何通过 frontmatter 选用布局、Slidev 内置布局的完整清单与用法、内置/主题/插件/自定义布局之间的加载与覆盖规则,以及如何亲手编写一个自定义 Vue 布局并与 slot 语法糖配合使用。读完本文,你将能够在项目中自由组合布局、理解布局解析的底层机制,并打造属于自己的可复用布局。
Layout 是什么:包在幻灯片外面的一层 Vue 组件
在 Slidev 的渲染模型里,每张幻灯片的内容(Markdown + Vue 语法)会先被编译成一个独立的 Vue 组件,而布局则是“包裹”这些内容的外层结构。官方文档给出的定义非常精炼:
Layouts in Slidev are used to define the structure for each slide. They are Vue components that wrap the content of the slides.
也就是说,写布局就是写 Vue 组件。布局负责页面骨架(标题放哪里、正文放哪里、要不要左右分栏、背景是纯色还是图片),而默认插槽<slot />则负责承接你写在 Markdown 正文里的实际内容。
从编译产物可以直观看到这层包裹关系:在源码 packages/slidev/node/vite/layoutWrapper.ts 中,每页幻灯片的模板内容会被改写成<InjectedLayout v-bind="_frontmatterToProps($frontmatter, index)">包裹起来的结构,这里的InjectedLayout正是动态 import 进来的布局组件(见该文件的第 56 行import InjectedLayout from ...)。因此,你在页面上看到的一切结构都来自布局组件,而 frontmatter 中除系统保留字段外的其余键值会被作为 props 传给布局。
使用布局:frontmatter 中声明 layout
为某页幻灯片指定布局,只需在该页的 frontmatter 中声明layout字段:
--- layout: quote --- A quote from someone对于quote布局来说,页面会把正文当作一段引人注目的引用语来排版展示。布局名即组件名,Slides 文件顶层(第一页)可以放置作用于全局的配置(theme、addons 等),而每一页也可以有自己的 frontmatter。
默认布局规则
如果你不显式指定布局,Slidev 会给出两个预设默认值:
- 第一张幻灯片默认使用
cover布局; - 其余幻灯片默认使用
default布局。
该规则有明确的源码依据:layoutWrapper.ts 在解析每一页时计算布局名:
const rawLayoutName = data.slides[index]?.frontmatter?.layout ?? data.slides[0]?.frontmatter?.defaults?.layout let layoutName = rawLayoutName || (index === 0 ? 'cover' : 'default')两处值得注意的细节:
- 除了单页的
layout字段,布局名还能通过第一页 frontmatter 中的defaults(全局默认 frontmatter)统一指定,这非常适合整个演示统一替换默认外观; - 若指定的布局名在所有来源中都找不到,Slidev 会在终端打印
Unknown layout "..."错误并列出所有可用布局名,随后回退到default布局而不是直接崩溃(layoutName = 'default')。
布局的slidev-layout前缀与slidev-layout default、slidev-layout cover这样的命名约定,在 packages/client/layouts 下的每个内置布局模板根元素中都可以看到,它们也是你在布局作用域样式中定位的关键钩子。
布局的加载优先级:后加载者覆盖先加载者
官方文档明确指出,布局按以下顺序解析,最后加载到的同名布局会覆盖前面加载的同名布局:
- 内置布局(见 docs/builtin/layouts.md);
- 主题(theme)提供的布局;
- Addon(插件)提供的布局;
- 项目
layouts目录中的自定义布局。
这一规则在源码中体现得非常直白。看 options.ts 中createDataUtils暴露的getLayouts():
- 它会遍历
[resolved.clientRoot, ...resolved.roots]这一串根目录(clientRoot即内置布局所在目录,roots依序为「主题根目录、addon 根目录、用户项目根目录」,见同文件 options.ts 的组合方式); - 在每个根目录里通过 fast-glob 扫描
layouts/**/*.{vue,js,mjs,ts,mts}文件; - 布局名取文件名去掉扩展名后的 basename(例如
MyLayout.vue即MyLayout); - 所有结果写入同一个
layouts对象,靠后的路径覆盖靠前的同名 key。
由于用户项目根目录排在最后,所以当你在自己的layouts/目录里放一个与内置布局同名的default.vue,它就会覆盖内置default布局——这就是“自定义优先”的实现原理。同样的覆盖机制也解释了为什么主题可以重新定义内置布局(主题根目录排在clientRoot之后)。最终这些布局由虚拟模块/@slidev/layouts收集成一个{ name: component }的对象导出给客户端使用(见 packages/slidev/node/virtual/layouts.ts)。
补充:主题与 addon 往往不只提供布局,还会提供组件、全局样式等整套扩展能力。若需要了解主题如何覆写布局以及两者整体协作方式,可进一步阅读 docs/guide/theme-addon.md。
内置布局全景与用法详解
Slidev 随仓库自带约 19 种内置布局,源码全部位于 packages/client/layouts,官方逐个说明见 docs/builtin/layouts.md。按其用途可分为四类:
| 分类 | 布局 | 适用场景 |
|---|---|---|
| 通用型 | default、cover、intro、section、end、none | 演示的结构化节点:封面、章节、结尾、纯内容页 |
| 强调型 | fact、quote、statement、center | 让某句话/数据成为视觉焦点 |
| 图像与网页型 | full、image、image-left、image-right、iframe、iframe-left、iframe-right | 以图片或内嵌网页为主的页面 |
| 分栏布局 | two-cols、two-cols-header | 左右分栏、上通栏下分栏 |
下面按类逐一展开。
通用型:default / cover / intro / section / end / none
default:最基础的布局,适合承载任意内容。其实现极其简单(default.vue),仅输出<div class="slidev-layout default"><slot /></div>。cover:封面页,用于展示演示标题、副标题、作者等开场信息(cover.vue 同样只有一个带slidev-layout cover类名的插槽容器,视觉样式由全局样式表负责)。intro:导言页,通常也承载标题、简短介绍与作者信息。section:标识一个新章节的开始,常作为“章节过渡页”。end:演示的结束页(致谢、Q&A 等)。none:不带任何预设样式的最裸布局,完全由你自己写样式。
强调型:fact / quote / statement / center
fact:把某条事实或数据以大字号、强存在感的方式呈现在屏幕中央。quote:以显著样式展示一段引言——这就是文章开头示例layout: quote的效果。statement:将一句断言/宣言作为页面主体内容。center:将内容垂直水平居中显示。
这四类布局通常不需要额外 frontmatter 参数,直接把正文写进去即可,适合“一页一句话”的节奏型演示。
图像与内嵌网页布局:image / image-left / image-right 与 iframe 系列
image系列布局将图片作为页面的视觉主体或侧栏。以 docs/builtin/layouts.md 中的用法为例:
image-left:图片在左、正文内容在右:
--- layout: image-left # the image source image: /path/to/the/image # a custom class name to the content class: my-cool-content-on-the-right ---image-right:图片在右、正文内容在左:
--- layout: image-right # the image source image: /path/to/the/image # a custom class name to the content class: my-cool-content-on-the-left ---image:图片作为整页主内容,正文可作为叠加内容:
--- layout: image # the image source image: /path/to/the/image ---这三个布局都支持通过backgroundSize覆盖默认的背景尺寸(默认为cover),可以传入任意合法的 CSSbackground-size值:
--- layout: image image: /path/to/the/image backgroundSize: contain ------ layout: image-left image: /path/to/the/image backgroundSize: 20em 70% ---从源码看,这些 frontmatter 键与布局组件的 props 一一对应。image.vue 声明了image与backgroundSize(默认'cover')两个 props,而 image-left.vue 则额外声明了classprop,并把背景应用在左侧图片容器、正文放在右侧slidev-layout default容器中。背景图的解析统一走 packages/client/layoutHelper.ts 的handleBackground():它会自动区分颜色值(#hex或rgb...)与图片 URL,前者直接作为background,后者写入backgroundImage,并允许用参数dim叠加一层渐变压暗遮罩以保证文字可读性,同时处理静态资源的 base 路径。
iframe系列则是把一整个网页内嵌到页面中,参数改为url:
iframe:网页作为整页主体:
--- layout: iframe # the web page source url: https://example.com ---iframe-left/iframe-right:网页在左/右半屏,正文放在另一侧,同样可用class自定义正文类名:
--- layout: iframe-left # the web page source url: https://example.com # a custom class name to the content class: my-cool-content-on-the-right ------ layout: iframe-right # the web page source url: https://example.com # a custom class name to the content class: my-cool-content-on-the-left ---分栏布局:two-cols 与 two-cols-header
two-cols将页面内容分隔为左右两栏,栏内内容通过slot 语法糖::right::划分。官方用法:
--- layout: two-cols --- # Left This shows on the left ::right:: # Right This shows on the righttwo-cols-header则在上方保留一个横跨整行的通栏区,第二行再左右分栏:
--- layout: two-cols-header --- This spans both ::left:: # Left This shows on the left ::right:: # Right This shows on the right <style> .two-cols-header { column-gap: 20px; /* Adjust the gap size as needed */ } </style>(官方示例中的<style>块用于演示如何针对布局根类名微调列间距,可按需保留或删去。)::name::是 docs/features/slot-sugar.md 定义的命名插槽速写语法,::right::、::left::、::default::分别对应 Vue 的命名插槽。值得留意的是源码实现:two-cols.vue 中,左侧栏同时渲染默认插槽slot与命名插槽slot name="left",右侧栏只渲染slot name="right",因此::right::之前的所有内容会自动落进左栏。它还声明了class(应用到左右栏内容)与layoutClass(应用到根容器)两个可选 prop。
关于::right::、::default::、::left::的完整语义(含显式指定::default::调整栏内容书写顺序的用法),详见 docs/features/slot-sugar.md。
编写自定义布局:在 layouts 目录写 Vue 组件
内置布局不够用时,可以完全自定义。根据官方指南 docs/guide/write-layout.md,只需要在项目根目录创建layouts/文件夹,往里面放 Vue 组件即可:
your-slidev/ ├── ... ├── slides.md └── layouts/ ├── ... └── MyLayout.vue写完后,即可在任意幻灯片 frontmatter 中通过layout: MyLayout使用它。因为布局本质就是 Vue 组件,Vue 模板、指令、响应式逻辑等全部能力都可用(包括<script setup>内的逻辑与useSlideContext提供的全局上下文)。
最基础的自定义布局只需一个默认插槽用于承接正文:
<template> <div class="slidev-layout my-layout"> <slot /> </div> </template>这与内置 default.vue、cover.vue 的结构完全一致。建议根元素保留slidev-layout前缀类名,以继承 packages/client/styles/layouts-base.css 中提供的基准排版样式。
使用命名插槽构造复杂布局
需要多个内容区时,可以声明命名插槽:
<template> <div class="slidev-layout split"> <div class="left"> <slot name="left" /> </div> <div class="right"> <slot name="right" /> </div> </div> </template>然后在幻灯片中用::left::/::right::(即 docs/features/slot-sugar.md 的速写语法)分别注入两侧内容。这样,一个简单的布局组件就能同时服务多类页面。two-cols、image-left等内置布局正是基于这种“命名插槽 + frontmatter 传参”的模式,如果你希望自定义布局也接收class、自定义图片路径等参数,同样可以为其声明 props。
布局如何接收 frontmatter 参数
前面提到布局会收到来自 frontmatter 的 props。其底层机制位于 packages/client/context.ts 的frontmatterToProps():
export function frontmatterToProps(frontmatter: Record<string, any>, pageNo: number) { return { ...objectOmit(frontmatter, pageNo === 0 ? HEADMATTER_FIELDS : FRONTMATTER_FIELDS), frontmatter, } }即:把 frontmatter 中除系统保留字段(如layout、clicks、transition、theme等,见 packages/client/constants.ts 中的FRONTMATTER_FIELDS/HEADMATTER_FIELDS)以外的键全部铺开为 props,同时额外注入完整的frontmatter对象。所以你在 frontmatter 里写image: /img.png,布局组件的defineProps({ image: String })就能拿到它;而布局内若需访问全部 frontmatter,则可用注入的frontmatterprop 或幻灯片上下文中的$frontmatter。这一点也提醒我们:不要用与系统保留字段同名的键作为自定义布局参数,否则它会被过滤而无法作为 prop 传递。
布局作用域内还能做什么
结合仓库中的其它扩展机制,自定义布局还可以做得更强大:
- 点击步进与动态内容:布局内可直接使用
v-click等指令与点击计数机制(相关源码在 packages/client/modules/v-click.ts),让布局自带动态出现效果; - 全局上下文:通过 packages/client/context.ts 暴露的
useSlideContext读取$slidev、$nav、$page、$clicks等运行态,实现根据页码/点击次数变化的结构; - 主题与 addon 自定义布局:主题和 addon 通过相同机制注入布局,并在加载顺序上先于项目自定义目录被扫描(被后者覆盖),要了解完整扩展生态可参阅 docs/guide/theme-addon.md。
实战建议与常见问题
- 遇到
Unknown layout报错:检查布局文件名与 frontmatter 是否完全一致(大小写敏感,因为getLayouts直接使用文件名 basename 作为 key);终端打印的错误信息会列出当前所有可用布局名,是排查主题/插件布局是否被正确安装的便捷途径。 - 想让整个演示使用同一种非默认布局:在第一页 frontmatter 的
defaults中设置layout字段,可让全部页面生效,而不必逐页书写。 - 需要临时覆盖内置布局:在项目
layouts/下放置同名文件即可(如layouts/default.vue),利用“最后加载者覆盖”的规则实现自定义默认外观,同时保持 Markdown 正文零改动。 - 图片/网页参数缺失时页面会异常:使用
image、iframe系列布局时,务必同时提供image或url参数,并确认资源路径以项目为基准可被正确解析。
延伸阅读
- 内置布局逐条说明与用法示例:docs/builtin/layouts.md
- 编写自定义布局完整指南:docs/guide/write-layout.md
- 布局命名插槽语法糖
::left::/::right::/::default:::docs/features/slot-sugar.md - 主题与 addon 如何扩展(含覆写布局):docs/guide/theme-addon.md
- 内置布局源码:packages/client/layouts
- 布局解析与优先级实现:packages/slidev/node/vite/layoutWrapper.ts、packages/slidev/node/options.ts、packages/slidev/node/virtual/layouts.ts
- frontmatter 到 props 的传递机制:packages/client/context.ts、packages/client/layoutHelper.ts
【免费下载链接】slidevPresentation Slides for Developers项目地址: https://gitcode.com/GitHub_Trending/sl/slidev
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考