Gturpin
e7f22a8dd1
|
3 years ago | |
---|---|---|
.. | ||
README.es.md | 3 years ago | |
README.fr.md | 3 years ago | |
README.hi.md | 3 years ago | |
README.it.md | 3 years ago | |
README.ja.md | 3 years ago | |
README.ko.md | 3 years ago | |
README.ms.md | 3 years ago | |
README.zh-tw.md | 3 years ago | |
assignment.es.md | 3 years ago | |
assignment.fr.md | 3 years ago | |
assignment.hi.md | 3 years ago | |
assignment.it.md | 4 years ago | |
assignment.ja.md | 3 years ago | |
assignment.ko.md | 3 years ago | |
assignment.ms.md | 3 years ago | |
assignment.nl.md | 3 years ago | |
assignment.zh-cn.md | 3 years ago | |
assignment.zh-tw.md | 3 years ago |
README.zh-tw.md
盆栽盒專案 Part 3 - DOM 元素控制與閉包
由 Tomomi Imura 繪製
課前測驗
大綱
操作 DOM (Document Object Model) 是網頁開發的一項關鍵。根據 MDN 文件, 「Document Object Model (DOM) 元素能根據網頁文件的結構與內容來呈現物件」。藉由使用 JavaScript 框架而非原始的 JavaScript 程式碼來管理 DOM,在網頁上操作 DOM 的挑戰已經不比以前困難了,但這裡我們要自己來管理它們!
此外,這堂課也會介紹有關JavaScript 閉包(Closure)的概念,你可以想像成一個函式被包在另一個函式中,以訪問外面函式範圍中的變數。
JavaScript 閉包是個廣闊且複雜的主題。本堂課只觸及建立盆栽盒需要的最基礎概念。你能得知一個閉包為:內部函式和外部函式建立一項關係,允許內部函式存取外部函式的變數等作用域。要得知更多關於閉包的原理,請造訪觀看額外的文件。
我們會使用閉包來操控 DOM。
想像 DOM 就像一棵樹,表現出所有操作網頁的方式。多樣的 APIs (Application Program Interfaces) 提供程式開發者,依照自己使用的程式語言,以存取、編輯、編排等方式管理 DOM 元素。
HTML 語法會參考 DOM 的呈現方式。出自 Olfa Nasraoui。
在這堂課中,我們會完成我們的盆栽盒專案,建立 JavaScript 來對網頁中的植物進行互動式操作。
開始之前
確保盆栽盒的 HTML 與 CSS 已經編輯完成。這堂課會新增拖曳植物進出盆栽罐的功能。
課題
在專案資料夾中,新增檔案 script.js
。 匯入該檔案在 HTML 檔 <head>
的部分:
<script src="./script.js" defer></script>
筆記:匯入外部 JavaScript 檔案到 HTML 檔案須使用
defer
,讓 JavaScript 檔案只有在 HTML 被完全載入時才被執行。你也可以使用async
的屬性,允許 JavaScript 在解析 HTML 檔時就被執行。這項專案中,我們必須確保 HTML 的元件被完整建立後才允許使用拖曳功能。
DOM 元素
我們要做的第一件事是建立 DOM 下,要被操控的物件的連結。在專案例子中,我們有罐子外的十四株植物等著被拖曳。
課題
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)
現在,你已經準備好要建立 dragElement 閉包,建立包在外部函式內的內部函式組,在我們的例子中,會用上三個函式。
閉包在一或多個以上函式要存取外部函式時非常好用。看看下面的例子:
function displayCandy(){
let candy = ['jellybeans'];
function addCandy(candyType) {
candy.push(candyType)
}
addCandy('gumdrops');
}
displayCandy();
console.log(candy)
這項例子中,函式 displayCandy 包住另一個函式 addCandy,新增新的糖果樣式到已存在的矩陣當中。當執行這段程式時,矩陣 candy
會被認作是未定義,因為它是函式的本地變數。
✅ 你能讓矩陣 candy
被存取嗎?試著將它移到閉包外面。這時,矩陣會變成全域變數,取消閉包內的存取限制。
課題
在檔案 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
的變數給函式內的物件使用。它們是本地變數,給每一個進到拖曳函式內的物件操控。盆栽盒會被這些拖曳物件填充,我們的網頁應用必須要持續追蹤這些物件的位置。
此外,進到函式的 terrariumElement 也被新增了 pointerdown
事件,它是管理 DOM 的其中一項網頁 APIs。當按鈕按下時,或是在我們案例中,一個拖曳物件被點擊時,onpointerdown
事件就會被觸發。這個事件處理器(event handler)皆運作在網頁與行動瀏覽器上,只有少部分的例外。
✅ 事件處理器 onclick
支援更多的瀏覽器。為什麼我們不在這邊使用它? 想想看我們在這此建立的視窗互動類型。
函式 pointerDrag
terrariumElement 已經準備好被拖曳了。當觸發 onpointerdown
事件時,函式 pointerDrag 會參與其中。新增這項函式在程式碼 terrariumElement.onpointerdown = pointerDrag;
下方:
課題
function pointerDrag(e) {
e.preventDefault();
console.log(e);
pos3 = e.clientX;
pos4 = e.clientY;
}
許多事情會發生。首先,你使用 e.preventDefault();
取消掉 pointerdown 原先的預設事件。這樣你可以操作更多的介面行為。
回到你建立的程式碼中,試著刪除
e.preventDefault()
並執行看看,發生了什麼事?
第二,用瀏覽器打開 index.html
並調查我們的介面。當你點擊植物時,你可以發現 'e' 事件被觸發了。專研一下,一個 pointerdown 事件會產生多少資訊!
接下來,紀錄本地變數 pos3
和 pos4
被設定為 e.clientX 和 e.clientY。你可以在觀察面板中,會發現 e
的數值。這項數值取得按下植物瞬間的 x 與 y 座標資訊。為了全面的控制植物行為,在拖曳植物時,我們會持續更新座標資訊。
✅ 將整個網頁應用建立在一個大閉包下,會讓程式碼變得比較清楚嗎?如果沒有,你有其他方法管理這十四株可拖曳的植物嗎?
增加初始化函式,在程式碼 pos4 = e.clientY
下方加上下列兩行事件處理:
document.onpointermove = elementDrag;
document.onpointerup = stopElementDrag;
現在,在游標拖曳時,你的植物能跟著你的游標走,而在你取消點擊時停下來。onpointermove
和 onpointerup
也是 onpointerdown
類型相同的 API。然而,現在介面會出現錯誤訊息,因為我們還沒建立函式 elementDrag
與 stopElementDrag
。
函式 elementDrag 與 stopElementDrag
新增兩條內部函式在閉包中,它們會處理拖曳植物與停止拖曳的事件。你希望你可以拖曳任何一株植物且放在螢幕上的任一地方。介面並沒有強制你盆栽盒的配置格式,你可以自由地增加、移除與移動盆栽罐內的植物。
課題
新增函式 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';
}
在這條函式之前,你編輯了四個本地變數位置的初始值在外部函式中。這邊又做了什麼事?
當你拖曳物件時,你更新數值 pos1
為 pos3
減去現在的 e.clientX
,而 pos3
在之前被初始化為為 e.clientX
。同樣的行為套用在 pos2
上。之後,你更新 pos3
與 pos4
到新的 XY 座標點位置。你能在 console 下看到數值在拖曳下更新的情況。我們也更新植物的 CSS 造型中的定位點為 pos1
與 pos2
,比較植物左上方座標點與新座標點的關係。
offsetTop
和offsetLeft
是 CSS 的屬性,決定物件與它父關係物件的定位關係。父關係物件可以是任何元素,只要它的定位屬性不為static
。
這些座標點的計算式讓你成功校整了植物與盆栽盒之間的行為。
課題
最後的課題是在介面上新增 stopElementDrag
函式,我們將它加在函式閉包 elementDrag
的正下方:
function stopElementDrag() {
document.onpointerup = null;
document.onpointermove = null;
}
這條小函式重制 onpointerup
與 onpointermove
事件,這樣你可以重新開始該植物的拖曳事件,或是拖曳新的植物。
✅ 如果不將這些事件設為空值時,會發生什麼事?
我們終於完成了這項專案!
🚀 挑戰
新增新的事件處理器到你的閉包中,讓你能對植物做更多的事情。舉例來說,雙擊植物讓它排列到最上層。發揮你的創意吧!
課後測驗
複習與自學
在螢幕上拖曳物件看似簡單,但依照不同的目的與實現方法會遭遇到不同的問題。事實上,這邊有一份關於你可以嘗試的拖曳 API。我們沒在專案中使用是為了建立不一樣的實現方法,試著使用這些 API 到專案中,看看你能完成什麼。
在 W3C 文件 和 MDN 網頁文件上取得更多關於 pointer 的事件。
記得習慣性用 CanIUse.com 檢查網頁的瀏覽器兼容性。