My Terrarium 盆栽应用实战:用 HTML/CSS/JS 实现 DOM 拖拽交互并部署到 Azure Static Web Apps
My Terrarium("我的生态瓶")是 3-terrarium 模块随附的一份完整示例工程:只用少量 HTML、CSS 与 JavaScript,即可完成一个可自由拖拽植物的静态网页——既能练习 DOM 操作与闭包,也能体验"写好代码即可发布"的纯静态站点部署流程。本文以该工程的自述文档为主线,结合 solution 目录 中的真实源码,讲解应用结构、拖拽交互实现,并完整还原通过 Azure Static Web Apps 将其发布到公网的三步走流程。读完你既能看懂这段"代码冥想"式的小项目,也能把任意无后端 API 的静态网页平滑托管到云端。
这是一个什么样的项目
在 solution README 的开篇中,作者把它定义为一次 small drag and drop code-meditation(小型的拖拽式代码冥想):只用一小段 HTML、JS 和 CSS,你就能构建一个网页界面、为它设置样式,并加上交互能力。
这句话点明了它作为教学项目的定位——它不追求功能复杂,而是把 Web 开发的三根支柱浓缩到一个 14 株植物、一个玻璃罐的应用里:
- HTML 负责搭建页面骨架(左侧、右侧的植物选择栏 + 中间的玻璃罐);
- CSS 负责视觉呈现(页面布局、用纯样式"画"出玻璃罐);
- JavaScript 负责交互核心(把植物从两侧拖进罐中,本质是对 DOM 元素位置属性的持续操作)。
从截图可以看到,最终成品以一个"罐内微景观"为核心:页面两侧是 14 株可拖拽的植物,中央的半透明罐体内部可以自由摆放。整个应用没有任何后端参与,属于典型的前端静态站点。
solution 目录的源码构成
工程就存放在 3-terrarium/solution 下,文件规模非常克制,全部是可以直接在浏览器打开的静态资源:
| 文件 / 目录 | 作用 |
|---|---|
index.html |
页面结构:导航、标题区、植物选择区与玻璃罐区 |
style.css |
全部样式:布局、色彩、以及用 CSS 绘制的罐体 |
script.js |
交互逻辑:14 株植物的拖拽实现 |
images/plant1.png ~ images/plant14.png |
14 张手绘植物素材 |
一个值得留意的细节是:这份工程无构建、无依赖安装,不需要 npm、打包器或本地服务器。index.html 通过普通 <link> 与 <script src="..."> 直接引用同级 CSS/JS,如图 index.html 所示:
<!-- import the webpage's stylesheet -->
<link rel="stylesheet" href="./style.css" />
<!-- import the webpage's JavaScript file -->
<script src="./script.js" defer></script>
这也正是它能用"静态站点"方式一键部署的根本原因。
HTML:两侧素材库 + 中间罐体的三段式结构
打开 index.html 可以看到清晰的语义结构,主体 main 由两个区域构成:
#plant-selection植物选择区,内含#left-container与#right-container两个容器,每个容器里是 7 个.plant-holder,每个 holder 放一张带唯一id的植物图片:
<div id="left-container" class="container">
<!-- Left-side plants -->
<div class="plant-holder">
<img class="plant" alt="plant" id="plant1" src="./images/plant1.png" />
</div>
<!-- ... plant2 ~ plant7 ... -->
</div>
<div id="right-container" class="container">
<!-- ... plant8 ~ plant14 ... -->
</div>
#terrarium-section生态瓶区,内部#terrarium的 DOM 结构值得玩味——罐体的"口"、"壁"、"土"与"底"全部是独立元素(.jar-top、.jar-walls、.jar-glossy-long、.jar-glossy-short、.dirt、.jar-bottom),也就是说视觉上的玻璃罐完全由 CSS 拼接绘制,而非一张罐子图片,对应的源码位于 index.html。
此外,成品页还附带由 Font Awesome 图标点缀的导航栏与页脚(nav/header/footer),说明这段示例同时示范了普通页面的完整骨架写法。
CSS:绝对定位布局与"纯样式"玻璃罐
布局逻辑集中在 style.css 中。左右两个 .container 采用绝对定位固定在中线两侧,各占视口宽度 15%(见 style.css):
.container {
background-color: #eee;
width: 15%;
top: 0px;
position: absolute;
height: 100%;
padding: 10px;
}
因为 .container 是定位参照系,.plant-holder 与 .plant 再使用 position: relative/absolute 形成层叠关系,配合 z-index 控制拖拽时植物浮在罐体之上(z-index: 2),罐身则 z-index: 1、土壤层 z-index: -1,保证"植物在罐里、土在罐底"的视觉顺序。
罐体的绘制是这份 CSS 最有趣的部分:罐壁 .jar-walls 用半透明青色 #d1e1df 加 border-radius 圆角生成;罐口 .jar-top、罐底 .jar-bottom、土壤 .dirt 都是独立的圆角矩形;两条高光 .jar-glossy-long / .jar-glossy-short 用极窄的高亮色块模拟玻璃反光。风格注释里明确写着这段样式参考自 Jakub Mandra 的玻璃罐 codepen 作品(style.css 处保留有来源 URL)。由此你可以直观理解:CSS 不仅是"上色",也能胜任纯代码绘图。
JavaScript:Pointer Events + 闭包实现的拖拽交互
交互的全部奥妙都在 script.js 中。文件顶部的注释说明了实现思路来自 w3schools 的 Draggable HTML Element 方案,随后对 14 株植物逐一调用 dragElement(...):
dragElement(document.getElementById('plant1'));
// ... 一直到 plant14
dragElement 是一个典型的闭包教学样板。其核心逻辑如下(script.js):
function dragElement(terrariumElement) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; // 记录光标/元素的坐标差
terrariumElement.onpointerdown = pointerDrag;
function pointerDrag(e) {
e.preventDefault();
pos3 = e.clientX; // 记住按下时的鼠标位置
pos4 = e.clientY;
document.onpointermove = elementDrag; // 移动时开始拖动
document.onpointerup = stopElementDrag; // 松开时结束
}
function elementDrag(e) {
pos1 = pos3 - e.clientX; // 计算 X 方向位移
pos2 = pos4 - e.clientY; // 计算 Y 方向位移
pos3 = e.clientX; // 更新"上一次位置"
pos4 = e.clientY;
// 用位移更新元素自身的坐标
terrariumElement.style.top = terrariumElement.offsetTop - pos2 + 'px';
terrariumElement.style.left = terrariumElement.offsetLeft - pos1 + 'px';
}
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
}
值得展开的几点原理:
- 闭包(closure):
pos1~pos4定义在dragElement的外层作用域,却被内层的pointerDrag/elementDrag/stopElementDrag三个函数共享修改。即代码注释所引用的定义——闭包是函数与其词法环境的捆绑,让你可以从内层函数访问外层函数的作用域。对每株植物调用一次dragElement,就为它生成一组独立私有的坐标状态,这正是用来跟踪"当前正在拖拽哪个元素"的机制。 - Pointer Events 统一鼠标与触摸:事件体系采用
onpointerdown/onpointermove/onpointerup,相比传统的 mouse 事件天然覆盖触屏设备。按下时把移动/抬起监听挂到document上,保证光标移出元素后拖拽仍不中断;松开后及时置空监听,避免"幽灵拖拽"。 - 读写 DOM 的几何属性:拖拽过程中反复读取
clientX/clientY(光标坐标)与offsetTop/offsetLeft(元素当前位移),再写回style.top/style.left完成逐帧移动——整个过程就是一次对 DOM 样式属性的高频读写演示。
无后端 API 的工程,为什么天然适合静态托管
README 特别强调了一句:"这个应用里没有 API,所以不用担心添加它"。这对应着它的技术形态——页面资源由浏览器直接下载渲染,所有"数据"(每株植物的位置)只存在于当前页面内存中,不依赖服务器接口、数据库或鉴权。
Azure Static Web Apps 正是为这类工程设计的托管服务:它把静态资源(HTML/CSS/JS/图片)分发到全球边缘,同时自动集成 GitHub 的持续构建。理解了这点,你也就理解了为什么部署流程可以简化到只剩"填一个根目录路径"。
部署到 Azure Static Web Apps 的三步流程
以下步骤完整继承自 solution README(英文原版)及 3-terrarium/README.md,其中关于向导配置项(App root、无 API、自动生成 .github)的说明均以文档原文为准:
第 1 步:Fork 本仓库
部署的前提是你拥有可推送到 GitHub 的代码副本,因此在 GitHub 上把该仓库 Fork 到自己的账号。
第 2 步:点击 "Deploy to Azure" 按钮
README 中内置了一枚 "Deploy to Azure" 徽章按钮,点击后会打开 Azure 门户中创建 Microsoft.StaticApp 资源的引导页。该按钮的价值在于把"登录 Azure → 新建 Static Web App → 关联 GitHub 仓库"的串联操作压缩成一次跳转。
第 3 步:跟随向导创建应用
进入创建向导后,需要留意的关键配置有两点:
- 把 App root(应用根目录)设置为
/solution,或设置为你代码库的根目录。因为本示例的index.html位于仓库的3-terrarium/solution/下,只有把根目录指到那里,构建服务才能找到入口页面;若整个代码库本身就是网页根,则填仓库根目录即可。 - 跳过 API 配置:本应用不含任何 API 与后端逻辑,向导中的 API 相关选项可以直接忽略。
向导结束后会发生两件自动化的事:
- 在你的 Fork 仓库中生成一个
.github工作流文件夹,其中写入 Azure Static Web Apps 所需的构建与部署配置; - 该构建服务自动构建应用并发布到一个新的公网 URL,之后每次向该分支推送代码都会触发重新部署。
换句话说,从 Fork 到拿到线上地址,全程不需要本地安装任何 CLI 工具。
这套示例在课程中的位置
如果你正在跟随本仓库系统学习,会更容易理解这份 solution 的用意:它对应 3-terrarium 模块前三个专题的教学成果,三者分别是 HTML 基础(搭建页面结构)、CSS 基础(绘制玻璃罐与布局)、DOM 与闭包(实现拖拽状态跟踪),前几课的作业正是让你从零一步步拼出这个成品。本 solution 即为上述练习的参考答案,也是部署演示的载体。
致谢与出处
该示例由 Jen Looper 编写:CSS 玻璃罐的风格源自 Jakub Mandra 在 CodePen 上公开的玻璃罐设计,作品插图则是作者使用 Procreate 手绘完成。本文所依据的自述文档(translations/ar/3-terrarium/solution/README.md)为原文的阿拉伯语机器翻译版本,涉及部署参数等关键信息时建议以仓库内英文原版 3-terrarium/solution/README.md 为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
