zoukankan      html  css  js  c++  java
  • 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,
    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 $();

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

  • 相关阅读:
    Golang手动分页,按等份拆分数据
    GORM无法映射到结构体上
    VSCODE GOLANG运行多个服务
    解决,MAVEN
    Properties配置文件常见错误写法以及转义字符说明
    Pentaho Data Integration (PDI/Kettle)与Java版本支持关系
    MYSQL之读写分离搭建方案
    Windows下创建软件快速启动命令
    Sonar的一些使用总结
    使用SVG Path绘图
  • 原文地址:https://www.cnblogs.com/hmiinyu/p/2377581.html
Copyright © 2011-2022 走看看