本博客支持在文章中嵌入沙箱化的交互式 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> 组件参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
html | string | (必填) | 通过 ?raw 导入的 HTML 字符串 |
title | string | "交互演示" | iframe 的 title,给屏幕阅读器用 |
autoSize | boolean | true | 自动适配高度 |
height | string | — | 显式高度(如 "600px"),优先级最高 |
aspect | string | — | 固定宽高比(如 "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 源码不会污染站内搜索索引。
调试技巧
- 开发期直接双击打开
.html文件——浏览器原生跑,DevTools 完整可用,改完刷新即可。等满意了再到博客页面看嵌入效果。 - iframe 内报错会显示
about:srcdoc路径,定位不便。如果排查困难,临时把 demo 拆成独立文件用第 1 条方式调。 postMessage不工作 / 高度不对:检查 demo 里有没有覆盖<body>的 height 计算,或者大量异步加载图片后没触发 resize。极端情况可以显式传height="600px"。
相关文件
- 组件源码:src/components/Sketch.astro
- 本文 demo:
src/content/posts/TechnicalTutorials/sketch-demo/sketch.html