Warning

本指南假设您具备JavaScript编写经验并熟悉TypeScript。

通常在网上,我们使用HTML编写布局代码,看起来像下面这样:

<article>
  <h1>An article header</h1>
  <p>Some content</p>
</article>

这段HTML代码展示了一个包含标题”An article header”的文章头部,以及内容段落”Some content”。通过结合CSS进行页面样式设计,并利用JavaScript实现交互功能。

然而,HTML本身并不支持创建可复用的模板组件。若需新建页面,您不得不手动复制粘贴上述代码片段并逐个修改标题和内容。这对于拥有大量相似布局内容的网站而言显然不够高效。React框架的创造者们也曾面临类似困扰,为此他们提出了**组件(Components)**的概念——即返回JSX的JavaScript函数——以解决代码重复问题。

本质上,组件允许您编写一个接收数据并输出HTML的JavaScript函数。虽然Quartz并未直接采用React框架,但它借鉴了相同的组件化理念,使您能够轻松地在Quartz站点中构建布局模板。

组件示例

构造函数

组件文件以 .tsx 格式编写,并存放于 quartz/components 目录下。这些组件通过 quartz/components/index.ts 文件进行统一导出,以便在布局文件和其他组件中便捷调用。

每个组件文件应包含一个默认导出项,该导出项需符合 QuartzComponentConstructor 函数签名规范。该函数接收一个可选参数 opts,并返回一个Quartz组件。参数 opts 的类型由您作为组件创建者定义的 Options 接口决定。

在组件内部,您可以通过配置选项中的值来动态调整渲染行为。例如,下方代码片段中的组件将在 favouriteNumber 选项值小于0时停止渲染。

interface Options {
  favouriteNumber: number
}
 
const defaultOptions: Options = {
  favouriteNumber: 42,
}
 
export default ((userOpts?: Options) => {
  const opts = { ...userOpts, ...defaultOpts }
  function YourComponent(props: QuartzComponentProps) {
    if (opts.favouriteNumber < 0) {
      return null
    }
 
    return <p>My favourite number is {opts.favouriteNumber}</p>
  }
 
  return YourComponent
}) satisfies QuartzComponentConstructor

Props

Quartz 组件本身(上文高亮的第 11-17 行)看起来像是一个 React 组件。它接收属性(通常称为 props)并返回 JSX。

所有 Quartz 组件都接受相同的 props 集合:

quartz/components/types.ts
// simplified for sake of demonstration
export type QuartzComponentProps = {
  fileData: QuartzPluginData
  cfg: GlobalConfiguration
  tree: Node<QuartzPluginData>
  allFiles: QuartzPluginData[]
  displayClass?: "mobile-only" | "desktop-only"
}
  • fileData: 当前页面的元数据,可能由plugins添加。
    • fileData.slug: 当前页面的别名。
    • fileData.frontmatter: 已解析的 frontmatter 数据。
  • cfg: quartz.config.ts 中的 configuration 字段。
  • tree: 文件处理后生成的 HTML AST。如果你想使用 hast-util-to-jsx-runtime 渲染内容会很有用(可以在 quartz/components/pages/Content.tsx 中找到示例)。
  • allFiles: 所有已解析文件的元数据。适用于生成页面列表或构建网站整体结构。
  • displayClass: 表示用户对移动端或桌面端渲染偏好的实用类。可用于根据设备类型条件性隐藏组件。

样式设计

Quartz 组件可以在函数组件上定义 .css 属性,该属性会被 Quartz 自动识别。这个属性应该是 CSS 字符串,可以直接内联或从 .scss 文件导入。

请注意内联样式必须是纯原生 CSS:

.component-class {
  color: red;
}
quartz/components/YourComponent.tsx
export default (() => {
  function YourComponent() {
    return <p class="red-text">Example Component</p>
  }
 
  YourComponent.css = `
  p.red-text {
    color: red;
  }
  `
 
  return YourComponent
}) satisfies QuartzComponentConstructor

不过,导入的样式可以来自 SCSS 文件:

quartz/components/YourComponent.tsx
// assuming your stylesheet is in quartz/components/styles/YourComponent.scss
import styles from "./styles/YourComponent.scss"
 
export default (() => {
  function YourComponent() {
    return <p>Example Component</p>
  }
 
  YourComponent.css = styles
  return YourComponent
}) satisfies QuartzComponentConstructor

Warning

Quartz 不使用 CSS 模块,因此您在此声明的所有样式都将_全局应用_。如果只想让样式作用于特定组件,请确保使用具体的类名和选择器。

脚本与交互性

如何实现交互功能?假设您需要添加点击事件处理程序。与组件上的 .css 属性类似,您还可以声明 .beforeDOMLoaded.afterDOMLoaded 属性,这些属性是包含脚本的字符串。

quartz/components/YourComponent.tsx
export default (() => {
  function YourComponent() {
    return <button id="btn">Click me</button>
  }
 
  YourComponent.beforeDOMLoaded = `
  console.log("hello from before the page loads!")
  `
 
  YourComponent.afterDOMLoaded = `
  document.getElementById('btn').onclick = () => {
    alert('button clicked!')
  }
  `
  return YourComponent
}) satisfies QuartzComponentConstructor

Hint

对于来自 React 的开发者,需要了解 Quartz 组件与 React 组件的不同之处:Quartz 仅使用 JSX 进行模板化和布局。诸如 useEffectuseState 等钩子不会被渲染,且接受函数类型的属性(如 onClick 事件处理器)也无法正常工作。如需实现类似功能,应通过直接操作 DOM 元素的普通 JavaScript 脚本来完成。

顾名思义,.beforeDOMLoaded 脚本会在页面加载完成之前执行,因此无法访问页面上的任何元素。这通常用于预加载关键数据。

.afterDOMLoaded 脚本则会在页面完全加载后执行。这是设置需要在整个网站访问期间持续存在的内容的理想位置(例如从本地存储中获取保存的数据)。

如果需要创建依赖于页面特定元素(这些元素可能在导航到新页面时发生变化)的 afterDOMLoaded 脚本,可以监听每当页面加载时触发的 "nav" 事件(如果启用了SPA 路由,在导航时也可能触发该事件)。

document.addEventListener("nav", () => {
  // do page specific logic here
  // e.g. attach event listeners
  const toggleSwitch = document.querySelector("#switch") as HTMLInputElement
  toggleSwitch.addEventListener("change", switchTheme)
  window.addCleanup(() => toggleSwitch.removeEventListener("change", switchTheme))
})

最佳实践是通过 window.addCleanup 跟踪所有事件处理程序以防止内存泄漏。 该方法会在页面导航时被调用。

导入代码

当然,在组件中直接以字符串字面量的形式编写代码并不总是可行(也不推荐)。

Quartz 支持通过 .inline.ts 文件导入组件代码。

quartz/components/YourComponent.tsx
// @ts-ignore: typescript doesn't know about our inline bundling system
// so we need to silence the error
import script from "./scripts/graph.inline"
 
export default (() => {
  function YourComponent() {
    return <button id="btn">Click me</button>
  }
 
  YourComponent.afterDOMLoaded = script
  return YourComponent
}) satisfies QuartzComponentConstructor
quartz/components/scripts/graph.inline.ts
// any imports here are bundled for the browser
import * as d3 from "d3"
 
document.getElementById("btn").onclick = () => {
  alert("button clicked!")
}

此外,如上面的示例所示,您可以在 .inline.ts 文件中导入包。这些内容将由 Quartz 打包并包含在实际脚本中。

使用组件

创建自定义组件后,请在 quartz/components/index.ts 中重新导出它:

quartz/components/index.ts
import ArticleTitle from "./ArticleTitle"
import Content from "./pages/Content"
import Darkmode from "./Darkmode"
import YourComponent from "./YourComponent"
 
export { ArticleTitle, Content, Darkmode, YourComponent }
然后,你就可以像使用 `quartz.layout.ts` 中的其他组件一样,通过 `Component.YourComponent()` 来使用它。更多详细信息,请参阅[[configuration#layout|布局]]部分。

由于 Quartz 组件只是返回 React 组件的函数,因此你可以在其他 Quartz 组件中组合式地使用它们。

quartz/components/AnotherComponent.tsx
import YourComponent from "./YourComponent"
 
export default (() => {
  function AnotherComponent(props: QuartzComponentProps) {
    return (
      <div>
        <p>It's nested!</p>
        <YourComponent {...props} />
      </div>
    )
  }
 
  return AnotherComponent
}) satisfies QuartzComponentConstructor

Hint

查看 quartz/components 中的更多组件示例,作为你自己组件的参考!