Javascript高級技術篇(1):搭建JS框架類庫


經過了"面向對象的Javascript系列"的預熱,讓我們再次起航進入Javascript富客戶端系列。基於鏈式調用關於類庫的講解,本講將一步一步搭建一個屬於自己的JS框架類庫。在這里,不妨問問大家:一個類庫怎樣才能算有價值的類庫呢?我想不妨從以下幾個方面去考量:

1). 避免改變JS固有的基礎對象。即如對JS對象Function,String,Array等,不要試圖改變這些對象的行為來適應你的場景。

2). 具有良好的版本控制和文檔注釋。即JS類庫必須有詳細的文檔,以備使用者能更快的應用。

3). 具有規范命名空間的約定。即JS類庫必須添加完整的命名空間,有利於開發者快速定位自己需要的功能。

4). 避免加入任何的業務代碼。即不要將與你業務邏輯相關的代碼添加到類庫中。

5). 模塊職責清晰,按需加載。即基礎類庫並不是一個超大庫,而是按職責划分出來組合在一起的,在你需要的場景中僅加載所需要的類庫。

6). ...........

可能還有很多,以上是一個優秀框架的基本特征。接下來我們開始搭建我們的JS框架類庫。

1). 定義基礎類庫的類。

var $ = function() {};

調用方式:

// Instantiate the $ library object as a singleton for use on a page
$ = new $();

2). 重寫window.onload事件。我們都知道一個標准的html包含<head>和<body>元素,通常JS開發人員習慣把所有的頁面初始化代碼放在window.onload中,但此事件是在整個頁面(包含外部資源如圖片,動畫等)加載完畢后觸發。這樣對於大型的站點來說,它的圖片量越大直接導致用戶等待頁面顯示的事件越久。但幸運的是,W3C組織定義了"DOMContentLoaded"事件,在頁面元素加載完畢之后和頁面外部資源文件加載之前觸發,這樣用戶不必再浪費時間去等待圖片全部加載完畢后才能看到頁面。不幸的是,有些瀏覽器(如IE6,7,8)並未實現該事件。因此,我們需要重寫window.onload事件來解決這個問題。

$.prototype.onDomReady = function(callback) {
if (document.addEventListener) {
// If the browser supports the DOMContentLoaded event,
// assign the callback function to execute when that event fires
document.addEventListener('DOMContentLoaded', callback, false);
} else {
if(document.body && document.body.lastChild) {
// If the DOM is available for access, execute the callback function
callback();
} else {
// Reexecute the current function, denoted by arguments.callee,
// after waiting a brief nanosecond so as not to lock up the browser
return setTimeout(arguments.callee, 0);
}
}
}

調用方式:

// Outputs "The DOM is ready!" when the DOM is ready for access
$.onDomReady(function() {
alert("The DOM is ready!");
});

3). 統一多瀏覽器事件觸發機制。大多數瀏覽器的事件觸發機制都差不多,如超鏈接點擊或Form提交。但在IE6和IE7以及其它瀏覽器卻不相同,不僅如此,在頁面元素和屬性之間也有差異,如獲取鼠標位置等。

// Add a new namespace to the $ library to hold all event-related code,
//
using an object literal notation to add multiple methods at once
$.prototype.Events = {
// The add method allows us to assign a function to execute when an
// event of a specified type occurs on a specific element
add: function(element, eventType, callback) {
// Store the current value of this to use within subfunctions
var self = this;
eventType = eventType.toLowerCase();

if (element.addEventListener) {
// If the W3C event listener method is available, use that
element.addEventListener(eventType, function(e) {
// Execute callback function, passing it a standardized version of
// the event object, e. The standardize method is defined later
callback(self.standardize(e));
}, false);
} else if(element.attachEvent) {
// Otherwise use the Internet Explorer-proprietary event handler
element.attachEvent("on" + eventType, function() {
// IE uses window.event to store the current event's properties
callback(self.standardize(window.event));
});
}
},
// The remove method allows us to remove previously assigned code from an event
remove: function(element, eventType, callback) {
eventType = eventType.toLowerCase();
if (element.removeEventListener) {
// If the W3C-specified method is available, use that
element.removeEventListener(element, eventType, callback);
} else if (element.detachEvent) {
// Otherwise, use the Internet Explorer-specific method
element.detachEvent("on" + eventType, callback);
}
},
// The standardize method produces a unified set of event
// properties, regardless of the browser
standardize: function(event) {
// These two methods, defined later, return the current position of the
// mouse pointer, relative to the document as a whole, and relative to the
// element the event occurred within
var page = this.getMousePositionRelativeToDocument(event);
var offset = this.getMousePositionOffset(event);
// Let's stop events from firing on element nodes above the current
if(event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
// We return an object literal containing seven properties and one method
return {
// The target is the element the event occurred on
target: this.getTarget(event),
// The relatedTarget is the element the event was listening for,
// which can be different from the target if the event occurred on an
// element located within the relatedTarget element in the DOM
relatedTarget: this.getRelatedTarget(event),
// If the event was a keyboard-related one, key returns the character
key: this.getCharacterFromKey(event),
// Return the x and y coordinates of the mouse pointer, relative to the document
pageX: page.x,
pageY: page.y,
// Return the x and y coordinates of the mouse pointer,
// relative to the element the current event occurred on
offsetX: offset.x,
offsetY: offset.y,
// The preventDefault method stops the default event of the element
// we're acting upon from occurring. If we were listening for click
// events on a hyperlink, for example, this method would stop the
// link from being followed
preventDefault: function() {
if (event.preventDefault) {
event.preventDefault(); // W3C method
} else {
event.returnValue = false; // Internet Explorer method
}
}
};
},
// The getTarget method locates the element the event occurred on
getTarget: function(event) {
// Internet Explorer value is srcElement, W3C value is target
var target = event.srcElement || event.target;
// Fix legacy Safari bug which reports events occurring on a text
// node instead of an element node
if (target.nodeType == 3) { // 3 denotes a text node
target = target.parentNode; // Get parent node of text node
}
// Return the element node the event occurred on
return target;
},
// The getCharacterFromKey method returns the character pressed when
// keyboard events occur. You should use the keypress event
// as others vary in reliability
getCharacterFromKey: function(event) {
var character = "";
if (event.keyCode) { // Internet Explorer
character = String.fromCharCode(event.keyCode);
} else if (event.which) { // W3C
character = String.fromCharCode(event.which);
}
return character;
},
// The getMousePositionRelativeToDocument method returns the current
// mouse pointer position relative to the top left edge of the current page
getMousePositionRelativeToDocument: function(event) {
var x = 0, y = 0;
if (event.pageX) {
// pageX gets coordinates of pointer from left of entire document
x = event.pageX;
y = event.pageY;
} else if (event.clientX) {
// clientX gets coordinates from left of current viewable area
// so we have to add the distance the page has scrolled onto this value
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
}
// Return an object literal containing the x and y mouse coordinates
return {
x: x,
y: y
}
},
// The getMousePositionOffset method returns the distance of the mouse
// pointer from the top left of the element the event occurred on
getMousePositionOffset: function(event) {
var x = 0, y = 0;
if (event.layerX) {
x = event.layerX;
y = event.layerY;
} else if (event.offsetX) {
// Internet Explorer-
proprietary
x = event.offsetX;
y = event.offsetY;
}
// Returns an object literal containing the x and y coordinates of the
// mouse relative to the element the event fired on
return {
x: x,
y: y
}
},
// The getRelatedTarget method returns the element node the event was set up to
// fire on, which can be different from the element the event actually fired on
getRelatedTarget: function(event) {
var relatedTarget = event.relatedTarget;
if (event.type == "mouseover") {
// With mouseover events, relatedTarget is not set by default
relatedTarget = event.fromElement;
} else if (event.type == "mouseout") {
// With mouseout events, relatedTarget is not set by default
relatedTarget = event.toElement;
}
return relatedTarget;
}
};

調用方式:

// Clicking anywhere on the page will output the current coordinates of the mouse pointer
$.Events.add(document.body, "click", function(e) {
alert("Mouse clicked at 'x' position " + e.pageX + " and 'y' position "+ e.pageY);
});

4). 加入AJAX異步加載機制。我們都知道Ajax的到來給JS帶來了無限的活力,它擁有很多鮮明的特性,如無刷新,動態異步加載等。我們可不能丟掉這塊"肥肉"了,趕快加入到我們的JS基礎框架體系中把。

// Define a new namespace within the $ library, called Remote, to store our Ajax methods
$.prototype.Remote = {
// The getConnector method returns the base object for performing
// dynamic browser-server communication through JavaScript
getConnector: function() {
var connectionObject = null;
if (window.XMLHttpRequest) {
// If the W3C-supported request object is available, use that
connectionObject = new XMLHttpRequest();
} else if (window.ActiveXObject) {
// Otherwise, if the IE-proprietary object is available, use that
connectionObject = new ActiveXObject('Microsoft.XMLHTTP');
}
// Both objects contain virtually identical properties and methods
// so it's just a case of returning the correct one that's supported
// within the current browser
return connectionObject;
},
// The configureConnector method defines what should happen while the
// request is taking place, and ensures that a callback method is executed
// when the response is successfully received from the server
configureConnector: function(connector, callback) {
// The readystatechange event fires at different points in the life cycle
// of the request, when loading starts, while it is continuing and again when it ends
connector.onreadystatechange = function() {
// If the current state of the request informs us that the current request has completed
if (connector.readyState == 4) {
// Ensure the HTTP status denotes successful download of content
if (connector.status == 200) {
// Execute the callback method, passing it an object
// literal containing two properties, the raw text of the
// downloaded content and the same content in XML format,
// if the content requested was able to be parsed as XML.
// We also set its owner to be the connector in case this
// object is required in the callback function
callback.call(connector, {
text: connector.responseText,
xml: connector.responseXML
});
}
}
};
},
// The load method takes an object literal containing a URL to load and a method
// to execute once the content has been downloaded from that URL. Since the
// Ajax technique is asynchronous, the rest of the code does not wait for the
// content to finish downloading before continuing, hence the need to pass in
// the method to execute once the content has downloaded in the background.
load: function(request) {
// Take the url from the request object literal input,
// or use an empty string value if it doesn't exist
var url = request.url || "";
// Take the callback method from the request input object literal,
// or use an empty function if it is not supplied
var callback = request.callback || function() {};
// Get our cross-browser connection object
var connector = this.getConnector();
if (connector) {
// Configure the connector to execute the callback method once the
// content has been successfully downloaded
this.configureConnector(connector, callback);
// Now actually make the request for the contents found at the URL
connector.open("GET", url, true);
connector.send("");
}
},
// The save method performs an HTTP POST action, effectively sending content,
// such as a form's field values, to a server-side script for processing
save: function(request) {
var url = request.url || "";
var callback = request.callback || function() {};
// The data variable is a string of URL-encoded name-value pairs to send to
// the server in the following format: "parameter1=value1&parameter2=value2&..."
var data = request.data || "";
var connector = this.getConnector();
if (connector) {
this.configureConnector(connector, callback);
// Now actually send the data to script found at the URL
connector.open("POST", url, true);
connector.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
connector.setRequestHeader("Content-length", data.length);
connector.setRequestHeader("Connection", "close");
connector.send(data);
}
}
};

調用方式:

// Load the contents of the URL index.html from the root of the web server
$.Remote.load({
url: "/index.html",
callback: function(response) {
// Get the plain text contents of the file
var text = response.text;
// If the HTML file was written in XHTML format, it would be available
// in XML format through the response.xml property
var xml = response.xml;
// Output the contents of the index.html file as plain text
alert(text);
}
});
// Send some data to a server-side script at the URL process-form.php
$.Remote.save({
url: "/index.html",
data: "name=Miracle&surname=He",
callback: function(response) {
// Output the server-side script's response to the form submission
alert(response.text);
}
});

5). 建立工具類庫。可封裝許多常用的功能,使開發者能以更快捷的方式實現功能。

// Add the Utils namespace to hold a set of useful, reusable methods
$.prototype.Utils = {
// The mergeObjects method copies all the property values of one object literal into another,
// replacing any properties that already exist, and adding any that don't
mergeObjects: function(original, newObject) {
// for ... in ... loops expose unwanted properties such as prototype
// and constructor, among others. Using the hasOwnProperty
// native method allows us to only allow real properties to pass
for (var key in newObject) {
if (newObject.hasOwnProperty(key)) {
// Loop through every item in the new object literal,
// getting the value of that item in the original object and
// the equivalent value in the original object, if it exists
var newPropertyValue = newObject[key];
var originalPropertyValue = original[key];
}
// Set the value in the original object to the equivalent value from the
// new object, except if the property's value is an object type, in
// which case call this method again recursively, in order to copy every
// value within that object literal also
original[key] = (originalPropertyValue &&
typeof newPropertyValue == 'object' &&
typeof originalPropertyValue == 'object') ?
this.mergeObjects(originalPropertyValue, newPropertyValue) :
newPropertyValue;
}
// Return the original object, with all properties copied over from the new object
return original;
},
// The replaceText method takes a text string containing placeholder values and
// replaces those placeholders with actual values passed in through the values
// object literal.
// For example: "You have {count} messages in the {folderName} folder"
// Each placeholder, marked with braces – { } – will be replaced with the
// actual value from the values object literal, the properties count and
// folderName will be sought in this case
replaceText: function(text, values) {
for (var key in values) {
if (values.hasOwnProperty(key)) {
// Loop through all properties in the value object literal
if (typeof values[key] == undefined) { // Code defensively
values[key] = "";
}
// Replace the property name wrapped in braces from the text
// string with the actual value of that property. The regular
// expression ensures that multiple occurrences are replaced
text = text.replace(new RegExp("{" + key +"}", "g"), values[key]);
}
}
// Return the text with all placeholder values replaced with real ones
return text;
},
// The toCamelCase method takes a hyphenated value and converts it into
// a camel case equivalent, e.g., margin-left becomes marginLeft. Hyphens
// are removed, and each word after the first begins with a capital letter
toCamelCase: function(hyphenatedValue) {
var result = hyphenatedValue.replace(/-\D/g, function(character) {
return character.charAt(1).toUpperCase();
});
return result;
},
// The toHyphens method performs the opposite conversion, taking a camel
// case string and converting it into a hyphenated one.
// e.g., marginLeft becomes margin-left
toHyphens: function(camelCaseValue) {
var result = camelCaseValue.replace(/[A-Z]/g, function(character) {
return ('-'+ character.charAt(0).toLowerCase());
});
return result;
}
};

調用方式:

// Combine two object literals
var creature = {
face: 1,
arms: 2,
legs: 2
};
var animal = {
legs: 4,
chicken: true
};
// Resulting object literal becomes...
//
{
//
face: 1,
//
arms: 2,
//
legs: 4,
//
chicken: true
//
}
creature = $.Utils.mergeObjects(creature, animal);
// Outputs "You have 3 messages waiting in your inbox.";
$.Utils.replaceText("You have {count} messages waiting in your {folder}.", {
count: 3,
folder: "inbox"
});
// Outputs "fontFamily"
alert($.Utils.toCamelCase("font-family"));
// Outputs "font-Family"
alert($.Utils.toHyphens("fontFamily"));

6). 處理元素樣式CSS。我們一致提倡頁面Html元素與樣式分離,也就是說樣式不應直接摻雜到頁面中,要構建高性能且富有表現力的頁面,我們可以動態添加或移除元素樣式以更好的表現。

// Define the CSS namespace within the $ library to store style-related methods
$.prototype.CSS = {
// The getAppliedStyle method returns the current value of a specific
// CSS style property on a particular element
getAppliedStyle: function(element, styleName) {
var style = "";
if (window.getComputedStyle) {
// W3C-specific method. Expects a style property with hyphens
style = element.ownerDocument.defaultView.getComputedStyle(element, null)
.getPropertyValue($.Utils.toHyphens(styleName));
} else if (element.currentStyle) {
// Internet Explorer-specific method. Expects style property names in camel case
style = element.currentStyle[$.Utils.toCamelCase(styleName)];
}
// Return the value of the style property found
return style;
},
// The getArrayOfClassNames method is a utility method which returns an
// array of all the CSS class names assigned to a particular element.
// Multiple class names are separated by a space character
getArrayOfClassNames: function(element) {
var classNames = [];
if (element.className) {
// If the element has a CSS class specified, create an array
classNames = element.className.split(' ');
}
return classNames;
},
// The addClass method adds a new CSS class of a given name to a particular element
addClass: function(element, className) {
// Get a list of the current CSS class names applied to the element
var classNames = this.getArrayOfClassNames(element);
// Add the new class name to the list
classNames.push(className);
// Convert the list in space-separated string and assign to the element
element.className = classNames.join(' ');
},
// The removeClass method removes a given CSS class name from a given element
removeClass: function(element, className) {
var classNames = this.getArrayOfClassNames(element);
// Create a new array for storing all the final CSS class names in
var resultingClassNames = [];
for (var index = 0; index < classNames.length; index++) {
// Loop through every class name in the list
if (className != classNames[index]) {
// Add the class name to the new list if it isn't the one specified
resultingClassNames.push(classNames[index]);
}
}
// Convert the new list into a space-separated string and assign it
element.className = resultingClassNames.join(" ");
},
// The hasClass method returns true if a given class name exists on a
// specific element, false otherwise
hasClass: function(element, className) {
// Assume by default that the class name is not applied to the element
var isClassNamePresent = false;
var classNames = this.getArrayOfClassNames(element);
for (var index = 0; index < classNames.length; index++) {
// Loop through each CSS class name applied to this element
if (className == classNames[index]) {
// If the specific class name is found, set the return value to true
isClassNamePresent = true;
}
}
// Return true or false, depending on if the specified class name was found
return isClassNamePresent;
},
// The getPosition method returns the x and y coordinates of the top-left
// position of a page element within the current page, along with the
// current width and height of that element
getPosition: function(element) {
var x = 0, y = 0;
var elementBackup = element;
if (element.offsetParent) {
// The offsetLeft and offsetTop properties get the position of the
// element with respect to its parent node. To get the position with
// respect to the page itself, we need to go up the tree, adding the
// offsets together each time until we reach the node at the top of
// the document, by which point, we'll have coordinates for the
// position of the element in the page
do {
x += element.offsetLeft;
y += element.offsetTop;
// Deliberately using = to force the loop to execute on the next
// parent node in the page hierarchy
} while (element = element.offsetParent)
}
// Return an object literal with the x and y coordinates of the element,
// along with the actual width and height of the element
return {
x: x,
y: y,
height: elementBackup.offsetHeight,
width: elementBackup.offsetWidth
}
}
};

調用方式:

// Locate the first <hr> element within the page
var horizontalRule = document.getElementsByTagName("hr")[0];
// Output the current width of the <hr> element
alert($.CSS.getAppliedStyle(horizontalRule, "width"));
// Add the hide CSS class to the <hr> element
$.CSS.addClass(horizontalRule, "hide");
// Remove the hide CSS class from the <hr> element
$.CSS.removeClass(horizontalRule, "hide");
// Outputs true if the hide CSS class exists on the <hr> element
alert($.CSS.hasClass(horizontalRule, "hide"));
// Outputs the x and y coordinates of the <hr> element
var position = $.CSS.getPosition(horizontalRule);
alert("The element is at 'x' position '" + position.x + "' and 'y' position '" + position.y +
"'. It also has a width of '" + position.width + "' and a height of '" + position.height + "'");

7). 快速定位DOM元素。改進JS獲取元素的復雜性,提供更快捷的方式獲取。

// Add a new Elements namespace to the $ library
$.prototype.Elements = {
// The getElementsByClassName method returns an array of DOM elements
// which all have the same given CSS class name applied. To improve the speed
// of the method, an optional contextElement can be supplied which restricts the
// search to only those child nodes within that element in the node hierarchy
getElementsByClassName: function(className, contextElement) {
var allElements = null;
if (contextElement) {
// Get an array of all elements within the contextElement
// The * wildcard value returns all tags
allElements = contextElement.getElementsByTagName("*");
} else {
// Get an array of all elements, if no contextElement was supplied
allElements = document.getElementsByTagName("*");
}
var results = [];
for (var elementIndex = 0; elementIndex < allElements.length; elementIndex++) {
// Loop through every element found
var element = allElements[elementIndex];
// If the element has the specified class, add that element to the output array
if ($.CSS.hasClass(element, className)) {
results.push(element);
}
}
// Return the list of elements that contain the specific CSS class name
return results;
}
};

8). 可能還有很多的功能加入,並通過版本控制的方式。這里先到此為止,我們在最后初始化$類庫。

// Instantiate the $ library as a singleton right at the end of the file,
//
ready to use on a page which references the $.js file
$ = new $();

最后,我將提供整個類庫完整實現,有興趣的朋友可以下載完整版壓縮版並擴展加以利用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM