Blume中文文档

内容

交互岛

用 islands 目录或 defineComponents 添加只为自身、按需加载 JavaScript 的交互组件

默认情况下,Blume 会把你的文档渲染成零 JavaScript的静态 HTML。当你需要交互能力时——一个可实时运行的示例、一张图表、一个 playground——就加上一个交互岛:一个只为自身、且只在用到它的页面上加载 JS 的框架组件。

islands/ 约定#

把组件放进项目根目录下的 islands/ 文件夹里。它的文件名会成为你可以在任意 .mdx 页面中使用、无需 import 的组件:

import { useState } from "react";

export default function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Clicked {count}</button>;
}
Here's a live counter: <Counter />

文件名就是组件名,因此它必须是帕斯卡命名的标识符——只能包含字母、数字和下划线(Counter.tsx → <Counter />)。小写文件名,以及带连字符、点或空格的名字(比如 Time-Picker.tsx)会被跳过,并给出构建警告。当两个交互岛解析出同一个名字时——Counter.tsx 与 Counter.vue,或者不同子文件夹下的两个 Counter.tsx——Blume 会按文件路径保留第一个、忽略另一个,并给出构建警告。

在 components.ts 中注册交互岛#

如果你更愿意把交互岛放在其余组件旁边——或者想给它们一个与文件名不同的名字——就用 defineComponents 把它们注册为带 client 模式的 mdx 条目。交互岛无非就是一个会水合的 MDX 组件。每个条目在每个 MDX 页面里都可用,而与某个 islands/ 文件同名的条目会取代它。

import { defineComponents } from "blume";
import Counter from "./widgets/Counter.tsx";

export default defineComponents({
  mdx: {
    Counter: { component: Counter, client: "visible" }, // 在任意 MDX 页面中使用 <Counter />,并水合
  },
});

你可以通过 import 或路径字符串来引用组件,并为每个交互岛选择水合模式("media" 需要同时提供一个 media 查询):

export default defineComponents({
  mdx: {
    Chart: { component: "./widgets/Chart.tsx", client: "only" },
  },
});

与文件夹方式不同,这里没有默认值:不带 client 的 mdx 条目会渲染成静态 HTML,若该条目是 React、Vue 或 Svelte 组件,Blume 会给出警告。Blume 会静态解析 components.ts,因此一个条目必须是导入进来的组件、路径字符串,或一个 { component, client, media } 对象字面量——可接受的形式以及其它写法会得到的报错,见自定义。

水合#

默认情况下交互岛使用 client:visible:当读者把它滚动到视野中时才水合,因此即使满页都是交互岛也能瞬间加载。要换用别的策略,就在交互岛文件里写 export const client:

// 完全跳过服务端渲染——适用于会访问 DOM/window 的组件
export const client = "only";

export default function Chart() {
  /* ... */
}
client 取值 何时水合 适用场景
"visible" (默认) 滚动到视野中时 大多数交互岛
"load" 页面加载时立即 首屏、必须瞬时呈现的 UI
"idle" 主线程空闲时 不紧急的交互
"only" 仅客户端,永不服务端渲染 需要 window/document 的库(图表、编辑器)
"media" (components.ts 仅支持) 某个 CSS 媒体查询匹配时 只在特定尺寸下运行的 UI,比如仅移动端的菜单

"media" 需要配套的查询,因此它只对 components.ts 条目可用——{ component: Menu, client: "media", media: "(max-width: 50em)" }。在 islands/ 文件里声明它会退回到 "visible" 并给出警告。

框架#

React 开箱即用——一旦你的项目里出现 .tsx/.jsx 交互岛,Blume 就会自动启用它。

只要启用了 React,React Compiler 就默认开启,因此你的交互岛会被自动 memo 化——不需要手写 useMemo/useCallback。它随 Blume 一起提供,无需安装任何东西。要关掉它,在 blume.config.ts 中:

export default defineConfig({
  react: { compiler: false },
});

Vue 和 Svelte 同样受支持;安装对应的 Astro 集成,Blume 会在看到 .vue 或 .svelte 交互岛时接好渲染器:

# Vue
npm install @astrojs/vue vue

# Svelte
npm install @astrojs/svelte svelte
<script setup>
import { ref } from "vue";
const on = ref(false);
</script>

<template>
  <button @click="on = !on">{{ on ? "On" : "Off" }}</button>
</template>

你在 MDX 里传入的属性(<Counter start={5} />)会转发给组件,子节点(<Counter>label</Counter>)则作为默认插槽传入。

Hooks#

交互岛各自独立水合,因此没有 React context 可以用来层层传递项目数据。取而代之,blume/hooks 会读取一小份由布局序列化进页面的快照——不需要逐层传属性:

import { useBlume, usePage } from "blume/hooks";

export default function PageInfo() {
  const blume = useBlume();
  const page = usePage();
  if (!(blume && page)) {
    return null;
  }
  return (
    <p>
      You're reading <strong>{page.title}</strong> on {blume.config.title}.
    </p>
  );
}
Hook 返回值
useBlume() 站点的 { config, navigation },挂载前为 null
usePage() 当前页面的 { route, title },挂载前为 null
useSearch() { search, results, loading }——查询已配置的搜索服务
useAssistant() { ask, messages, loading, reset }——从 assistant 接口流式输出

useBlume() 和 usePage() 在交互岛挂载前会返回 null(这样服务端与客户端渲染出的首帧一致)——请为此做判断。快照只在携带 React 的页面上输出,因此全静态站点不为此付出任何代价。

在用 PageLayout 构建的自定义页面上,传入 clientData,好让那里的交互岛能读取它:

<PageLayout
  clientData={{ config: data.config, navigation: data.navigation, page: { route: "/", title: "Home" } }}
  {/* …other props… */}
/>