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 $();

最后,我将提供整个类库完整实现,有兴趣的朋友可以下载完整版压缩版并扩展加以利用。

posted @ 2012-03-02 17:49  Miracle He  阅读(6173)  评论(8编辑  收藏  举报