Javascript高级技术篇(1):搭建JS框架类库
2012-03-02 17:49
531 查看
经过了"面向对象的Javascript系列"的预热,让我们再次起航进入Javascript富客户端系列。基于链式调用关于类库的讲解,本讲将一步一步搭建一个属于自己的JS框架类库。在这里,不妨问问大家:一个类库怎样才能算有价值的类库呢?我想不妨从以下几个方面去考量:
1). 避免改变JS固有的基础对象。即如对JS对象Function,String,Array等,不要试图改变这些对象的行为来适应你的场景。
2). 具有良好的版本控制和文档注释。即JS类库必须有详细的文档,以备使用者能更快的应用。
3). 具有规范命名空间的约定。即JS类库必须添加完整的命名空间,有利于开发者快速定位自己需要的功能。
4). 避免加入任何的业务代码。即不要将与你业务逻辑相关的代码添加到类库中。
5). 模块职责清晰,按需加载。即基础类库并不是一个超大库,而是按职责划分出来组合在一起的,在你需要的场景中仅加载所需要的类库。
6). ...........
可能还有很多,以上是一个优秀框架的基本特征。接下来我们开始搭建我们的JS框架类库。
1). 定义基础类库的类。
调用方式:
2). 重写window.onload事件。我们都知道一个标准的html包含<head>和<body>元素,通常JS开发人员习惯把所有的页面初始化代码放在window.onload中,但此事件是在整个页面(包含外部资源如图片,动画等)加载完毕后触发。这样对于大型的站点来说,它的图片量越大直接导致用户等待页面显示的事件越久。但幸运的是,W3C组织定义了"DOMContentLoaded"事件,在页面元素加载完毕之后和页面外部资源文件加载之前触发,这样用户不必再浪费时间去等待图片全部加载完毕后才能看到页面。不幸的是,有些浏览器(如IE6,7,8)并未实现该事件。因此,我们需要重写window.onload事件来解决这个问题。
调用方式:
3). 统一多浏览器事件触发机制。大多数浏览器的事件触发机制都差不多,如超链接点击或Form提交。但在IE6和IE7以及其它浏览器却不相同,不仅如此,在页面元素和属性之间也有差异,如获取鼠标位置等。
调用方式:
4). 加入AJAX异步加载机制。我们都知道Ajax的到来给JS带来了无限的活力,它拥有很多鲜明的特性,如无刷新,动态异步加载等。我们可不能丢掉这块"肥肉"了,赶快加入到我们的JS基础框架体系中把。
调用方式:
5). 建立工具类库。可封装许多常用的功能,使开发者能以更快捷的方式实现功能。
调用方式:
6). 处理元素样式CSS。我们一致提倡页面Html元素与样式分离,也就是说样式不应直接掺杂到页面中,要构建高性能且富有表现力的页面,我们可以动态添加或移除元素样式以更好的表现。
调用方式:
7). 快速定位DOM元素。改进JS获取元素的复杂性,提供更快捷的方式获取。
8). 可能还有很多的功能加入,并通过版本控制的方式。这里先到此为止,我们在最后初始化$类库。
最后,我将提供整个类库完整实现,有兴趣的朋友可以下载完整版和压缩版并扩展加以利用。
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¶meter2=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 $();
最后,我将提供整个类库完整实现,有兴趣的朋友可以下载完整版和压缩版并扩展加以利用。
相关文章推荐
- JavaScript的Backbone.js框架环境搭建及Hellow world示例
- JavaScript的Backbone.js框架环境搭建及Hellow world示例
- JS和Native交互之 - 运用JavaScriptCore框架进行交互
- 【Vue.js】渐进式 JavaScript 框架
- js创建数据共享接口——简化框架之间相互传值_javascript技巧_脚本之家
- JS小框架 fly javascript framework
- 【干货】利用MVC5+EF6搭建博客系统(四)(上)前后台页面布局页面实现,介绍使用的UI框架以及JS组件
- js-JavaScript高级程序设计学习笔记13
- JS框架与类库的区别
- 帮助你操作数字和处理数字格式的javascript类库 - Numeral.js
- Node.js利用Express框架搭建小型网站(上)
- 超酷的实时颜色数据跟踪javascript类库 - Tracking.js
- Ember.js 1.0 RC 发布,JavaScript 框架
- js-JavaScript高级程序设计学习笔记20
- 服务器集群管理框架高级应用示例(五):自动化测试框架搭建图示
- 【前端Js】高级加密解密标准AES加密(Javascript代码实现)
- 帮助你实现移动设备上的拖拽刷新功能的javascript类库 - hook.js
- JavaScript高级程序设计 阅读笔记(十八) js跨平台的事件
- 一个必用的javascript框架:underscore.js - wine的思考 - ITeye技术网站
- JavaScript高级程序设计(第3版)学习笔记5 js语句