TypeScript 架构分析 推荐学习

Quartz 架构分析

一个"管道 + 插件"架构的静态站点生成器:Markdown 进、网站出

📅 2026-05-25 🔗 jackyzha0/quartz v4 📐 架构视角

🏗️ 整体架构

Quartz 的架构可以用一句话概括:三阶段管道 + 可插拔插件 + JSX 组件布局

架构鸟瞰图

📁 输入层
content/ 目录下的 Markdown 文件 + 静态资源(图片、PDF 等)
⚙️ 引导层 — bootstrap-cli.mjs
esbuild 转译 TypeScript → 缓存到 .quartz-cache/ → 动态 import → 传入 CLI 参数
🧩 处理层 — 三阶段管道
Transformers(解析 Markdown AST)→ Filters(过滤内容)→ Emitters(生成 HTML)
🏠 输出层
public/ 目录:纯静态 HTML + CSS + JS,可部署到任意静态托管

⚙️ 启动与构建流程

当你执行 npx quartz build 时,背后发生了以下事情:

Step 1:CLI 入口

package.jsonbin 字段指向 quartz/bootstrap-cli.mjs,npm 通过 shebang 行用 Node.js 执行它。

Step 2:esbuild 转译

引导程序用 esbuild 将整个 TypeScript 代码库转译为 JavaScript。关键细节:

Step 3:动态导入并执行

// 伪代码:引导流程
const cacheFile = ".quartz-cache/transpiled-build.mjs"
const buildModule = await import(cacheFile)
await buildModule.buildQuartz(argv, mutex, clientRefreshCallback)

Step 4:开发模式(--serve)

如果带 --serve 标志,还会启动 chokidar 文件监听器:

设计洞察:双层 esbuild

Quartz 巧妙地用了两个 esbuild 实例:一个在 Node 端转译构建逻辑,另一个打包浏览器端脚本。这使得 *.inline.ts 文件可以像普通模块一样 import 其他 Quartz 源码,但最终输出是浏览器可执行的 bundle。

🌐 三阶段处理管道

Quartz 的核心是一个三阶段管道,定义在 quartz/build.ts 中:

// build.ts 核心流程(简化)
const initialContent = await parseMarkdown(directory, filePaths, pluginTransformers, ctx)
const filteredContent = filterContent(initialContent, pluginFilters)
await emitContent(directory, filteredContent, pluginEmitters, ctx, staticResources, fps)

阶段 1:Transformers — 解析与转换

parseMarkdown() 遍历所有 .md 文件,依次经过每个 Transformer 插件处理:

职责典型插件做什么
Frontmatter 解析FrontMatter()提取 YAML 头部元数据
Markdown 增强OxHugoFlavoredMarkdown()兼容 Hugo 风格的 Markdown 扩展
数学公式Latex()将 LaTeX 语法转为 KaTeX/MathJax 渲染
代码高亮SyntaxHighlighting()为代码块添加语法高亮
链接处理CrawlLinks()解析 wikilinks、处理相对路径
描述生成Description()从内容自动提取页面描述

阶段 2:Filters — 内容过滤

filterContent() 对已处理的内容进行过滤,决定哪些页面会被发布:

// Filter 插件接口(简化)
interface QuartzFilterPlugin {
  name: string
  shouldPublish(content: ProcessedContent): boolean
}

阶段 3:Emitters — 内容生成

emitContent() 将过滤后的内容转化为最终的 HTML/CSS/JS 文件:

Emitter职责
ContentPage()为每个 Markdown 页面生成 HTML 页面(使用组件布局)
TagPage()生成标签聚合页
AliasRedirects()为页面别名生成重定向 HTML
ComponentResources()打包所有组件的 CSS/JS 资源
Static()复制静态资源文件
Sitemap()生成 sitemap.xml
Assets()处理图片等资源文件
管道设计的优势

三阶段分离使得每一层都可以独立扩展:你可以只添加一个 Transformer 来支持新的 Markdown 语法,而不影响过滤和输出逻辑。这是经典的管道-过滤器架构模式。

🧩 插件系统

Quartz 的所有内容处理逻辑都通过插件实现,用户在 quartz.config.ts 中配置:

const config: QuartzConfig = {
  configuration: { /* 站点级配置 */ },
  plugins: {
    transformers: [  // 阶段 1
      Plugin.FrontMatter(),
      Plugin.Latex({ renderEngine: "katex" }),
      Plugin.SyntaxHighlighting(),
      Plugin.CrawlLinks(),
      Plugin.Description(),
    ],
    filters: [       // 阶段 2
      Plugin.RemoveDrafts(),
    ],
    emitters: [      // 阶段 3
      Plugin.ContentPage(),
      Plugin.TagPage(),
      Plugin.ComponentResources(),
      Plugin.Static(),
      Plugin.Sitemap(),
    ],
  },
}

插件接口定义

每种类型的插件都有明确的 TypeScript 接口:

// Transformer 插件
type QuartzTransformerPlugin<Options extends object | undefined = undefined> = {
  name: string
  htmlPlugins?: (ctx: BuildCtx) => PluggableList  // remark/rehype 插件
  externalResources?: (ctx: BuildCtx) => StaticResources
  markdownPlugins?: (ctx: BuildCtx) => PluggableList
}

// Filter 插件
type QuartzFilterPlugin = {
  name: string
  shouldPublish: (content: ProcessedContent) => boolean
}

// Emitter 插件
type QuartzEmitterPlugin<Options extends object | undefined = undefined> = {
  name: string
  emit: (ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources) => Promise<FilePath[]>
  getQuartzComponents?: (ctx: BuildCtx) => QuartzComponent[]
}

插件工厂模式

每个插件是一个工厂函数,接受选项参数,返回插件对象:

// 插件工厂示例
export const Latex: QuartzTransformerPlugin<{ renderEngine: "katex" | "mathjax" }> = (opts) => {
  return {
    name: "Latex",
    markdownPlugins(ctx) {
      return [remarkMath]  // remark 插件:解析数学语法
    },
    htmlPlugins(ctx) {
      return [
        rehypeMathjax,     // rehype 插件:渲染为 HTML
        opts?.renderEngine === "katex" ? rehypeKatex : rehypeMathjax,
      ]
    },
  }
}
设计洞察:基于 Unified 生态

Transformer 插件不是自己实现 Markdown 解析,而是返回 remark/rehype 插件列表。Quartz 将它们统一编排为一个处理链:Markdown → remark 插件链 → MDAST → rehype 插件链 → HAST → HTML。这意味着任何现有的 remark/rehype 插件都可以直接复用。

🧬 组件系统

Quartz 的页面布局通过 JSX 组件系统实现,定义在 quartz/components/ 目录下。

组件接口

type QuartzComponent = (props: QuartzComponentProps, children: JSX.Element[]) => JSX.Element
type QuartzComponentProps = {
  fileData: FrontmatterDatum
  cfg: GlobalConfiguration
  allFiles: ContentDetails[]
  displayClass?: "mobile-only" | "desktop-only"
}

布局配置

用户在 quartz.layout.ts 中定义页面的区域布局:

const layout: FullPageLayout = {
  head: Component.Head(),           // <head> 区域
  header: [Component.PageTitle()],  // 页面标题区
  beforeBody: [Component.TagList()], // 正文前
  pageBody: Component.Content(),    // 主内容区
  afterBody: [Component.Backlinks()],// 正文后
  left: [Component.Explorer()],     // 侧边栏
  footer: Component.Footer(),       // 页脚
}

内置组件

组件功能
Head生成 HTML <head>,注入 CSS/JS 资源
PageTitle显示页面标题
Content渲染 Markdown 正文
Explorer文件树导航
Backlinks反向链接列表
Graph知识图谱可视化(D3.js)
Search全文搜索(FlexSearch)
TagList页面标签
TableOfContents目录导航
Footer页脚信息

客户端脚本注入

组件可以声明 *.inline.ts 文件作为客户端脚本。这些文件被 esbuild 单独打包后注入到页面中。例如搜索组件的 search.inline.ts 在浏览器端执行 FlexSearch 初始化。

⚡ SPA 路由机制

Quartz 实现了客户端 SPA 路由,使得页面切换无需整页刷新:

工作原理

  1. 拦截点击:监听所有内部链接的点击事件
  2. 预加载:鼠标悬停时通过 <link rel="prefetch"> 预加载目标页面
  3. 局部替换:fetch 目标页面 HTML,只替换 <body> 中的动态部分
  4. 更新历史:通过 history.pushState 更新 URL,不触发页面刷新
  5. 重新初始化:触发组件的客户端脚本重新绑定

性能影响

Tradeoff:客户端 JS 较重

SPA 路由 + 搜索 + 图谱 + 其他交互组件使得 Quartz 页面包含较多客户端 JS。对于追求"零 JS"的场景,这是一个需要权衡的点。

📊 核心数据结构

BuildCtx — 构建上下文

贯穿整个构建流程的上下文对象:

interface BuildCtx {
  buildId: string           // 本次构建的唯一 ID
  argv: Argv                // CLI 参数
  cfg: QuartzConfig         // 用户配置
  allSlugs: FullSlug[]      // 所有页面的 slug 列表
  allFiles: FilePath[]      // 所有文件路径
  incremental: boolean      // 是否增量构建
}

ContentMap — 内容映射表

构建过程中维护的全局内容索引:

type ContentMap = Map<FilePath,
  | { type: "markdown"; content: ProcessedContent }
  | { type: "other" }
>

Markdown 文件存储处理后的 AST + VFile 元数据,非 Markdown 文件只标记类型。

ProcessedContent — 处理后的内容

经过 Transformer 阶段处理后的内容,包含 [MDAST/HAST tree, VFile] 二元组。VFile 携带 frontmatter、slug、依赖关系等元数据。

StaticResources — 静态资源

所有插件声明的 CSS/JS 资源的集合,最终由 ComponentResources emitter 统一打包输出。

🔧 技术栈选型

层次技术选型为什么选它
语言TypeScript (85.2%)类型安全 + IDE 支持 + 社区生态
运行时Node.js >= v22最新 LTS,支持最新 JS 特性
构建工具esbuildGo 编写,构建速度极快;支持双目标(Node + Browser)
Markdown 处理unified + remark + rehype最成熟的 Markdown AST 处理生态,插件丰富
模板/布局JSX(Preact 兼容)组件化布局,开发者熟悉度高
样式SCSSCSS 超集,支持变量、嵌套、mixin
客户端交互原生 JS + D3.js(图谱)轻量,避免引入大型框架
搜索FlexSearch客户端全文搜索,无需服务端
文件监听chokidar跨平台文件监听,开发模式热重载

⚖️ 设计取舍

取舍 1:TypeScript 配置 vs YAML/TOML

选择收益代价
用 TS 写配置类型检查、自动补全、可在配置中写逻辑非开发者学习成本更高

取舍 2:esbuild 专用 vs 通用打包器

选择收益代价
esbuild构建速度极快,API 简洁不支持 CSS Modules 等高级特性

取舍 3:客户端 SPA vs 纯静态

选择收益代价
SPA 路由 + 客户端搜索页面切换极快,搜索无需服务端JS bundle 较大,首屏需要加载更多资源

取舍 4:个人维护 vs 组织维护

选择收益代价
单人维护 + 社区贡献设计一致性高,决策快发版节奏慢,bus factor = 1

取舍 5:fork 源码 vs npm 包

选择收益代价
直接 fork 仓库可自由修改任何源码,深度定制升级需要手动 merge,维护成本高

📦 可复用模式

模式 1:管道-过滤器架构

Quartz 的三阶段管道是经典的管道-过滤器模式。每个插件是一个过滤器,通过配置组合形成处理链。适用于:数据处理流水线、ETL 工具、内容转换系统。

模式 2:工厂函数 + 选项对象

所有插件通过工厂函数创建,接受选项对象参数。这比 class 继承更灵活,也更容易做 tree-shaking。

// 可复用的插件工厂模式
export const MyPlugin: QuartzPlugin<MyOptions> = (opts = defaults) => ({
  name: "MyPlugin",
  // ... 实现
})

模式 3:双目标 esbuild 打包

一个代码库同时为 Node.js 和浏览器两个目标构建。通过文件命名约定(*.inline.ts)区分打包目标。适用于:需要在服务端和客户端都执行的工具。

模式 4:VFile 元数据传递

使用 unified 生态的 VFile 机制在插件之间传递元数据,避免全局状态。每个文件的处理结果自包含,便于增量构建和并行处理。

模式 5:增量构建 + 文件监听

通过 ContentMap 记录所有已处理内容,增量构建时只重新处理变更文件。配合 chokidar 监听和 Mutex 锁,实现安全的开发模式热重载。

📝 个人备注