输入关键词,开始搜索

↑↓ 选择↵ 打开esc 关闭
沉浸

组件与结构

组件与结构

主题的文件怎么组织,新增东西该放哪。

目录

src/├── layouts/          页面骨架├── components/│   ├── base/         布局级组件(导航、侧边栏、页脚)│   ├── content/      内容展示(文章卡片、相册卡片)│   ├── docs/         文档系统专用│   ├── ui/           通用小部件(按钮、日期、面包屑)│   └── widgets/      功能组件(搜索、目录、播放器)├── views/            视图系统(极简、简历)各自的布局与组件├── styles/│   ├── base/         token、配色、排版│   ├── components/   组件与指令样式│   └── utilities/    工具类├── plugins/          Markdown 指令的 remark/rehype 插件├── i18n/             界面文案字典└── scripts/          客户端脚本

布局

有五个布局,各自服务不同的页面类型:

布局用于
BaseLayout绝大多数页面
DocsLayout文档,带左侧目录树
AlbumLayout相册详情,有自己的氛围层
MinimalLayout极简视图
ResumeLayout简历

它们共享同一套约定:<html lang> 跟随配置、body 开头有跳过导航链接、主内容区是 <main id="main-content">。新增布局时照着抄这三样。

不要复制布局

主题里曾经有一个 DefaultLayout,是 BaseLayout 的分叉,九成内容相同。后来 BaseLayout 修过的问题它没有,两边慢慢长歪,最后只能删掉。

需要一个变体时,优先给 BaseLayout 加 prop,而不是复制一份改。

共享组件

下面这些是已经抽出来的,改动或新增页面时优先复用:

组件作用
base/PrevNext.astro上下篇导航。文章、文档、项目共用
base/DirectiveStyles.astro按需加载重量级指令的样式
ui/FormattedDate.astro日期显示
ui/Breadcrumb.astro面包屑
widgets/Pagination.astro分页

PrevNext 接受 prev 和 next 两个对象,各有 href、title,以及可选的 label 和 note:

Astro
<PrevNext    prev={{ href: '/blog/a/', title: '前一篇' }}    next={{ href: '/blog/b/', title: '后一篇', label: '推荐', note: '来自专栏「X」' }}/>

样式

样式入口是 src/styles/global.css,按顺序引入 token、配色、排版、指令样式、工具类、动画。新增样式文件时在这里加一行 @import,位置按类别放。

组件自己的样式优先写在 .astro 文件的 <style> 里,Astro 会自动作用域隔离。只有需要跨组件复用时才提到 styles/ 下。

指令样式的按需加载

全站 CSS 里有相当大一部分是 Markdown 指令的样式,但一篇文章通常只用到三五个指令。所以体积大、又只可能出现在正文里的指令,样式被移出了全局包,由 DirectiveStyles.astro 按页面实际用到与否内联。

目前走按需加载的有:plan、calendar、okr、deadline、yoicard、private、story、sites、posters、mind。

渲染正文的页面都要挂上它:

Astro
---import DirectiveStyles from '../../components/base/DirectiveStyles.astro';const { Content } = await render(post);--- <div class="prose">    <DirectiveStyles content={post.body || ''} />    <Content /></div>

检测依据是 Markdown 源码里的 :::name 开标记,和 remark 插件匹配指令的依据一致。

如果你新增了一个体积很大的指令样式,可以照着 DirectiveStyles.astro 里的写法加进 LAZY 数组,同时把它从 styles/components/directives.css 的 @import 列表里移除。判断不准时宁可留在全局包里——漏判会导致样式丢失,多加载只是多几 KB。

客户端脚本

有两种写法,区别很重要:

<script is:inline> 不经过打包,原样输出到 HTML。不能用 import,但可以用 define:vars 从服务端传值进去。

<script>(不带 is:inline)会被 Vite 打包,可以 import 模块,会做 tree-shaking 和代码分割。但不能用 define:vars,要传值只能通过 DOM 的 data-* 属性。

需要传服务端数据又需要 import 时,选后者,把数据放在属性上:

Astro
<div id="app" data-config={JSON.stringify(config)}></div> <script>    import { init } from '../scripts/my-module.js';    const el = document.getElementById('app');    init(JSON.parse(el.dataset.config));</script>

共享逻辑放在 src/scripts/ 下,用 ES 模块导出。比如目录的滚动高亮被侧边栏目录和浮动目录两处共用,就抽在了 scripts/toc-scrollspy.js,差异通过回调传入。

内容指令插件

指令的实现在 src/plugins/directives/,每个文件负责一组指令,index.mjs 做分发。

新增指令的步骤:

  1. 在对应的 .mjs 里写处理函数,产出 hast 节点
  2. 在 index.mjs 的名字数组里登记
  3. 在 styles/components/directives/ 下加样式,并在 directives.css 里 @import
用户输入必须转义

指令的属性来自用户的 Markdown,直接拼进 HTML 会有注入风险。shared.mjs 提供了几个工具,务必使用:

  • escapeHtml(text) — 文本和属性值
  • escapeUrl(url) — 链接,会限制协议,挡掉 javascript:
  • safeCssValue(v) — CSS 值,挡掉能跳出声明的字符
  • resolveColor(c) — 颜色,白名单校验
JavaScript
// 不要这样`<div title="${attrs.title}">` // 这样`<div title="${escapeHtml(attrs.title)}">`

指令里的界面文案通过 options 传入,不要硬编码:

JavaScript
export function processMyDirective(node, options = {}) {    const i18n = options.i18n?.myDirective ?? { empty: '暂无内容' };    // ...}
未知标题
00:00
00:00
播放列表