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 集合:
// 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;
}
export default (() => {
function YourComponent() {
return <p class="red-text">Example Component</p>
}
YourComponent.css = `
p.red-text {
color: red;
}
`
return YourComponent
}) satisfies QuartzComponentConstructor
不过,导入的样式可以来自 SCSS 文件:
// 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
属性,这些属性是包含脚本的字符串。
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 进行模板化和布局。诸如
useEffect
、useState
等钩子不会被渲染,且接受函数类型的属性(如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
文件导入组件代码。
// @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
// 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
中重新导出它:
import ArticleTitle from "./ArticleTitle"
import Content from "./pages/Content"
import Darkmode from "./Darkmode"
import YourComponent from "./YourComponent"
export { ArticleTitle, Content, Darkmode, YourComponent }
由于 Quartz 组件只是返回 React 组件的函数,因此你可以在其他 Quartz 组件中组合式地使用它们。
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
中的更多组件示例,作为你自己组件的参考!