26 KiB
โครงการ Terrarium ตอนที่ 3: การจัดการ DOM และ Closure
ภาพสเก็ตโน้ตโดย Tomomi Imura
แบบทดสอบก่อนเรียน
บทนำ
การจัดการ DOM หรือ "Document Object Model" เป็นส่วนสำคัญของการพัฒนาเว็บ ตามที่ MDN กล่าวไว้ว่า "Document Object Model (DOM) คือการแสดงข้อมูลของวัตถุที่ประกอบขึ้นเป็นโครงสร้างและเนื้อหาของเอกสารบนเว็บ" ความท้าทายในการจัดการ DOM บนเว็บมักเป็นแรงผลักดันให้ใช้เฟรมเวิร์ก JavaScript แทนการใช้ JavaScript แบบดั้งเดิมเพื่อจัดการ DOM แต่ในบทเรียนนี้เราจะจัดการด้วยตัวเอง!
นอกจากนี้ บทเรียนนี้จะนำเสนอแนวคิดของ JavaScript closure ซึ่งคุณสามารถคิดว่าเป็นฟังก์ชันที่ถูกล้อมรอบด้วยฟังก์ชันอื่น เพื่อให้ฟังก์ชันภายในสามารถเข้าถึงขอบเขตของฟังก์ชันภายนอกได้
JavaScript closures เป็นหัวข้อที่กว้างและซับซ้อน บทเรียนนี้จะกล่าวถึงแนวคิดพื้นฐานที่สุดที่ในโค้ดของ terrarium คุณจะพบ closure: ฟังก์ชันภายในและฟังก์ชันภายนอกที่ถูกสร้างขึ้นในลักษณะที่อนุญาตให้ฟังก์ชันภายในเข้าถึงขอบเขตของฟังก์ชันภายนอก สำหรับข้อมูลเพิ่มเติมเกี่ยวกับวิธีการทำงานนี้ โปรดเยี่ยมชม เอกสารที่ครอบคลุม
เราจะใช้ closure เพื่อจัดการ DOM
ลองนึกถึง DOM ว่าเป็นต้นไม้ที่แสดงถึงวิธีการทั้งหมดที่เอกสารหน้าเว็บสามารถถูกจัดการได้ มี API (Application Program Interfaces) ต่าง ๆ ที่ถูกเขียนขึ้นเพื่อให้โปรแกรมเมอร์สามารถเข้าถึง DOM และแก้ไข เปลี่ยนแปลง จัดเรียงใหม่ และจัดการได้ตามต้องการ
การแสดง DOM และ HTML markup ที่อ้างถึงมัน จาก Olfa Nasraoui
ในบทเรียนนี้ เราจะทำโครงการ terrarium แบบโต้ตอบให้เสร็จสมบูรณ์โดยสร้าง JavaScript ที่จะช่วยให้ผู้ใช้สามารถจัดการพืชบนหน้าเว็บได้
สิ่งที่ต้องเตรียม
คุณควรมี HTML และ CSS สำหรับ terrarium ของคุณพร้อมแล้ว เมื่อจบบทเรียนนี้ คุณจะสามารถย้ายพืชเข้าและออกจาก terrarium โดยการลากและวางได้
งานที่ต้องทำ
ในโฟลเดอร์ terrarium ของคุณ สร้างไฟล์ใหม่ชื่อ script.js
และนำเข้าไฟล์นั้นในส่วน <head>
:
<script src="./script.js" defer></script>
หมายเหตุ: ใช้
defer
เมื่อนำเข้าไฟล์ JavaScript ภายนอกในไฟล์ html เพื่อให้ JavaScript ทำงานหลังจากไฟล์ HTML ถูกโหลดเสร็จสมบูรณ์ คุณสามารถใช้ attributeasync
ได้เช่นกัน ซึ่งอนุญาตให้สคริปต์ทำงานขณะ HTML กำลังถูกแปลง แต่ในกรณีของเรา สิ่งสำคัญคือการมีองค์ประกอบ HTML พร้อมใช้งานสำหรับการลากก่อนที่เราจะอนุญาตให้สคริปต์ลากทำงาน
องค์ประกอบ DOM
สิ่งแรกที่คุณต้องทำคือสร้างการอ้างอิงไปยังองค์ประกอบที่คุณต้องการจัดการใน DOM ในกรณีของเรา คือพืช 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'));
เกิดอะไรขึ้นที่นี่? คุณกำลังอ้างอิงเอกสารและค้นหาใน DOM เพื่อหาองค์ประกอบที่มี Id เฉพาะ จำได้ไหมว่าในบทเรียนแรกเกี่ยวกับ HTML คุณได้ให้ Id เฉพาะแก่แต่ละภาพพืช (id="plant1"
)? ตอนนี้คุณจะใช้ประโยชน์จากความพยายามนั้น หลังจากระบุองค์ประกอบแต่ละรายการ คุณส่งรายการนั้นไปยังฟังก์ชันที่เรียกว่า dragElement
ซึ่งคุณจะสร้างในอีกสักครู่ ดังนั้นองค์ประกอบใน HTML จะสามารถลากได้ หรือจะสามารถลากได้ในไม่ช้า
✅ ทำไมเราถึงอ้างอิงองค์ประกอบโดย Id? ทำไมไม่ใช้ CSS class? คุณอาจอ้างอิงบทเรียนก่อนหน้านี้เกี่ยวกับ CSS เพื่อหาคำตอบสำหรับคำถามนี้
Closure
ตอนนี้คุณพร้อมที่จะสร้าง closure dragElement
ซึ่งเป็นฟังก์ชันภายนอกที่ล้อมรอบฟังก์ชันภายในหนึ่งหรือหลายฟังก์ชัน (ในกรณีของเรา เราจะมีสามฟังก์ชัน)
Closure มีประโยชน์เมื่อฟังก์ชันหนึ่งหรือหลายฟังก์ชันต้องการเข้าถึงขอบเขตของฟังก์ชันภายนอก นี่คือตัวอย่าง:
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
ในตัวอย่างนี้ ฟังก์ชัน displayCandy
ล้อมรอบฟังก์ชันที่เพิ่มประเภทขนมใหม่ลงในอาร์เรย์ที่มีอยู่ในฟังก์ชัน หากคุณเรียกใช้โค้ดนี้ อาร์เรย์ candy
จะไม่ถูกกำหนดค่า เนื่องจากมันเป็นตัวแปรท้องถิ่น (ท้องถิ่นต่อ closure)
✅ คุณจะทำให้อาร์เรย์ candy
เข้าถึงได้อย่างไร? ลองย้ายมันออกไปนอก closure ด้วยวิธีนี้ อาร์เรย์จะกลายเป็น global แทนที่จะยังคงอยู่ในขอบเขตท้องถิ่นของ closure
งานที่ต้องทำ
ภายใต้การประกาศองค์ประกอบใน script.js
สร้างฟังก์ชัน:
function dragElement(terrariumElement) {
//set 4 positions for positioning on the screen
let pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
terrariumElement.onpointerdown = pointerDrag;
}
dragElement
ได้รับวัตถุ terrariumElement
จากการประกาศที่ด้านบนของสคริปต์ จากนั้นคุณตั้งค่าตำแหน่งท้องถิ่นที่ 0
สำหรับวัตถุที่ส่งผ่านไปยังฟังก์ชัน นี่คือตัวแปรท้องถิ่นที่จะถูกจัดการสำหรับแต่ละองค์ประกอบเมื่อคุณเพิ่มฟังก์ชันลากและวางภายใน closure ให้กับแต่ละองค์ประกอบ Terrarium จะถูกเติมเต็มด้วยองค์ประกอบที่ถูกลากเหล่านี้ ดังนั้นแอปพลิเคชันจำเป็นต้องติดตามตำแหน่งที่วางไว้
นอกจากนี้ terrariumElement
ที่ถูกส่งไปยังฟังก์ชันนี้จะถูกกำหนดเหตุการณ์ pointerdown
ซึ่งเป็นส่วนหนึ่งของ web APIs ที่ออกแบบมาเพื่อช่วยในการจัดการ DOM onpointerdown
จะทำงานเมื่อปุ่มถูกกด หรือในกรณีของเรา เมื่อองค์ประกอบที่สามารถลากได้ถูกสัมผัส เหตุการณ์นี้ทำงานได้ทั้ง เว็บและเบราว์เซอร์มือถือ โดยมีข้อยกเว้นบางประการ
✅ เหตุการณ์ onclick
มีการสนับสนุนข้ามเบราว์เซอร์มากกว่า; ทำไมคุณถึงไม่ใช้มันที่นี่? ลองคิดถึงประเภทของการโต้ตอบหน้าจอที่คุณพยายามสร้างที่นี่
ฟังก์ชัน Pointerdrag
terrariumElement
พร้อมที่จะถูกลากไปรอบ ๆ เมื่อเหตุการณ์ onpointerdown
ถูกเรียกใช้ ฟังก์ชัน pointerDrag
จะถูกเรียกใช้ เพิ่มฟังก์ชันนั้นใต้บรรทัดนี้: terrariumElement.onpointerdown = pointerDrag;
:
งานที่ต้องทำ
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
มีหลายสิ่งเกิดขึ้นที่นี่ อย่างแรก คุณป้องกันเหตุการณ์เริ่มต้นที่ปกติจะเกิดขึ้นเมื่อ pointerdown โดยใช้ e.preventDefault();
ด้วยวิธีนี้คุณจะมีการควบคุมพฤติกรรมของอินเทอร์เฟซมากขึ้น
กลับมาที่บรรทัดนี้เมื่อคุณสร้างไฟล์สคริปต์เสร็จสมบูรณ์และลองใช้โดยไม่มี
e.preventDefault()
- จะเกิดอะไรขึ้น?
ถัดไป เปิด index.html
ในหน้าต่างเบราว์เซอร์ และตรวจสอบอินเทอร์เฟซ เมื่อคุณคลิกที่พืช คุณจะเห็นว่าเหตุการณ์ 'e' ถูกจับ ลองเจาะลึกเข้าไปในเหตุการณ์เพื่อดูว่ามีข้อมูลมากแค่ไหนที่ถูกรวบรวมโดยเหตุการณ์ pointer down เพียงครั้งเดียว!
จากนั้น สังเกตว่าตัวแปรท้องถิ่น pos3
และ pos4
ถูกตั้งค่าเป็น e.clientX คุณสามารถค้นหาค่าของ e
ในหน้าต่างตรวจสอบ ค่าพวกนี้จับพิกัด x และ y ของพืชในขณะที่คุณคลิกหรือสัมผัสมัน คุณจะต้องควบคุมพฤติกรรมของพืชอย่างละเอียดเมื่อคุณคลิกและลากมัน ดังนั้นคุณจึงติดตามพิกัดของมัน
✅ มันชัดเจนขึ้นหรือยังว่าทำไมแอปนี้ถึงถูกสร้างด้วย closure ขนาดใหญ่? ถ้าไม่ใช่ closure คุณจะรักษาขอบเขตสำหรับพืชที่สามารถลากได้ทั้ง 14 ชนิดได้อย่างไร?
ทำฟังก์ชันเริ่มต้นให้เสร็จโดยเพิ่มการจัดการเหตุการณ์ pointer อีกสองรายการใต้ pos4 = e.clientY
:
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
ตอนนี้คุณกำลังระบุว่าคุณต้องการให้พืชถูกลากไปพร้อมกับ pointer ขณะที่คุณเคลื่อนมัน และให้การลากหยุดเมื่อคุณยกเลิกการเลือกพืช onpointermove
และ onpointerup
เป็นส่วนหนึ่งของ API เดียวกันกับ onpointerdown
อินเทอร์เฟซจะโยนข้อผิดพลาดในตอนนี้เนื่องจากคุณยังไม่ได้กำหนดฟังก์ชัน elementDrag
และ stopElementDrag
ดังนั้นสร้างฟังก์ชันเหล่านั้นต่อไป
ฟังก์ชัน elementDrag และ stopElementDrag
คุณจะทำ closure ให้เสร็จสมบูรณ์โดยเพิ่มฟังก์ชันภายในอีกสองฟังก์ชันที่จะจัดการสิ่งที่เกิดขึ้นเมื่อคุณลากพืชและหยุดลากมัน พฤติกรรมที่คุณต้องการคือคุณสามารถลากพืชใด ๆ ได้ทุกเมื่อและวางมันไว้ที่ใดก็ได้บนหน้าจอ อินเทอร์เฟซนี้ค่อนข้างไม่จำกัด (ไม่มี drop zone ตัวอย่างเช่น) เพื่อให้คุณสามารถออกแบบ terrarium ของคุณได้ตามที่คุณต้องการโดยการเพิ่ม ลบ และจัดตำแหน่งพืชใหม่
งานที่ต้องทำ
เพิ่มฟังก์ชัน 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';
}
ในฟังก์ชันนี้ คุณทำการแก้ไขตำแหน่งเริ่มต้น 1-4 ที่คุณตั้งค่าเป็นตัวแปรท้องถิ่นในฟังก์ชันภายนอก เกิดอะไรขึ้นที่นี่?
ขณะที่คุณลาก คุณกำหนดค่าใหม่ให้กับ pos1
โดยทำให้มันเท่ากับ pos3
(ซึ่งคุณตั้งค่าไว้ก่อนหน้านี้เป็น e.clientX
) ลบด้วยค่าปัจจุบันของ e.clientX
คุณทำการดำเนินการที่คล้ายกันกับ pos2
จากนั้นคุณตั้งค่าใหม่ให้กับ pos3
และ pos4
เป็นพิกัด X และ Y ใหม่ขององค์ประกอบ คุณสามารถดูการเปลี่ยนแปลงเหล่านี้ในคอนโซลขณะที่คุณลาก จากนั้นคุณจัดการสไตล์ css ของพืชเพื่อกำหนดตำแหน่งใหม่ของมันตามตำแหน่งใหม่ของ pos1
และ pos2
โดยคำนวณพิกัด X และ Y ด้านบนและด้านซ้ายของพืชโดยเปรียบเทียบ offset ของมันกับตำแหน่งใหม่เหล่านี้
offsetTop
และoffsetLeft
เป็นคุณสมบัติ CSS ที่กำหนดตำแหน่งขององค์ประกอบตามตำแหน่งของ parent; parent สามารถเป็นองค์ประกอบใดก็ได้ที่ไม่ได้ถูกกำหนดตำแหน่งเป็นstatic
การคำนวณตำแหน่งใหม่ทั้งหมดนี้ช่วยให้คุณปรับแต่งพฤติกรรมของ terrarium และพืชของมันได้อย่างละเอียด
งานที่ต้องทำ
งานสุดท้ายเพื่อทำให้อินเทอร์เฟซสมบูรณ์คือการเพิ่มฟังก์ชัน stopElementDrag
หลังจากปิดวงเล็บปีกกาของ elementDrag
:
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
ฟังก์ชันเล็ก ๆ นี้รีเซ็ตเหตุการณ์ onpointerup
และ onpointermove
เพื่อให้คุณสามารถเริ่มต้นความคืบหน้าของพืชใหม่ได้โดยเริ่มลากมันอีกครั้ง หรือเริ่มลากพืชใหม่
✅ จะเกิดอะไรขึ้นถ้าคุณไม่ตั้งค่าเหตุการณ์เหล่านี้เป็น null?
ตอนนี้คุณได้ทำโครงการของคุณเสร็จสมบูรณ์แล้ว!
🥇ขอแสดงความยินดี! คุณได้สร้าง terrarium ที่สวยงามเสร็จสมบูรณ์แล้ว!
🚀ความท้าทาย
เพิ่ม event handler ใหม่ใน closure ของคุณเพื่อทำสิ่งเพิ่มเติมกับพืช; ตัวอย่างเช่น ดับเบิลคลิกที่พืชเพื่อทำให้มันอยู่ด้านหน้า ลองสร้างสรรค์ดู!
แบบทดสอบหลังเรียน
ทบทวนและศึกษาด้วยตนเอง
แม้ว่าการลากองค์ประกอบไปรอบ ๆ หน้าจอจะดูเหมือนเป็นเรื่องเล็กน้อย แต่มีหลายวิธีในการทำสิ่งนี้และมีข้อผิดพลาดมากมาย ขึ้นอยู่กับผลลัพธ์ที่คุณต้องการ ในความเป็นจริง มี drag and drop API ทั้งหมดที่คุณสามารถลองใช้ได้ เราไม่ได้ใช้มันในโมดูลนี้เพราะผลลัพธ์ที่เราต้องการแตกต่างออกไป แต่ลองใช้ API นี้ในโครงการของคุณเองและดูว่าคุณสามารถทำอะไรได้บ้าง
ค้นหาข้อมูลเพิ่มเติมเกี่ยวกับ pointer events ได้ที่ เอกสาร W3C และ เอกสาร MDN
ตรวจสอบความสามารถของเบราว์เซอร์เสมอโดยใช้ CanIUse.com
งานที่ได้รับมอบหมาย
ข้อจำกัดความรับผิดชอบ:
เอกสารนี้ได้รับการแปลโดยใช้บริการแปลภาษา AI Co-op Translator แม้ว่าเราจะพยายามให้การแปลมีความถูกต้อง แต่โปรดทราบว่าการแปลอัตโนมัติอาจมีข้อผิดพลาดหรือความไม่แม่นยำ เอกสารต้นฉบับในภาษาต้นทางควรถือเป็นแหล่งข้อมูลที่เชื่อถือได้ สำหรับข้อมูลที่สำคัญ ขอแนะนำให้ใช้บริการแปลภาษาจากผู้เชี่ยวชาญ เราไม่รับผิดชอบต่อความเข้าใจผิดหรือการตีความที่ผิดพลาดซึ่งเกิดจากการใช้การแปลนี้