你是否曾驚嘆於React的魔力?是否曾好奇Dojo是如何運作的?是否曾對jQuery的巧妙操作感到好奇?在本教程中,我們將潛入幕後,嘗試構建一個超簡化的jQuery版本。
我們幾乎每天都在使用JavaScript庫。無論是實現算法、提供API抽像還是操作DOM,庫在大多數現代網站中都執行許多功能。
在本教程中,我們將嘗試從頭開始構建一個這樣的庫(當然,這是一個簡化的版本)。我們將創建一個用於DOM操作的庫,類似於jQuery。是的,這很有趣,但在你興奮之前,讓我澄清幾點:
prepend
方法只在你傳遞給它們我們的庫實例時才有效;它們不適用於原始DOM節點或節點列表。 我們將從模塊本身開始。我們將使用ECMAScript模塊(ESM),這是一種在Web上導入和導出代碼的現代方法。
export class Dome { constructor(selector) { } }
如你所見,我們導出一個名為Dome
的類,其構造函數將接受一個參數,但它可以是多種類型。如果它是一個字符串,我們將假設它是一個CSS選擇器,但我們也可以接受單個DOM節點或document.querySelectorAll
的結果來簡化元素查找。如果它具有length
屬性,我們將知道我們擁有一個節點列表。我們將把這些元素存儲在this.elements
中,Dome
對象可以包裝多個DOM元素,我們幾乎需要在每種方法中循環遍歷每個元素,因此這些實用程序將非常方便。
讓我們從一個map
函數開始,它接受一個參數,一個回調函數。我們將循環遍歷數組中的項目,收集回調函數返回的內容,Dome
實例將接收兩個參數:當前元素和索引號。
我們還需要一個forEach
方法,默認情況下,我們可以簡單地將調用轉發到mapOne
。很容易看出這個函數的作用,但真正的問題是,為什麼我們需要它?這需要一點你可能稱之為“庫理念”的東西。
如果構建庫只是編寫代碼,那將不是一項太難的工作。但在從事這個項目時,我發現更難的部分是決定某些方法應該如何工作。
很快,我們將構建一個Dome
對象,它包裝了多個DOM節點($("li").text()
),你將得到一個包含所有元素文本連接在一起的單個字符串。這有用嗎?我認為沒有,但我不知道更好的返回值是什麼。
對於這個項目,我將把多個元素的文本作為數組返回,除非數組中只有一個項目;然後我們只返回文本字符串,而不是包含單個項目的數組。我認為你最常獲取單個元素的文本,所以我們對此情況進行了優化。但是,如果你正在獲取多個元素的文本,我們將返回你可以使用的內容。
因此,mapOne
將首先調用map
,然後返回數組或數組中的單個項目。如果你仍然不確定這如何有用,請繼續關注:你將看到!
mapOne(callback) { const m = this.map(callback); return m.length > 1 ? m : m[0]; };
接下來,讓我們添加text
方法來查看我們是在設置還是獲取。請注意,這只是遍曆元素並設置它們的文本。如果我們正在獲取,我們將返回元素的mapOne
方法:如果我們正在處理多個元素,這將返回一個數組;否則,它將只是一個字符串。
html
方法與text
方法幾乎相同,只是它將使用innerHTML
。
html(html) { if (typeof html !== "undefined") { this.forEach(function (el) { el.innerHTML = html; }); return this; } else { return this.mapOne(function (el) { return el.innerHTML; }); } }
就像我說的:幾乎相同。
接下來,我們要能夠添加和刪除類,所以讓我們編寫addClass
和removeClass
方法。
我們的addClass
方法將在每個元素上使用classList.add
方法。當傳遞字符串時,只添加該類,當傳遞數組時,我們將遍歷數組並添加其中包含的所有類。
addClass(classes) { return this.forEach(function (el) { if (typeof classes !== "string") { for (const elClass of classes) { el.classList.add(elClass); } } else { el.classList.add(classes); } }); }
很簡單,對吧?
現在,刪除類呢?為此,你幾乎要做同樣的事情,只是使用classList.remove
方法。
接下來,讓我們添加attr
函數。這將很容易,因為它與我們的html
方法幾乎相同。像這些方法一樣,我們將能夠同時獲取和設置屬性:我們將接受一個屬性名稱和值來設置,只接受一個屬性名稱來獲取。
attr(attr, val) { if (typeof val !== "undefined") { return this.forEach(function (el) { el.setAttribute(attr, val); }); } else { return this.mapOne(function (el) { return el.getAttribute(attr); }); } }
如果val
已定義,我們將使用setAttribute
方法。否則,我們將使用getAttribute
方法。
我們應該能夠創建新元素,任何好的庫都可以做到這一點。當然,這作為Dome
類的方法是沒有意義的。
export function create(tagName,attrs) { }
如你所見,我們將接受兩個參數:元素的名稱和屬性對象。大多數屬性將通過我們的attr
方法應用,文本內容將通過text
方法應用於Dome
對象。以下是所有這些的實際操作:
export function create(tagName, attrs) { let el = new Dome([document.createElement(tagName)]); if (attrs) { for (let key in attrs) { if (attrs.hasOwnProperty(key)) { el.attr(key, attrs[key]); } } } return el; }
如你所見,我們創建元素並將其直接發送到新的Dome
對像中。
但是現在我們正在創建新元素,我們將希望將它們插入到DOM中,對吧?
接下來,我們將編寫append
和prepend
方法。這些函數有點棘手,主要是因為有多種用例。以下是我們想要能夠做的事情:
dome1.append(dome2); dome1.prepend(dome2);
我們可能想要附加或前置:
我使用“新”來表示尚未在DOM中的元素;現有元素已在DOM中。讓我們現在逐步講解:
append(els) { }
我們期望els
是一個Dome
對象。一個完整的DOM庫會將其作為節點或節點列表接受,但我們不會這樣做。我們必須遍歷我們的每個元素,然後在其中,我們遍歷我們想要附加的每個元素。
如果我們正在附加,則來自作為參數傳入的外部Dome
對象的i
將只包含原始(未克隆的)節點。因此,如果我們只將單個元素附加到單個元素,則所有涉及的節點都將是它們各自的prepend
方法的一部分。
為了完整起見,讓我們添加一個remove
方法。這將非常簡單,因為我們只需要使用removeChild
方法。為了使事情更簡單,我們將使用forEach
循環反向遍歷,我將使用removeChild
方法反向遍歷循環,每個元素的Dome
對象仍然可以正常工作;我們可以使用任何我們想要的方法,包括將其附加或前置回DOM。不錯,對吧?
最後但並非最不重要的是,我們將編寫一些事件處理程序函數。
查看on
方法,然後我們將討論它:
on(evt, fn) { return this.forEach(function (el) { el.addEventListener(evt, fn, false); }); }
這很簡單。我們只需遍曆元素並使用addEventListener
方法。 off
函數(它取消掛鉤事件處理程序)幾乎相同:
off(evt, fn) { return this.forEach(function (el) { el.removeEventListener(evt, fn, false); }); }
要使用Dome
,只需將其放入腳本並導入它。
import {Dome, create} from "./dome.js"
從那裡,你可以像這樣使用它:
new Dome("li") ...
確保你導入它的腳本是ES模塊。
我希望你能嘗試一下我們的小型庫,甚至可以擴展它一點。正如我前面提到的,我已經把它放在GitHub上了。隨意分叉它,玩耍,並發送拉取請求。
讓我再次澄清一下:本教程的目的並不是建議你應該總是編寫自己的庫。有專門的團隊在共同努力,使大型的、成熟的庫盡可能好。這裡的目的是讓你對庫內部可能發生的事情有所了解;我希望你在這裡學到了一些技巧。
我強烈建議你在你的一些最喜歡的庫中四處挖掘。你會發現它們並不像你想像的那麼神秘,而且你可能會學到很多東西。以下是一些不錯的起點:
這篇文章已更新,其中包含Jacob Jackson的貢獻。 Jacob是一位網絡開發者、技術作家、自由職業者和開源貢獻者。
以上是構建您的第一個JavaScript庫的詳細內容。更多資訊請關注PHP中文網其他相關文章!