首页
/ Web-Dev-For-Beginners Terrarium 实战:用 HTML、纯 CSS 玻璃罐与 JavaScript 闭包实现可拖拽电子生态缸

Web-Dev-For-Beginners Terrarium 实战:用 HTML、纯 CSS 玻璃罐与 JavaScript 闭包实现可拖拽电子生态缸

2026-09-06 13:15:41作者:申梦珏Efrain

本篇基于 Web-Dev-For-Beginners 仓库中 Terrarium 项目的完整解答展开,讲解如何用少量 HTML、CSS 和 JavaScript 搭建一个可交互的"电子生态缸"(Terrarium):14 株植物可以被鼠标或手指自由拖拽摆放,玻璃罐完全由 CSS 绘制。读完后你将掌握 DOM 元素选取、指针事件生命周期、闭包状态管理这三项核心能力,并按仓库给出的步骤把项目发布到 Azure Static Web Apps。

Terrarium 成品效果:左右两侧的植物选择区与中央由 CSS 绘制的玻璃罐

项目定位:一堂拖放式"代码冥想"

solution/README.md 将本项目描述为 "A small drag and drop code-meditation"——用一点 HTML、JS 和 CSS 构建一个 Web 界面、为它做样式、并加上交互。它是 Terrarium 课程三个阶段(HTML 结构、CSS 样式、DOM 与闭包)的完整成品,对应源码目录为 3-terrarium/solution/

文件 职责
index.html 页面骨架:导航、标题、左右植物栏、玻璃罐、页脚
style.css 全部视觉呈现:玻璃罐、土壤、植物定位、响应式导航
script.js 拖拽系统:指针事件 + 闭包坐标追踪
images/ 14 张手绘植物素材(plant1.png ~ plant14.png)

整个方案不依赖任何框架,全部为原生 Web 技术,这正是它作为教学样本的价值所在。

页面骨架:index.html 的结构设计

index.html<head> 做了四件事:声明 viewport meta 保证移动端适配、引入 Font Awesome 6.4.2 图标库、链接 ./style.css、以及以 defer 属性加载 ./script.js

<link rel="stylesheet" href="./style.css" />
<script src="./script.js" defer></script>

defer 属性的意义在于:脚本会等待整个 HTML 解析完毕后才执行,从而保证 script.js 顶部那 14 行 dragElement(document.getElementById('plantN')) 调用运行时,所有植物元素已经存在于 DOM 中,不会出现"元素未就绪"的取值错误。

<body> 采用语义化布局,结构为:

<div id="page">
  <nav>…五个带图标的导航项…</nav>
  <header><h1>My Terrarium</h1></header>
  <main>
    <section id="plant-selection">
      <div id="left-container"  class="container">…plant1 ~ plant7…</div>
      <div id="right-container" class="container">…plant8 ~ plant14…</div>
    </section>
    <section id="terrarium-section">
      <div id="terrarium">
        <div class="jar-top"></div>
        <div class="jar-walls">
          <div class="jar-glossy-long"></div>
          <div class="jar-glossy-short"></div>
        </div>
        <div class="dirt"></div>
        <div class="jar-bottom"></div>
      </div>
    </section>
  </main>
  <footer></footer>
</div>

两个关键设计点:

  • 每株植物都有唯一 IDplant1plant14),并被包在 .plant-holder 容器内。ID 保证 document.getElementById() 能精确定位单个元素——这与 DOM 课程中"用 ID 而非 class 操作单个元素"的建议一致。
  • 玻璃罐由空的 <div> 组成jar-topjar-wallsdirtjar-bottom 以及罐壁上的两条高光 jar-glossy-long / jar-glossy-short,全部依赖 CSS 定位渲染,没有任何图片。

纯 CSS 玻璃罐:style.css 的视觉实现

style.css 中的玻璃罐部分受 Jakub Mandra 的玻璃罐作品启发(README 致谢中注明)。核心思路是:所有部件都用 position: absolute 锚定在 #terrarium 内部,靠 bottom/left 百分比对齐,再用 opacity 制造玻璃的半透明感。

罐壁是罐体的主体,占宽 60%、高 80%,浅青色加圆角,透明度 0.5:

.jar-walls {
    height: 80%;
    width: 60%;
    background: #d1e1df;
    border-radius: 1rem;
    position: absolute;
    bottom: 0.5%;
    left: 20%;
    opacity: 0.5;
    z-index: 1;
}

土壤层用深棕色 #3a241d,底部圆角,并且刻意设置 z-index: -1,让它被植物(z-index: 2)压在后面、又盖在罐壁之上:

.dirt {
    width: 60%;
    height: 5%;
    background: #3a241d;
    position: absolute;
    border-radius: 0 0 1rem 1rem;
    bottom: 1%;
    left: 20%;
    opacity: 0.7;
    z-index: -1;
}

罐口(.jar-top)、罐底(.jar-bottom)分别定位在罐壁上沿与最底部;两条 #ddfbff 的细长高光模拟玻璃反光。

可拖拽的前提藏在植物样式里——.plant 必须是绝对定位,JavaScript 修改 top/left 才有效果:

.plant {
    position: absolute;
    max-width: 150%;
    max-height: 150%;
    z-index: 2;
}

两侧植物栏 .containerleft: 0 / right: 0 绝对贴边、高 100%,.plant-holder 以相对定位给出每格 13% 的高度。样式表末尾还包含一段 @media (max-width: 700px) 响应式规则:窄屏下导航由横向 flex 转为纵向排列,图标链接的字号与内边距同步缩小。

拖拽系统:script.js 中的闭包与指针事件

script.js 是整个项目的交互核心,代码结构分两段:前 16 行为 14 株植物逐一绑定拖拽能力,随后定义 dragElement 函数。文件头部注释明确说明:拖拽方案参考了 w3schools 的经典实现,且刻意用闭包(closure)来追踪每个被拖元素的状态。

为什么需要闭包

每次调用 dragElement(terrariumElement) 都会在函数内部生成一组"私有"变量:

function dragElement(terrariumElement) {
    let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
    terrariumElement.onpointerdown = pointerDrag;

    function pointerDrag(e) { ... }
    function elementDrag(e) { ... }
    function stopElementDrag() { ... }
}

pos1~pos4 四个局部变量被内部的 pointerDragelementDragstopElementDrag 三个函数共同引用。由于每次调用 dragElement 都会创建一套全新的局部变量,plant1 和 plant2 的坐标状态互不干扰——这正是闭包"函数与其词法环境绑定"的典型应用,也是 DOM 课程中"每株植物拥有自己的记忆口袋"这一比喻的具体实现。

拖拽生命周期:三个内部函数

1. pointerDrag——按下起点(pointerdown)

function pointerDrag(e) {
    e.preventDefault();
    pos3 = e.clientX;   // 记录指针 X 起点
    pos4 = e.clientY;   // 记录指针 Y 起点
    document.onpointermove = elementDrag;
    document.onpointerup = stopElementDrag;
}
  • e.preventDefault() 阻止浏览器默认行为,否则拖拽过程中会触发文本选中、图片默认拖拽等干扰;
  • clientX/clientY 是相对视口的坐标,保证在不同屏幕尺寸与缩放下计算一致;
  • 注意 move 与 up 事件挂在整个 document 上而非植物元素本身:这样即使指针快速移出植物边界,拖拽依然连续不断。

2. elementDrag——移动计算(pointermove)

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';
}

计算逻辑是"增量式"的:每次 pointermove 只计算自上一次事件以来的位移,再叠加到 offsetTop/offsetLeft(元素相对定位父容器的当前位置)上,写入内联 style.top/style.left。由于 CSS 中 .plantposition: absolute,这两个属性立即生效,实现跟手移动。

3. stopElementDrag——释放清理(pointerup)

function stopElementDrag() {
    document.onpointerup   = null;
    document.onpointermove = null;
}

把 document 级监听器置空,是避免"松手后指针移动仍持续计算、多个元素监听器叠加"这类问题的手段,从源码结构看这也是课程反复强调的"事件清理"实践。

为什么选指针事件而非鼠标事件

script.js 使用 onpointerdown / onpointermove / onpointerup 的写法可以确认:方案面向鼠标、触屏、手写笔统一输入。对比 onmousedown(仅桌面鼠标)或 onclick(只有按下-抬起,无法表达持续位移),Pointer Events 是一套跨设备的现代写法,与课程中给出的事件选型建议(click 适合按钮、pointerdown 兼顾鼠标与触摸)相符。

部署上线:用 Azure Static Web Apps 发布

solution/README.md 给出了把成品发布到 Web 的完整流程,使用的服务是 Azure Static Web Apps,步骤如下:

  1. Fork 本仓库——在自己的 GitHub 账号下复制一份完整代码;
  2. 点击 README 中提供的 "Deploy to Azure" 徽章按钮,跳转至 Azure 门户的 Static Web App 创建入口;
  3. 按向导创建应用,其中两个关键配置:
    • App root(应用根目录)必须设置为 /solution,或者你自己代码库的根目录。因为本仓库是课程仓库,成品代码位于 3-terrarium/solution 子目录而非仓库顶层,若不做此设置,构建服务会找不到 index.html
    • 本应用没有 API,无需配置 API 相关项;
    • 向导会在你的 fork 仓库中自动生成一个 .github 文件夹,其中的工作流文件供 Azure Static Web Apps 的构建服务自动构建并发布到一个新的 URL。

从源码结构看,该部署方式成立的前提也很直观:整个方案是纯静态站点——只有 HTML、CSS、JS、图片,唯一的运行时外部依赖是 index.html 中通过 CDN 引入的 Font Awesome 图标样式表,没有任何后端请求,因此静态托管即可完整运行。

致谢与可继续扩展的方向

solution/README.md 的 Credits 部分:项目由 Jen Looper 编写,纯 CSS 玻璃罐的设计灵感来自 Jakub Mandra 的玻璃罐作品,植物素材为 Jen Looper 使用 Procreate 手绘。

若想在成品基础上继续练习,DOM 课程的作业列出了一组与现有代码自然衔接的扩展点:双击植物提升 z-index 使其置顶、为拖拽添加边界约束防止植物移出画面、用 localStorage 保存植物位置实现"记住我的布置"、以及给植物添加拾取/放置时的音效。这些扩展都只需要修改 script.js 的事件函数与 style.css 的样式,不涉及框架或构建工具,适合作为巩固 DOM 操作与闭包概念的练手项目。

登录后查看全文
热门项目推荐
相关项目推荐