组件与结构
组件与结构
主题的文件怎么组织,新增东西该放哪。
目录
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:
<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。
渲染正文的页面都要挂上它:
---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 时,选后者,把数据放在属性上:
<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 做分发。
新增指令的步骤:
- 在对应的
.mjs里写处理函数,产出 hast 节点 - 在
index.mjs的名字数组里登记 - 在
styles/components/directives/下加样式,并在directives.css里@import
指令的属性来自用户的 Markdown,直接拼进 HTML 会有注入风险。shared.mjs 提供了几个工具,务必使用:
escapeHtml(text)— 文本和属性值escapeUrl(url)— 链接,会限制协议,挡掉javascript:safeCssValue(v)— CSS 值,挡掉能跳出声明的字符resolveColor(c)— 颜色,白名单校验
// 不要这样`<div title="${attrs.title}">` // 这样`<div title="${escapeHtml(attrs.title)}">`指令里的界面文案通过 options 传入,不要硬编码:
export function processMyDirective(node, options = {}) { const i18n = options.i18n?.myDirective ?? { empty: '暂无内容' }; // ...}