返回首页

在文章里嵌入交互演示:Sketch 组件使用说明

1156 字 6 分钟
目录

本博客支持在文章中嵌入沙箱化的交互式 HTML 演示——典型用途是算法可视化、动画、可玩的小工具。本篇是工作流的官方说明,也是组件本身的回归用例。

下面就是一个最简单的例子:

工作原理#

每个 demo 是一个独立的 .html 文件,构建时通过 Vite 的 ?raw 后缀读成字符串,由 <Sketch> 组件渲染成一个 <iframe srcdoc=...>。iframe 开了 sandbox="allow-scripts"不开 allow-same-origin,因此里面的脚本拿不到本站的 cookie、localStorage、DOM。换句话说,即使把第三方代码塞进去也基本无害。

iframe 高度默认会自动适配内容(通过 postMessage 把 body 高度报给父页面),不需要手动调比例。

目录约定:文章包#

写一篇带 demo 的文章时,不要用扁平的 xxx.md,而是开一个同名文件夹,里面放 index.mdx + 任意数量的 .html

src/content/posts/<分类>/<文章名>/
index.mdx ← 文章本体(注意是 mdx 不是 md)
sketch.html ← demo 1
sketch-2.html ← demo 2(一篇文章可以有多个)
diagram.html ← 任意命名都行

Astro 5 会自动把 <文章名>/index.mdx 的 slug 处理成 <文章名>,URL 干净。但要注意:如果同分类下已经存在扁平的 <文章名>.md,必须先删掉,否则会撞 slug 报错(DuplicateContentEntrySlugError)。

老文章保留 .md 平铺形式即可,没有 demo 就不用迁移。

在 MDX 里插入 demo#

.mdx 顶部加两行 import:

import sketch from "./sketch.html?raw"
import Sketch from "@/components/Sketch.astro"

正文里需要的地方写一行:

<Sketch html={sketch} title="正弦波演示" />

如果一篇文章有多个 demo,分别 import 不同名字即可:

import sortDemo from "./sort.html?raw"
import graphDemo from "./graph.html?raw"
<Sketch html={sortDemo} title="快速排序" />
...一些解释文字...
<Sketch html={graphDemo} title="图的广度优先搜索" />

<Sketch> 组件参数#

参数类型默认值说明
htmlstring(必填)通过 ?raw 导入的 HTML 字符串
titlestring"交互演示"iframe 的 title,给屏幕阅读器用
autoSizebooleantrue自动适配高度
heightstring显式高度(如 "600px"),优先级最高
aspectstring固定宽高比(如 "16 / 9""4 / 3"

优先级height > aspect > autoSize > fallback 16/10

常见姿势:

<!-- 默认:内容多高 iframe 多高 -->
<Sketch html={sketch} />
<!-- 全屏沉浸式 demo -->
<Sketch html={sketch} height="80vh" />
<!-- 固定比例,响应式 -->
<Sketch html={sketch} aspect="16 / 9" autoSize={false} />

写 demo HTML 的建议#

1. 文件可以是片段也可以是完整文档

组件会检测开头是不是 <!doctype,不是就自动包一层 <html><body>。所以下面两种写法都合法:

<!-- 完整文档(推荐,可以双击直接用浏览器打开调试) -->
<!doctype html>
<html>
<head><style>...</style></head>
<body>
<canvas id="c"></canvas>
<script>...</script>
</body>
</html>
<!-- 片段(更短) -->
<canvas id="c"></canvas>
<script>...</script>

2. 写完整文档的好处

直接在文件管理器双击 sketch.html 就能在浏览器里打开调试,不用启 pnpm dev。报错信息会指向真实文件名而不是 about:srcdoc。开发期建议都用完整文档形式。

3. 让 demo 自适应宽度

iframe 会占满文章正文宽度,所以 demo 里的 canvas / 容器最好别写死像素宽度:

canvas {
width: 100%;
max-width: 640px; /* 限个上限,避免在大屏上太大 */
height: auto;
}

4. 配色跟博客主题

iframe 内是独立文档,不会继承博客的 CSS 变量。如果 demo 要在亮/暗模式都好看,自己写媒体查询:

@media (prefers-color-scheme: dark) {
body { background: #0d1117; color: #e6edf3; }
}
@media (prefers-color-scheme: light) {
body { background: #fff; color: #24292f; }
}

5. 关于外部资源

因为 sandbox 不开 allow-same-origin,iframe 内不能用相对路径引用同目录的图片或数据文件。要么:

  • 把图片转 base64 内联进 HTML
  • 用 CDN 的绝对 URL(如 https://cdn.jsdelivr.net/...
  • 把数据 inline 成 JS 字面量

引用 CDN 的库(D3、p5.js 之类)完全没问题:

<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>

安全说明#

iframe 沙箱配置:

  • sandbox="allow-scripts" —— 允许跑脚本
  • 不开 allow-same-origin —— 脚本拿不到本站 cookie / DOM
  • 不开 allow-top-navigation —— 不能跳转父页面
  • 不开 allow-forms —— 不能提交表单
  • 不开 allow-popups —— 不能开新窗口

外层 .sketch 容器加了 data-pagefind-ignore,demo 源码不会污染站内搜索索引。

调试技巧#

  1. 开发期直接双击打开 .html 文件——浏览器原生跑,DevTools 完整可用,改完刷新即可。等满意了再到博客页面看嵌入效果。
  2. iframe 内报错会显示 about:srcdoc 路径,定位不便。如果排查困难,临时把 demo 拆成独立文件用第 1 条方式调。
  3. postMessage 不工作 / 高度不对:检查 demo 里有没有覆盖 <body> 的 height 计算,或者大量异步加载图片后没触发 resize。极端情况可以显式传 height="600px"

相关文件#