ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Web-Dev-For-Beginners Terrarium 实战:用 DOM 操作与 JavaScript 闭包实现植物拖拽交互

Web-Dev-For-Beginners Terrarium 实战:用 DOM 操作与 JavaScript 闭包实现植物拖拽交互 Web-Dev-For-Beginners Terrarium 实战用 DOM 操作与 JavaScript 闭包实现植物拖拽交互【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners本文以 Web-Dev-For-Beginners 课程 Terrarium虚拟生态瓶项目的第三部分为核心完整讲解如何用原生 JavaScript 通过 DOM 操作让 14 株植物支持按住即拖、松手即放的交互。读完后你将掌握三件事如何用document.getElementById()建立脚本与 HTML 元素的引用、如何用闭包为每个可拖拽元素维护独立私有状态坐标变量、以及pointerdown/pointermove/pointerup三类指针事件如何协同完成一次完整的拖拽生命周期。DOM 与闭包交互页面的两根支柱DOM 是什么DOMDocument Object Model文档对象模型是组成网页文档结构与内容的那些对象的数据表示文档引用 MDN 的定义。你可以把 DOM 想象成一棵树它代表了网页文档可以被操作的所有方式浏览器加载页面时会在内存中构建出这棵对象树JavaScript 通过 API 访问、编辑、重排和管理它。上图是一张经典的 DOM 与 HTML 标记对照示意图引自课程原文。在原始资料中由 Olfa Nasraoui 提供。操纵 DOM 正是 JavaScript 框架大量出现的动因之一——历史上围绕 DOM 操作的诸多痛点推动开发者转向框架但本课刻意反其道而行我们用 vanilla JS 自己管理 DOM因为理解了底层机制之后再看任何框架的虚拟 DOM 优化都会更通透。闭包是什么闭包closure可以理解为一个被外层函数包裹的内层函数——内层函数因此能够访问外层函数的作用域。用更形象的说法闭包像是给函数配了一个私有记事本即使外层函数执行完毕内层函数仍记得那些局部变量。课程里给了一个糖果数组的小例子来演示作用域问题function displayCandy(){ let candy [jellybeans]; function addCandy(candyType) { candy.push(candyType) } addCandy(gumdrops); } displayCandy(); console.log(candy) // 报错candy 未定义candy是displayCandy内部的局部变量外部console.log拿不到它。如果希望外部可访问可以把数组移出闭包使其成为全局变量——但在本例Terrarium中我们恰恰希望变量保持私有这正是闭包的用武之地。为什么 Terrarium 必须用闭包因为页面里有14 株可拖拽的植物每株都需要独立跟踪自己的坐标状态。若把坐标变量放到全局作用域14 株植物会互相踩踏同一份数据而每次调用dragElement(某株植物)都会生成一份新的闭包实例4 个坐标变量pos1~pos4就变成该植物私有的、持久存在的状态。前置条件与脚本接入开始本课之前你需要已经完成了 Terrarium 的前两课index.html与style.css中搭好的页面骨架左右两侧的植物栏 中间的玻璃瓶。本课结束后你将能够把植物拖出侧栏、拖入瓶中任意位置。在 terrarium 目录下新建script.js并在index.html的head中引入script src./script.js defer/scriptdefer与async的区别值得注意文档原文强调defer脚本只在 HTML 完全加载并解析完成后才执行。本例中我们必须等所有植物元素都解析出来拖拽脚本才有东西可拖所以选defer。async脚本可以在 HTML 解析过程中随时执行适合与页面内容无时序依赖的独立脚本。仓库中的成品 solution/index.html 正是这样写的第 13 行script src./script.js defer/script第一步为 14 株植物建立 DOM 引用先创建对要操作元素的引用。在script.js顶部写下 14 行调用dragElement(document.getElementById(plant1)); dragElement(document.getElementById(plant2)); dragElement(document.getElementById(plant3)); dragElement(document.getElementById(plant4)); dragElement(document.getElementById(plant5)); dragElement(document.getElementById(plant6)); dragElement(document.getElementById(plant7)); dragElement(document.getElementById(plant8)); dragElement(document.getElementById(plant9)); dragElement(document.getElementById(plant10)); dragElement(document.getElementById(plant11)); dragElement(document.getElementById(plant12)); dragElement(document.getElementById(plant13)); dragElement(document.getElementById(plant14));这里做了什么访问document在 DOM 中按 ID 找到特定元素。还记得第一课 HTML 里给每株植物图片赋予的唯一ididplant1…吗此刻正是兑现那一刻劳动的回报。每找到一株植物就把它交给dragElement函数下面即将创建这株元素随即获得或即将获得可拖拽能力。对照仓库中的 solution/index.html 可以看到左右两个侧栏各放了 7 株植物每株都是形如这样的结构div classplant-holder img classplant altplant idplant1 src./images/plant1.png / /div为什么按 ID 而不是按 CSS class 引用这是课程留的思考题。答案在于语义分工class 设计用于为一组元素施加统一样式比如上面.plant类同时作用在 14 张图上而 ID 是页面内唯一标识符getElementById()只返回匹配到的第一个元素。JavaScript 要精确定位并单独操作某一个元素时ID 是唯一且可靠的句柄。第二步构建 dragElement 闭包现在创建核心闭包dragElement——一个外部函数内部封装三个函数。在script.js的元素声明下方添加function dragElement(terrariumElement) { // 设置 4 个屏幕定位变量 let pos1 0, pos2 0, pos3 0, pos4 0; terrariumElement.onpointerdown pointerDrag; }逐点拆解dragElement从脚本顶部的 14 行声明中接收一个terrariumElement对象某株植物的img节点。pos1~pos4初始化为 0它们是闭包私有变量之后每个指针事件都会读写它们。Terrarium 将被这些被拖动的元素住满所以应用必须持续跟踪它们的位置——4 个变量分别记录上一帧与当前帧的 X/Y 坐标。terrariumElement.onpointerdown pointerDrag给该元素挂上pointerdown事件处理器。指针事件Pointer Events是 Web API 的一部分专为跨设备输入设计onpointerdown在鼠标按键按下或手指触摸到可拖拽元素时触发且同时适用于桌面浏览器与移动端文档提示存在少数兼容例外。为什么不用浏览器支持更广的onclick这是课程的第二道思考题答案藏在拖拽这个动作本身click只描述按下→松开一个完整周期的结果而拖拽需要在按住不放的整个过程中持续跟踪指针移动。onpointerdown捕获的是按下这个起点后续配合onpointermove才能还原出连续轨迹onclick在语义上就无法覆盖这段过程。第三步pointerDrag —— 捕获拖拽起点当onpointerdown触发时调用pointerDrag。把它添加在terrariumElement.onpointerdown pointerDrag;这一行的下方即dragElement函数体内function pointerDrag(e) { e.preventDefault(); console.log(e); pos3 e.clientX; pos4 e.clientY; document.onpointermove elementDrag; document.onpointerup stopElementDrag; }这一小段里发生了四件事e.preventDefault()阻止浏览器对指针按下事件的默认行为例如在拖动图片时触发选中、在页面拖选文字。不阻止它浏览器自己的行为会干扰自定义拖拽。文档特意留了一个实验任务写完整脚本后把这行删掉再试一次亲眼看看会发生什么。console.log(e)调试手段。在浏览器中打开index.html并用检查器Elements/Console 面板观察点击一株植物时可以看到被捕获的事件对象e展开它能看到指针事件收集到的大量信息坐标、指针类型、目标元素等。pos3 e.clientX; pos4 e.clientY把闭包私有的pos3、pos4设为事件对象的clientX/clientY即用户按下那一刻指针相对视口的 X/Y 坐标。要精确控制植物在点击、拖动时的表现就必须持续跟踪这些坐标。注册两个 document 级监听document.onpointermove elementDrag; document.onpointerup stopElementDrag;这意味着植物要跟着指针在整个文档范围内移动松开手取消选中植物时拖拽手势终止。onpointermove和onpointerup与onpointerdown属于同一套指针事件 API。把 move/up 监听挂在document而不是植物元素上是为了即使指针快速移出该img的边界拖拽跟踪也不中断。此时若立即运行页面由于elementDrag与stopElementDrag尚未定义控制台的赋值语句不会立即报错属性赋值只是存了个undefined或延迟求值但拖拽行为尚不完整——补上最后两个函数。第四步elementDrag —— 拖拽中的坐标运算这是闭包中数学最重的一环。在pointerDrag的结束大括号之后添加function elementDrag(e) { pos1 pos3 - e.clientX; pos2 pos4 - e.clientY; pos3 e.clientX; pos4 e.clientY; console.log(pos1, pos2, pos3, pos4); terrariumElement.style.top terrariumElement.offsetTop - pos2 px; terrariumElement.style.left terrariumElement.offsetLeft - pos1 px; }它大量读写外层函数中初始化的 4 个私有坐标变量运算逻辑如下pos1 pos3 - e.clientX水平方向位移 上一次记录的 Xpos3− 当前指针 X。pos2同理处理垂直方向。pos3 e.clientX; pos4 e.clientY把pos3/pos4重置为当前指针坐标成为下一次pointermove计算的基线。拖动过程中在控制台能看到这四个值的滚动变化。改写 CSS 定位terrariumElement.style.top terrariumElement.offsetTop - pos2 pxleft同理。元素的新top/left由它当前的offsetTop/offsetLeft减去本次位移得到字符串拼接px后写入行内样式。offsetTop与offsetLeft是读元素相对其定位父元素当前位置的属性只有父级存在非static的定位时该关系才成立。这一点直接呼应了仓库中 solution/style.css 的布局设定拖拽能成立依赖两处 CSS 前提.plant-holder { position: relative; /* 为每株植物提供局部定位上下文 */ height: 13%; left: -10px; } .plant { position: absolute; /* 图片脱离文档流top/left 才生效 */ max-width: 150%; max-height: 150%; z-index: 2; }从源码结构看.plant被设为position: absolute正是style.top/style.left赋值能立刻改变图片位置的先决条件——若它是静态定位static改动top/left将被浏览器忽略整个拖拽体系随之失效。这套持续重算让植物可以落在屏幕的任意位置而界面对此不做任何限制没有投放区drop zone、没有吸附你想怎么摆就怎么摆。第五步stopElementDrag —— 清理拖拽状态最后一个函数添加在elementDrag的结束大括号之后function stopElementDrag() { document.onpointerup null; document.onpointermove null; }这个短小的函数把document上的onpointerup与onpointermove重置为null作用是解除监听松手后不再持续计算你可以重新开始拖同一株植物或改拖另一株。思考题如果不把它们置为 null 会怎样可以推断的后果包括拖动 A 植物结束后document上仍挂着 A 的elementDrag此时你只是普通地在页面上移动鼠标A 也会跟着移动因为 move 监听从未解绑同时各次拖拽注册的处理器会相互覆盖行为不可预测。参考实现与完整代码对照仓库自带的完整可运行版本位于 solution/script.js与上面逐段累加的最终形态一致其中还附有注释说明闭包的动机文件头部第 18–21 行的注释直接给出闭包定义闭包是函数与其词法环境的组合内层函数因此能访问外层作用域。完整的dragElement结构如下方便你一次性对照检查自己的代码function dragElement(terrariumElement) { // 设置 4 个屏幕定位变量 let pos1 0, pos2 0, pos3 0, pos4 0; terrariumElement.onpointerdown pointerDrag; function pointerDrag(e) { e.preventDefault(); console.log(e); // 用 pos3 和 pos4 记录初始鼠标位置 pos3 e.clientX; pos4 e.clientY; // 鼠标移动时开始拖拽 document.onpointermove elementDrag; // 鼠标抬起时停止拖拽 document.onpointerup stopElementDrag; } function elementDrag(e) { // pos1 鼠标 X 之前的位置 - 现在的位置 pos1 pos3 - e.clientX; // pos2 鼠标 Y 之前的位置 - 现在的位置 pos2 pos4 - e.clientY; // 将 pos3 重置为鼠标 X 当前位置 pos3 e.clientX; // 将 pos4 重置为鼠标 Y 当前位置 pos4 e.clientY; console.log(pos1, pos2, pos3, pos4); // 设置元素新位置 terrariumElement.style.top terrariumElement.offsetTop - pos2 px; terrariumElement.style.left terrariumElement.offsetLeft - pos1 px; } function stopElementDrag() { // 鼠标松开后停止计算 document.onpointerup null; document.onpointermove null; } }仓库注释同时注明该可拖拽元素的解法参考了 w3schools 的 Draggable HTML Element 方案并改用了指针事件。至此项目完成——你的 Terrarium 已经是一个真正活起来的页面14 株植物各自拥有独立闭包状态任意点击、拖动、放置均可流畅进行。挑战扩展你的拖拽系统课程结尾给出了进阶挑战任选其一动手双击置顶给闭包再挂一个事件处理器如ondblclick双击某株植物时把它带到最前面——用style.zIndex递增即可实现注意 solution/style.css 中.plant的初始z-index是 2边界约束在elementDrag中加入条件判断禁止植物被拖出 Terrarium 玻璃瓶区域位置持久化用localStorage在stopElementDrag中记录各植物最终坐标刷新后恢复布局鼓励自拟其他创意悬停高亮、拿起/放下音效等。自学延伸不止指针事件一条路拖拽在屏幕上看似 trivial实则方法众多、坑也不少取决于你想要的效果。文档复习与自学部分特别提醒浏览器还内置了完整的HTML Drag and Drop APIdraggable属性 dragstart/dragover/drop事件族本课刻意没有使用它因为那种 API 的交互效果幽灵镜像、投放区高亮与本课想要的元素原地跟手移动不同——建议在自己的项目里尝试对比两者。此外还可顺藤摸瓜学习事件委托event delegation用单一监听器处理多个元素的交互transform属性替代top/left做位移通常对渲染性能更友好虚拟 DOM 概念主流框架正是为管理 DOM 而出现的本课写的手动 diff式坐标更新正是它们在底层自动做的事。配套的课后练习见 assignment.es.md从 DOM 接口列表中认领一个接口在一个真实网站上找到它的使用实例写一段带示例的说明文字。其评分标准只有一个维度——是否提交了一段带示例的说明段落优秀档要求成段写作且含示例合格档为成段但无示例未提交则不合格。小结技术点在本课中的角色关键代码DOM 引用找到 14 株植物document.getElementById(plant1)闭包每株植物私有pos1~pos4function dragElement(terrariumElement) { let pos10, … }pointerdown拖拽起点、记录初始坐标terrariumElement.onpointerdown pointerDragpointermove持续跟手、坐标重算pos1 pos3 - e.clientX; …pointerup结束拖拽、解除监听document.onpointermove nullCSS 定位位移真正生效的前提.plant { position: absolute }defer保证 DOM 就绪后再执行脚本script src./script.js defer/script这套闭包 指针事件 行内样式写入的组合正是从文件拖放上传到看板卡片拖动等众多交互界面共用的底层原理——在 Terrarium 这个项目里你只用不到 60 行原生 JS 就把它们全部亲手实现了一遍。【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表