創建你的第一個JavaScript庫

是否曾對Mootools的魔力感到驚奇?是否有想知道Dojo如何做到那樣的?是否對jQuery感到好奇?在這個教程中,我們將瞭解它們背後的東西並且動手創建一個超級簡單的你最喜歡的庫。

20121022110555534359.JPG

  我們其乎每天都在使用JavaScript庫。當你剛入門時,利用jQuery是一件非常奇妙的事,主要是因爲它的DOM操作。首先,DOM對於入門者來說可能是相對困難的事情;其次用它我們幾乎可以不用考慮跨瀏覽器兼容的問題。

  在這個教程中,我們將試着從頭開始實現一個很簡單的庫。是的,它非常有意思,但是在你高興之前讓我申明幾點:

  • 這不會是全功能的庫。我們有很多方法要寫,但是它不是jQuery。我們將會做足工作來讓你感受到在你創建一個庫時會遇到的各種問題。

  • 我們不會完全解決所有瀏覽器的兼容性問題。我們寫的代碼能支持IE8+,Firefox 5+,Opera 10+,Chrome和Safari。

  • 我們不會覆蓋使用我們庫的所有可能性。比如我們的append和prepend方法只在你傳入一個我們庫的實例時纔有效,它們不支持原生的DOM節點或節點集合。

 步驟1: 創建庫樣板文件Creating the Library Boilerplate

  我們以一些封裝代碼開始,它將會包含我們整個庫。它就是你經常用到的立即執行函數表達式

1
2
3
4
5
6
7
8
9
window.dome = (function() {
functionDome (els) {
}
vardome = {
get: function(selector) {
}
};
returndome;
}());

  如你所見,我們把我們的庫叫Dome,因爲它主要就是一個針對DOM的庫,是的,它很不完整。

  到此我們做了兩件事。首先,我們定義了一個函數,它最終會是實例化我們庫的構造函數,這些對象將會封裝我們選擇或創建的元素。

  接下來我們創建了dome對象,它是我們實際的庫對象;你能看到,它在最後被返回。它有一個空的get函數,我們將用它來從頁面中選擇元素。所以,讓我們現在來填充它的代碼。


 步驟2: 獲取元素

  dome.get函數傳入一個參數,但是它可以有好幾種情況。如果它是一個字符串,我們假定它是一個CSS選擇器;但是我們也可以傳入單個DOM節點或是一個NodeList。

1
2
3
4
5
6
7
8
9
10
11
get: function(selector) {
varels;
if(typeofselector === "string") {
els = document.querySelectorAll(selector);
} elseif(selector.length) {
els = selector;
} else{
els = [selector];
}
returnnewDome(els);
}

  我們使用document.querySelectorAll來簡化元素的查找:當然這有瀏覽器兼容性問題,但是對於我們的例子來說它是ok的。如果selector不是字符串,我們將檢查它的length屬性。如果它存在,我們就知道它是一個NodeList;否則它是單個元素然後我們將它放到一個數組中。這就是我們下面需要將調用Dome的結果傳給一個數組的原因;你可以看到我們返回一個新的Dome對象。所以讓我們回頭看看Dome函數並填充它。


 步驟3: 創建Dome實例

  下面是Dome函數:

1
2
3
4
5
6
functionDome (els) {
for(vari = 0; i < els.length; i++ ) {
this[i] = els[i];
}
this.length = els.length;
}

  它確實很簡單:我們只是遍歷我們選擇的元素並把它們附到帶有數字索引的新對象中。然後我們添加一個length屬性。

  但是這的關鍵是什麼呢?爲什麼不直接返回元素?我們將元素封裝到一個對象因爲我們想爲這個對象創建方法;這些方法可以讓我們與這些元素交互。這實際上就是jQuery採用的方法的簡化版本。

  所以,我們返回了Dome對象,讓我們在它的原型上添加一些方法。我把這些方法直接寫在Dome函數中。


 步驟4: 添加一些常用工具函數

  我們要寫的第一個方法是一個簡單的工具函數。因爲我們的Dome對象可以封裝多個DOM元素,幾乎每個方法都需要遍歷每個元素;所以,這些工具函數會非常便利。

  讓我們以一個map函數開始:

1
2
3
4
5
6
7
Dome.prototype.map = function(callback) {
varresults = [], i = 0;
for( ; i < this.length; i++) {
results.push(callback.call(this, this[i], i));
}
returnresults;
};

  當然,map函數傳入單個參數,一個回調函數。我們遍歷數組中的每一項,收集回調函數返回的所有內容放到results數組中。注意我們如何調用回調函數:

1
callback.call(this, this[i], i));

  這樣函數就會在我們的Dome實例的上下文中被調用,它接受兩個參數:當前元素,以及索引號。

  我們也想要一個forEach函數。它確實非常簡單:

1
2
3
4
Dome.prototype.forEach(callback) {
this.map(callback);
returnthis;
};

  map和forEach間的唯一區別是map需要返回一些東西,因此我們也可以只傳入我們的回調函數給this.map並忽略返回的數組,我們將返回this來使得我們的庫支持鏈式操作。我們將經常使用forEach。所以,注意當返回我們的this.forEach對函數的調用時,我們事實上是返回了this。例如,下面的方法實際上返回相同的東西:

1
2
3
4
5
6
7
Dome.prototype.someMethod1 = function(callback) {
this.forEach(callback);
returnthis;
};
Dome.prototype.someMethod2 = function(callback) {
returnthis.forEach(callback);
};

  另外:mapOne。很容易看出這個函數是幹什麼的,但是問題是爲什麼我們需要它?它需要一些你可以叫做“庫哲學”的東西來解釋。

 一個簡單的“哲學的”迂迴

  如果創建一個庫只是寫代碼,那就不是什麼難的工作了。但是我正在做這個項目,我發現困難的部分是決定一些方法應該如何工作。

  很快,我們將建一個text方法,它返回我們選擇元素的文本。如果我們的Dome對象封裝幾個DOM節點(如dome.get("li")),它會返回什麼呢?如果你在jQuery做類似的事情($("li").text()),你將會得到一個所有元素的文本拼起來的字符串。它有用嗎?我認爲沒用,但是我不知道更好的返回是什麼。

  在這個項目中,我將以數組形式返回多個元素的文本,除非數組中只有一個元素,那我們就返回一個文本字符串,而不是隻有一個元素的數組。我想你最常用的是獲取單個元素的文本,所以我們對這個情況進行優化。然而,如果你獲取多個元素的文本,我們也會返回一些你能操作的東西。

 回到代碼

  所以,mapOne方法只是簡單的運行map,然後要麼返回數組,要麼返回單元素數組中的元素。如果你還是不確定這有什麼用,等一會你會發現的!

1
2
3
4
Dome.prototype.mapOne = function(callback) {
varm = this.map(callback);
returnm.length > 1 ? m : m[0];
};

 步驟5: 處理文本和HTML

  接下來,讓我們添加text方法。就像jQuery一樣,我們可以給它傳入一個字符串並設置元素的文本,或不傳參數來獲取元素的文本。

1
2
3
4
5
6
7
8
9
10
11
Dome.prototype.text = function(text) {
if(typeoftext !== "undefined") {
returnthis.forEach(function(el) {
el.innerText = text;
});
} else{
returnthis.mapOne(function(el) {
returnel.innerText;
});
}
};
1
2
3
4
5
6
7
8
9
10
11
Dome.prototype.text = function(text) {
if(typeoftext !== "undefined") {
returnthis.forEach(function(el) {
el.innerText = text;
});
} else{
returnthis.mapOne(function(el) {
returnel.innerText;
});
}
};

  你可能也想到了,我們需要檢查text的值來看它是要設置還是要獲取。注意如果只是用if(text)會有問題,因爲空字符串會被判斷爲false。

  如果我們在設置值,我們將對元素調用forEach並且設置它們的innerText屬性爲text。如果我們要獲取,我們將返回元素的 innerText屬性。注意我們使用mapOne方法:如果我們在處理多個元素,它將返回一個數組,否則它將就是一個字符串。

  html方法幾乎與text一樣,除了它使用innerHTML屬性而不是innerText。

1
2
3
4
5
6
7
8
9
10
11
12
Dome.prototype.html = function(html) {
if(typeofhtml !== "undefined") {
this.forEach(function(el) {
el.innerHTML = html;
});
returnthis;
} else{
returnthis.mapOne(function(el) {
returnel.innerHTML;
});
}
};

  就像我說的:幾乎完全一樣。


 步驟6: 調整樣式

  再接下來,我們希望能添加和刪除樣式,因此讓我們來寫一個addClass和removeClass方法。

  我們的addClass方法將接收一個字符串或是樣式名稱的數組。爲了做到這點,我們需要檢查參數的類型。如果是數組,我們將遍歷它並創建一個樣式名的字符串。否則,我們就簡單的在樣式名前加一個空格,這樣它就不會和元素已有的樣式混在一些。然後我們遍歷元素並且將新的樣式附加到className屬性後面。

1
2
3
4
5
6
7
8
9
10
11
12
13
Dome.prototype.addClass = function(classes) {
varclassName = "";
if(typeofclasses !== "string") {
for(vari = 0; i < classes.length; i++) {
className += " "+ classes[i];
}
} else{
className = " "+ classes;
}
returnthis.forEach(function(el) {
el.className += className;
});
};

  很直接,對嗎?

  那如何刪除樣式呢?爲了保持簡單,我們只允許一次刪除一個樣式。

1
2
3
4
5
6
7
8
9
Dome.prototype.removeClass = function(clazz) {
returnthis.forEach(function(el) {
varcs = el.className.split(" "), i;
while( (i = cs.indexOf(clazz)) > -1) {
cs = cs.slice(0, i).concat(cs.slice(++i));
}
el.className = cs.join(" ");
});
};

  對每個元素,我們將el.className分隔成一個數組。然後,我們使用一個while循環來剔除我們傳入的樣式,直到cs.indexOf(clazz)返回-1。我們這樣做是爲了處理同樣的樣式在一個元素中出現的不止一次的特殊情況:我們必須保證它真的被刪除了。一旦我們確保刪除每個樣式的實例,我們用空格連接數組的每一項並把它設置到el.className。


 步驟7: 修正一個IE的Bug

  我們正在處理的最糟糕的瀏覽器是IE8。在我們的小小的庫中,只有一個IE bug需要我們處理,很幸運它很簡單。IE8不支持Array的indexOf方法;我們在removeClass中使用到它,所以讓我們修復它:

1
2
3
4
5
6
7
8
9
10
if(typeofArray.prototype.indexOf !== "function") {
Array.prototype.indexOf = function(item) {
for(vari = 0; i < this.length; i++) {
if(this[i] === item) {
returni;
}
}
return-1;
};
}

  它非常簡單,並且這不是一個完全的實現(不支持第二個參數),但是能達到我們的目的。


 步驟8: 調節屬性

  現在,我們想要一個attr函數。這很容易,因爲它與我們的text或html方法非常類似。像那些方法一樣,我們能夠獲取或設置屬性值:我們可以傳入元素名和值來設置,也可以只傳入屬性名來獲取。

1
2
3
4
5
6
7
8
9
10
11
Dome.prototype.attr = function(attr, val) {
if(typeofval !== "undefined") {
returnthis.forEach(function(el) {
el.setAttribute(attr, val);
});
} else{
returnthis.mapOne(function(el) {
returnel.getAttribute(attr);
});
}
};

  如果val有一個值,我們將遍歷這些元素並且將選擇的屬性設置爲這個值,使用元素的setAttribute方法。否則,我們使用mapOne通過getAttribute方法來返回屬性值。


 步驟9: 創建元素

  像很多好的庫一樣,我們應該能夠創建新的元素。當然它作爲一個Dome實例的一個方法不是很好,所以讓我們直接把它掛到dome對象上去。

1
2
3
4
5
vardome = {
// get method here
create: function(tagName, attrs) {
}
};

  你已經看到,我們使用兩個參數:元素的名字,和屬性值對象。大部分屬性能過attr方法賦值,但是兩種方法可以做特殊處理。我們使用addClass方法操作className屬性,以及text方法操作text屬性。當然,我們首先需要創建元素和Dome對象。下面是整個操作的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create: function(tagName, attrs) {
varel = newDome([document.createElement(tagName)]);
if(attrs) {
if(attrs.className) {
el.addClass(attrs.className);
deleteattrs.className;
}
if(attrs.text) {
el.text(attrs.text);
deleteattrs.text;
}
for(varkey inattrs) {
if(attrs.hasOwnProperty(key)) {
el.attr(key, attrs[key]);
}
}
}
returnel;
}

  我們創建元素並將它傳給一個新的Dome對象。然後中我們處理屬性。注意在操作完它們後我們必須刪除className和text屬性。這樣可以避免當我們在attrs中遍歷剩下的key值時被應用爲屬性。當然我們最後要返回這個新建的Dome對象。

  但是現在只是創建了新的元素,我們希望把它插入到DOM中對嗎?


 步驟10: 附加元素

  下一步,我們將寫append和prepend方法。這些確實是有點難搞的函數,主要是因爲有很多種使用情況。以下是我們希望能做到的:

1
2
dome1.append(dome2);
dome1.prepend(dome2);

  使用情況如下:我們可能想要append或prepend

  • 一個新的元素到一個或多個已存在的元素

  • 多個新元素到一個或多個已存在的元素

  • 一個已存在的元素到一個或多個已存在的元素

  • 多個已存在的元素到一個或多個已存在的元素

  注意:我使用“新”來表示元素還沒有在DOM中;已存在的元素是已經在DOM中有的。

  讓我們一步一步來:

1
2
3
4
5
6
Dome.prototype.append = function(els) {
this.forEach(function(parEl, i) {
els.forEach(function(childEl) {
});
});
};

  我們期望els參數是一個Dome對象。一個完整的DOM庫可以接受一個節點或nodelist作爲參數,但是我們暫時不這樣做。我們必須遍歷我們每一個元素,並且在它裏面,我們還要遍歷每個我們需要append的元素。

  如果我們將els到多個元素,我們需要克隆它們。然而,我們不想在他們第一次被附加的時候克隆節點,而時隨後再說。所以我們這樣:

1
2
3
if(i > 0) {
childEl = childEl.cloneNode(true);
}

  這個i來自外層的forEach循環:它是當前父元素的索引。如果我們不是附加到第一個父元素,我們將克隆節點。這樣,真正的節點將會放到第一個父節點中,其它父節點將獲得一個拷貝。這樣很好用,因爲傳入的Dome對象將只會擁有原始的節點。所以如果我們只是附加單個元素到單個元素,使用的所有節點都將是各自Dome對象的一部分。

  最後,我們終於可以附加元素:

1
parEl.appendChild(childEl);

  所以,彙總起來是這樣

1
2
3
4
5
6
7
8
9
10
Dome.prototype.append = function(els) {
returnthis.forEach(function(parEl, i) {
els.forEach(function(childEl) {
if(i > 0) {
childEl = childEl.cloneNode(true);
}
parEl.appendChild(childEl);
});
});
};

 prepend方法

  我們想要prepend方法也滿足同樣的情況,所以這個方法非常類似:

1
2
3
4
5
6
7
8
Dome.prototype.prepend = function(els) {
returnthis.forEach(function(parEl, i) {
for(varj = els.length -1; j > -1; j--) {
childEl = (i > 0) ? els[j].cloneNode(true) : els[j];
parEl.insertBefore(childEl, parEl.firstChild);
}
});
};

  當prepend時所不同的是如果你順次prepend一系列元素到另外一個元素時,它們是倒序的。因爲我們不能反向forEach,我將使用for循環反向遍歷。同樣,我們將克隆節點如果它不是我們第一個要附件到的父節點。


 步驟11: 移除節點

  對於我們最後一個節點處理方法,我們想要從DOM中刪除節點。其實很簡單:

1
2
3
4
5
Dome.prototype.remove = function() {
returnthis.forEach(function(el) {
returnel.parentNode.removeChild(el);
});
};

  就是遍歷節點並在每個元素的parentNode上調用removeChild方法。這裏漂亮的地方在於這個Dome對象還將正常工作;我們可以在它上面使用任何方法,包括重新放回到DOM中去。


 步驟12: 處理事件

  最後,但是肯定不是用得最少的,我們將寫一些函數處理事件。你可以知道,IE8使用老式的IE事件,所以我們需要檢查它。同時,我們將拋出DOM 0事件,就因爲我們可以。

簽出方法,然後中我們將討論它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dome.prototype.on = (function() {
if(document.addEventListener) {
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el.addEventListener(evt, fn, false);
});
};
} elseif(document.attachEvent)  {
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el.attachEvent("on"+ evt, fn);
});
};
} else{
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el["on"+ evt] = fn;
});
};
}
}());

  在這,我們使用了一個立即執行函數表達式,在函數裏面我們做了特徵檢查。如果document.addEventListener存在,我們將使用它;否則我們檢查document.attachEvent或者求助於DOM 0事件。注意我們如何返回最後的函數:它將在結束時被賦給Dome.prototype.on。當做特徵檢測時,非常方便地像這樣賦給合適的函數,而不是每次函數運行時都得檢查一次。

  off函數用於卸載事件,它與前面非常類似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Dome.prototype.off = (function() {
if(document.removeEventListener) {
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el.removeEventListener(evt, fn, false);
});
};
} elseif(document.detachEvent)  {
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el.detachEvent("on"+ evt, fn);
});
};
} else{
returnfunction(evt, fn) {
returnthis.forEach(function(el) {
el["on"+ evt] = null;
});
};
}
}());

 就是這樣!

  我希望你能試一試我們的小小的庫,並且能稍稍擴展一點點。就像我之前是提到的一樣,我把它放到Github上了。可以免費fork,玩一玩,並且發送一個pull請求。

  讓我再申明一下,這個教程的目的不是說建議你總是要寫一個自己的庫。

  有專業的團隊在做一個龐大的,穩定的越來越好的庫。這裏我們只是想讓大家看看一個庫內部是什麼樣子的,希望你能在這學到一些東西。

原文鏈接:Build Your First JavaScript Library


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章