Warning

本指南假设您有编写JavaScript的经验,并且熟悉TypeScript。

通常在网页上,我们使用HTML编写布局代码,如下所示:

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

这段HTML表示一篇文章,其前导标题为“文章标题”,段落包含文本“某些内容”。这与CSS相结合来设计页面样式,与JavaScript相结合来增加交互性。

然而,HTML不允许您创建可重复使用的模板。如果你想创建一个新页面,你需要复制并粘贴上面的片段,并自己编辑标题和内容。如果我们的网站上有很多内容共享很多类似的布局,那就不太好了。创建React的聪明人也有类似的抱怨,他们发明了组件的概念——返回JSX的JavaScript函数——来解决代码重复问题。

实际上,组件允许您编写一个JavaScript函数,该函数接受一些数据并生成HTML作为输出虽然Quartz不使用React,但它使用了相同的组件概念,允许您在Quartz网站中轻松表达布局模板

示例组件

Constructor

组件文件是在quartz/components文件夹中的.tsx 文件中编写的。这些内容以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: 任何元数据插件都可能已添加到当前页面。
    • fileData.slug: 当前页面的唯一标识。
    • fileData.frontmatter: 任何已解析的模版(frontmatter)。
  • cfg: quartz.config.ts中的 configuration 字段。
  • tree: 在处理和转换文件之后的HTML AST结果。如果您想使用hast-util-to-jsx-runtime 来呈现内容,这将非常有用(您可以在quarter/components/pages/Content.tsx中找到一个例子)。
  • allFiles: 已分析的所有文件的元数据。用于列出页面或了解整个网站结构。
  • displayClass: 一个实用的class,指示用户关于如何在移动或桌面设置中渲染它的偏好。如果你想有条件地隐藏移动设备或桌面上的组件,这很有帮助。一个实用程序类,指示用户关于如何在移动或桌面设置中渲染它的偏好。如果你想有条件地隐藏移动设备或桌面上的组件,这很有帮助。

Styling

Quartz组件还可以在实际功能组件上定义一个.css 属性,该属性将由Quartz获取。这应该是一个CSS字符串,可以内联,也可以从.scss 文件导入。

请注意,内联样式必须为普通CSS:

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模块,所以您在这里声明的任何样式都应用于_全局_。如果您只希望它应用于您的组件,请确保使用特定的类名和选择器。

Scripts 和交互

互动性呢?例如,假设您想要添加一个点击处理程序。与组件上的.css 属性一样,您也可以声明 .beforeDOMLoaded.afterDOMLoaded 属性,它们是包含脚本的字符串。

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

Hint

对于来自React的组件,Quartz组件与React组件的不同之处在于,它只使用JSX进行模板和布局。 useEffectuseState,等挂钩不会被呈现,接受onClick 处理程序等函数的其他属性也不会工作。相反,可以使用直接修改DOM元素的常规JS脚本来完成。

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

页面完全加载后,将执行.afterDOMLoaded脚本。这是一个很好的地方,可以设置任何应该在访问期间持续存在的东西(例如,从本地存储中获得一些东西)。

如果您需要创建一个afterDOMLoaded 脚本,该脚本依赖于导航到新页面时可能更改的特定元素,则可以侦听每当加载页面时触发的 "nav" 事件(如果启用了SPA Routing,则可能在导航时发生)。

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.afterDOM = 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 }

Then, you can use it like any other component in quartz.layout.ts via Component.YourComponent(). See the layout section for more details. 然后,您可以通过Component.YourComponent()像使用quartz.layout.ts中的任何其他组件一样使用它。请参阅布局部分了解更多详细信息。

由于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中查找更多石英组件示例,作为您自己组件的参考!