社交媒体

从一段 HTML 到可运行的 Playground

从一段 HTML 到可运行的 Playground

为什么需要可运行示例

技术文章里最让人犹豫的一句话是“复制下面的代码即可运行”。读者真正复制时,可能发现缺少样式、入口文件名不同,或依赖版本早已变化。截图能证明作者当时看见了结果,却无法让后来的人检查过程。

我想要一种介于静态代码块和完整项目仓库之间的形式:示例足够小,可以嵌在文章里;文件关系保持清楚,读者可以修改并立即预览;即使运行器失效,源码仍然留在正文中,而不是只剩一个空白框。

Moonlit Playground 因此采用结构化多文件内容块。它支持原生 HTML、CSS、JavaScript,也支持 React;可以指定入口文件、npm 依赖、初始打开文件,以及文件是否隐藏或只读。下面五个例子分别验证自动运行、React 手动运行、控制台、多文件权限和静态回退。

一、原生模板与自动预览

第一个例子只有 HTML 与 CSS,不包含敏感信息。它显式使用自动运行,用来展示页面打开后直接生成预览的效果。正式文章仍应优先使用手动运行,因为自动模式会更早连接第三方沙箱。

<main class="moon-card">
  <p class="eyebrow">MOON PHASE / 07</p>
  <h1>今晚适合慢一点</h1>
  <p>一个不依赖脚本的原生预览。</p>
</main>
<link rel="stylesheet" href="/styles.css" />
:root { color-scheme: dark; font-family: system-ui, sans-serif; }
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #090d12; color: #e8edf2; }
.moon-card { width: min(420px, calc(100% - 40px)); padding: 32px; border: 1px solid #26313b; border-radius: 20px; background: #111820; }
.eyebrow { color: #7ee0c3; letter-spacing: .14em; }

二、手动运行的 React 状态

React 示例把同一个想法改写成状态组件。示例明确声明 React 与 React DOM 版本,默认停在源码视图;只有读者点击运行后,文件和依赖才会交给 Sandpack。

import React, { useState } from "react";

export default function App() {
  const [observing, setObserving] = useState(false);
  return (
    <main className="moon-card">
      <p className="eyebrow">MOON PHASE / 07</p>
      <h1>今晚适合慢一点</h1>
      <p>{observing ? "正在观测:请把屏幕亮度调低一些。" : "观测暂停,月光仍留在窗沿。"}</p>
      <button type="button" onClick={() => setObserving((value) => !value)}>
        {observing ? "暂停观测" : "开始观测"}
      </button>
    </main>
  );
}
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./styles.css";

createRoot(document.getElementById("root")).render(<App />);
html, body, #root { min-height: 100%; margin: 0; }
body { display: grid; place-items: center; color: #eef4f2; background: #090d12; font-family: system-ui; }
.moon-card { width: min(430px, calc(100% - 40px)); padding: 32px; border: 1px solid #294039; border-radius: 20px; background: #101a18; }
.eyebrow { color: #7ee0c3; letter-spacing: .14em; }
button { padding: 10px 16px; border: 0; border-radius: 999px; background: #7ee0c3; color: #07110e; font: inherit; }
button:focus-visible { outline: 3px solid #d6ed55; outline-offset: 4px; }

三、把观察结果留在 Console

有些示例不需要视觉预览,重点是数据转换与调试过程。这个原生项目把 Console 设为默认视图,并使用表格和断言展示输出。

<main id="app">打开 Console 查看月相数据。</main>
<script type="module" src="/index.js"></script>
const observations = [
  { time: "19:40", cloud: 18, visible: true },
  { time: "21:10", cloud: 46, visible: true },
  { time: "22:35", cloud: 92, visible: false },
];

console.table(observations);
console.assert(observations.some((item) => item.visible), "今晚至少有一次可见记录");
console.log("平均云量", observations.reduce((sum, item) => sum + item.cloud, 0) / observations.length);

四、多文件、隐藏与只读

隐藏文件只是减少标签栏噪音,绝不等于保密。下面的天气数据会发送到浏览器和运行环境;只读样式也仍然能够被任何查看文章源码的人读到。这里刻意使用完全公开的演示数据。

<main id="app" aria-live="polite"></main>
<link rel="stylesheet" href="/styles.css" />
<script type="module" src="/app.js"></script>
import { weather } from "./data.js";

document.querySelector("#app").innerHTML =
  "<p class="eyebrow">NIGHT WEATHER</p>" +
  "<h1>" + weather.condition + "</h1>" +
  "<p>体感 " + weather.feelsLike + "℃ · 东北风 " + weather.wind + " 级</p>";
export const weather = {
  condition: "薄云间晴",
  feelsLike: 16,
  wind: 2,
};
body { min-height: 100vh; margin: 0; display: grid; place-items: center; background: #0a0f15; color: #edf4f1; font-family: system-ui; }
#app { width: min(390px, calc(100% - 40px)); padding: 28px; border: 1px solid #314138; border-radius: 18px; background: #121a1b; }
.eyebrow { color: #7ee0c3; letter-spacing: .12em; }

五、即使不运行,也留下可读源码

最后一个块关闭预览和控制台,只把一个完整 HTML 文件作为可读内容保存。JavaScript 被禁用、运行器加载失败或读者不愿连接第三方服务时,下面的 precode 仍然存在。这是渐进增强的底线。

<article>
  <p>即使运行器没有加载,这段源码仍然可以阅读与复制。</p>
  <blockquote>增强可以缺席,内容不能一起消失。</blockquote>
</article>

多文件不是越多越好

Playground 支持隐藏文件和只读文件,但“隐藏”只是界面选项,不是保密机制。隐藏的初始化文件仍会随文章内容发送到浏览器,也会在运行时进入第三方沙箱。因此示例必须完全公开,不能包含密码、访问令牌、个人信息和私有源码。

只读适合用于不希望读者误删的启动文件,隐藏适合把与讲解无关的样板代码收起。真正影响例子的文件应保持可见,让读者理解结果来自哪里。

运行之前说明边界

运行环境由 CodeSandbox Sandpack 提供。读者点击运行、预览或外部打开后,示例源码和依赖会发送到该第三方服务。文章应明确说明这一点,并默认采用手动运行,给读者做决定的机会。

如果 JavaScript 被禁用、运行器加载失败,或插件遇到未来版本协议,结构化源码仍应保留。渐进增强的重点不是保证所有环境拥有同样体验,而是增强失败后基础内容仍然成立。

让示例成为文章的一部分

一个好的 Playground 不该只是页面里的技术烟花。正文先解释问题,代码块承载可验证步骤,预览展示结果,控制台帮助观察过程。读者可以只阅读,也可以在某一步停下来修改颜色、文案或状态。

当代码能够被触摸,技术文章就从“相信这个结论”变成“自己改变一个变量看看”。这正是我希望 Playground 提供的能力:不是替代完整项目,而是在文章与实践之间搭一块足够稳的小平台。