创建你的第一个JavaScript库

是否曾对Mootools的魔力感到惊奇?是否有想知道Dojo如何做到那样的?是否对jQuery感到好奇?在这个教程中,我们将了解它们背后的东西并且动手创建一个超级简单的你最喜欢的库。

  我们其乎每天都在使用JavaScript库。当你刚入门时,利用jQuery是一件非常奇妙的事,主要是因为它的DOM操作。首先,DOM对于入门者来说可能是相对困难的事情;其次用它我们几乎可以不用考虑跨浏览器兼容的问题。

  在这个教程中,我们将试着从头开始实现一个很简单的库。是的,它非常有意思,但是在你高兴之前让我申明几点:

  • 这不会是全功能的库。我们有很多方法要写,但是它不是jQuery。我们将会做足工作来让你感受到在你创建一个库时会遇到的各种问题。
  • 我们不会完全解决所有浏览器的兼容性问题。我们写的代码能支持IE8+,Firefox 5+,Opera 10+,Chrome和Safari。
  • 我们不会覆盖使用我们库的所有可能性。比如我们的append和prepend方法只在你传入一个我们库的实例时才有效,它们不支持原生的DOM节点或节点集合。

 步骤1: 创建库样板文件Creating the Library Boilerplate

  我们以一些封装代码开始,它将会包含我们整个库。它就是你经常用到的立即执行函数表达式

window.dome = (function () {

    function Dome (els) {

    }

    var dome = {

        get: function (selector) {

        }

    };

    return dome;

}());

  如你所见,我们把我们的库叫Dome,因为它主要就是一个针对DOM的库,是的,它很不完整。

  到此我们做了两件事。首先,我们定义了一个函数,它最终会是实例化我们库的构造函数,这些对象将会封装我们选择或创建的元素。

  接下来我们创建了dome对象,它是我们实际的库对象;你能看到,它在最后被返回。它有一个空的get函数,我们将用它来从页面中选择元素。所以,让我们现在来填充它的代码。


 步骤2: 获取元素

  dome.get函数传入一个参数,但是它可以有好几种情况。如果它是一个字符串,我们假定它是一个CSS选择器;但是我们也可以传入单个DOM节点或是一个NodeList。

get: function (selector) {

    var els;

    if (typeof selector === "string") {

        els = document.querySelectorAll(selector);

    } else if (selector.length) {

        els = selector;

    } else {

        els = [selector];

    }

    return new Dome(els);

}

  我们使用document.querySelectorAll来简化元素的查找:当然这有浏览器兼容性问题,但是对于我们的例子来说它是ok的。如果 selector不是字符串,我们将检查它的length属性。如果它存在,我们就知道它是一个NodeList;否则它是单个元素然后我们将它放到一个数组中。这就是我们下面需要将调用Dome的结果传给一个数组的原因;你可以看到我们返回一个新的Dome对象。所以让我们回头看看Dome函数并填充它。


 步骤3: 创建Dome实例

  下面是Dome函数:

function Dome (els) {

    for(var i = 0; i < els.length; i++ ) {

        this[i] = els[i];

    }

    this.length = els.length;

}

  它确实很简单:我们只是遍历我们选择的元素并把它们附到带有数字索引的新对象中。然后我们添加一个length属性。

  但是这的关键是什么呢?为什么不直接返回元素?我们将元素封装到一个对象因为我们想为这个对象创建方法;这些方法可以让我们与这些元素交互。这实际上就是jQuery采用的方法的简化版本。

  所以,我们返回了Dome对象,让我们在它的原型上添加一些方法。我把这些方法直接写在Dome函数中。


 步骤4: 添加一些常用工具函数

  我们要写的第一个方法是一个简单的工具函数。因为我们的Dome对象可以封装多个DOM元素,几乎每个方法都需要遍历每个元素;所以,这些工具函数会非常便利。

  让我们以一个map函数开始:

Dome.prototype.map = function (callback) {

    var results = [], i = 0;

    for ( ; i < this.length; i++) {

        results.push(callback.call(this, this[i], i));

    }

    return results;

};

  当然,map函数传入单个参数,一个回调函数。我们遍历数组中的每一项,收集回调函数返回的所有内容放到results数组中。注意我们如何调用回调函数:

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

  这样函数就会在我们的Dome实例的上下文中被调用,它接受两个参数:当前元素,以及索引号。

  我们也想要一个forEach函数。它确实非常简单:

Dome.prototype.forEach(callback) {

    this.map(callback);

    return this;

};

  map和forEach间的唯一区别是map需要返回一些东西,因此我们也可以只传入我们的回调函数给this.map并忽略返回的数组,我们将返回 this来使得我们的库支持链式操作。我们将经常使用forEach。所以,注意当返回我们的this.forEach对函数的调用时,我们事实上是返回了this。例如,下面的方法实际上返回相同的东西:

Dome.prototype.someMethod1 = function (callback) {

    this.forEach(callback);

    return this;

};

Dome.prototype.someMethod2 = function (callback) {

    return this.forEach(callback);

};

  另外:mapOne。很容易看出这个函数是干什么的,但是问题是为什么我们需要它?它需要一些你可以叫做“库哲学”的东西来解释。

 一个简单的“哲学的”迂回

  如果创建一个库只是写代码,那就不是什么难的工作了。但是我正在做这个项目,我发现困难的部分是决定一些方法应该如何工作。

  很快,我们将建一个text方法,它返回我们选择元素的文本。如果我们的Dome对象封装几个DOM节点(如dome.get("li")),它会返回什么呢?如果你在jQuery做类似的事情($("li").text()),你将会得到一个所有元素的文本拼起来的字符串。它有用吗?我认为没用,但是我不知道更好的返回是什么。

  在这个项目中,我将以数组形式返回多个元素的文本,除非数组中只有一个元素,那我们就返回一个文本字符串,而不是只有一个元素的数组。我想你最常用的是获取单个元素的文本,所以我们对这个情况进行优化。然而,如果你获取多个元素的文本,我们也会返回一些你能操作的东西。

 回到代码

  所以,mapOne方法只是简单的运行map,然后要么返回数组,要么返回单元素数组中的元素。如果你还是不确定这有什么用,等一会你会发现的!

Dome.prototype.mapOne = function (callback) {

    var m = this.map(callback);

    return m.length > 1 ? m : m[0];

};

 步骤5: 处理文本和HTML

  接下来,让我们添加text方法。就像jQuery一样,我们可以给它传入一个字符串并设置元素的文本,或不传参数来获取元素的文本。

Dome.prototype.text = function (text) {

    if (typeof text !== "undefined") {

        return this.forEach(function (el) {

            el.innerText = text;

        });

    } else {

        return this.mapOne(function (el) {

            return el.innerText;

        });

    }

};
Dome.prototype.text = function (text) {

    if (typeof text !== "undefined") {

        return this.forEach(function (el) {

            el.innerText = text;

        });

    } else {

        return this.mapOne(function (el) {

            return el.innerText;

        });

    }

};

  你可能也想到了,我们需要检查text的值来看它是要设置还是要获取。注意如果只是用if(text)会有问题,因为空字符串会被判断为false。

  如果我们在设置值,我们将对元素调用forEach并且设置它们的innerText属性为text。如果我们要获取,我们将返回元素的 innerText属性。注意我们使用mapOne方法:如果我们在处理多个元素,它将返回一个数组,否则它将就是一个字符串。

  html方法几乎与text一样,除了它使用innerHTML属性而不是innerText。

Dome.prototype.html = function (html) {

    if (typeof html !== "undefined") {

        this.forEach(function (el) {

            el.innerHTML = html;

        });

        return this;

    } else {

        return this.mapOne(function (el) {

            return el.innerHTML;

        });

    }

};

  就像我说的:几乎完全一样。


 步骤6: 调整样式

  再接下来,我们希望能添加和删除样式,因此让我们来写一个addClass和removeClass方法。

  我们的addClass方法将接收一个字符串或是样式名称的数组。为了做到这点,我们需要检查参数的类型。如果是数组,我们将遍历它并创建一个样式名的字符串。否则,我们就简单的在样式名前加一个空格,这样它就不会和元素已有的样式混在一些。然后我们遍历元素并且将新的样式附加到className属性后面。

Dome.prototype.addClass = function (classes) {

    var className = "";

    if (typeof classes !== "string") {

        for (var i = 0; i < classes.length; i++) {

            className += " " + classes[i];

        }

    } else {

        className = " " + classes;

    }

    return this.forEach(function (el) {

        el.className += className;

    });

};

  很直接,对吗?

  那如何删除样式呢?为了保持简单,我们只允许一次删除一个样式。

Dome.prototype.removeClass = function (clazz) {

    return this.forEach(function (el) {

        var cs = el.className.split(" "), i;

        while ( (i = cs.indexOf(clazz)) > -1) {

            cs = cs.slice(0, i).concat(cs.slice(++i));

        }

        el.className = cs.join(" ");

    });

};

  对每个元素,我们将el.className分隔成一个数组。然后,我们使用一个while循环来剔除我们传入的样式,直到 cs.indexOf(clazz)返回-1。我们这样做是为了处理同样的样式在一个元素中出现的不止一次的特殊情况:我们必须保证它真的被删除了。一旦我们确保删除每个样式的实例,我们用空格连接数组的每一项并把它设置到el.className。


 步骤7: 修正一个IE的Bug

  我们正在处理的最糟糕的浏览器是IE8。在我们的小小的库中,只有一个IE bug需要我们处理,很幸运它很简单。IE8不支持Array的indexOf方法;我们在removeClass中使用到它,所以让我们修复它:

if (typeof Array.prototype.indexOf !== "function") {

    Array.prototype.indexOf = function (item) {

        for(var i = 0; i < this.length; i++) {

            if (this[i] === item) {

                return i;

            }

        }

        return -1;

    };

}

  它非常简单,并且这不是一个完全的实现(不支持第二个参数),但是能达到我们的目的。


 步骤8: 调节属性

  现在,我们想要一个attr函数。这很容易,因为它与我们的text或html方法非常类似。像那些方法一样,我们能够获取或设置属性值:我们可以传入元素名和值来设置,也可以只传入属性名来获取。

Dome.prototype.attr = function (attr, val) {

    if (typeof val !== "undefined") {

        return this.forEach(function(el) {

            el.setAttribute(attr, val);

        });

    } else {

        return this.mapOne(function (el) {

            return el.getAttribute(attr);

        });

    }

};

  如果val有一个值,我们将遍历这些元素并且将选择的属性设置为这个值,使用元素的setAttribute方法。否则,我们使用mapOne通过getAttribute方法来返回属性值。


 步骤9: 创建元素

  像很多好的库一样,我们应该能够创建新的元素。当然它作为一个Dome实例的一个方法不是很好,所以让我们直接把它挂到dome对象上去。

var dome = {

    // get method here

    create: function (tagName, attrs) {

    }

};

  你已经看到,我们使用两个参数:元素的名字,和属性值对象。大部分属性能过attr方法赋值,但是两种方法可以做特殊处理。我们使用addClass 方法操作className属性,以及text方法操作text属性。当然,我们首先需要创建元素和Dome对象。下面是整个操作的代码:

create: function (tagName, attrs) {

    var el = new Dome([document.createElement(tagName)]);

    if (attrs) {

        if (attrs.className) {

            el.addClass(attrs.className);

            delete attrs.className;

        }

        if (attrs.text) {

            el.text(attrs.text);

            delete attrs.text;

        }

        for (var key in attrs) {

            if (attrs.hasOwnProperty(key)) {

                el.attr(key, attrs[key]);

            }

        }

    }

    return el;

}

  我们创建元素并将它传给一个新的Dome对象。然后中我们处理属性。注意在操作完它们后我们必须删除className和text属性。这样可以避免当我们在attrs中遍历剩下的key值时被应用为属性。当然我们最后要返回这个新建的Dome对象。

  但是现在只是创建了新的元素,我们希望把它插入到DOM中对吗?


 步骤10: 附加元素

  下一步,我们将写append和prepend方法。这些确实是有点难搞的函数,主要是因为有很多种使用情况。以下是我们希望能做到的:

dome1.append(dome2);

dome1.prepend(dome2);

  使用情况如下:我们可能想要append或prepend

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

  注意:我使用“新”来表示元素还没有在DOM中;已存在的元素是已经在DOM中有的。

  让我们一步一步来:

Dome.prototype.append = function (els) {

    this.forEach(function (parEl, i) {

        els.forEach(function (childEl) {

        });

    });

};

  我们期望els参数是一个Dome对象。一个完整的DOM库可以接受一个节点或nodelist作为参数,但是我们暂时不这样做。我们必须遍历我们每一个元素,并且在它里面,我们还要遍历每个我们需要append的元素。

  如果我们将els到多个元素,我们需要克隆它们。然而,我们不想在他们第一次被附加的时候克隆节点,而时随后再说。所以我们这样:

if (i > 0) {

    childEl = childEl.cloneNode(true);

}

  这个i来自外层的forEach循环:它是当前父元素的索引。如果我们不是附加到第一个父元素,我们将克隆节点。这样,真正的节点将会放到第一个父节点中,其它父节点将获得一个拷贝。这样很好用,因为传入的Dome对象将只会拥有原始的节点。所以如果我们只是附加单个元素到单个元素,使用的所有节点都将是各自Dome对象的一部分。

  最后,我们终于可以附加元素:

parEl.appendChild(childEl);

  所以,汇总起来是这样

Dome.prototype.append = function (els) {

    return this.forEach(function (parEl, i) {

        els.forEach(function (childEl) {

            if (i > 0) {

                childEl = childEl.cloneNode(true);

            }

            parEl.appendChild(childEl);

        });

    });

};
 prepend方法

  我们想要prepend方法也满足同样的情况,所以这个方法非常类似:

Dome.prototype.prepend = function (els) {

    return this.forEach(function (parEl, i) {

        for (var j = els.length -1; j > -1; j--) {

            childEl = (i > 0) ? els[j].cloneNode(true) : els[j];

            parEl.insertBefore(childEl, parEl.firstChild);

        }

    });

};

  当prepend时所不同的是如果你顺次prepend一系列元素到另外一个元素时,它们是倒序的。因为我们不能反向forEach,我将使用for循环反向遍历。同样,我们将克隆节点如果它不是我们第一个要附件到的父节点。


 步骤11: 移除节点

  对于我们最后一个节点处理方法,我们想要从DOM中删除节点。其实很简单:

Dome.prototype.remove = function () {

    return this.forEach(function (el) {

        return el.parentNode.removeChild(el);

    });

};

  就是遍历节点并在每个元素的parentNode上调用removeChild方法。这里漂亮的地方在于这个Dome对象还将正常工作;我们可以在它上面使用任何方法,包括重新放回到DOM中去。


 步骤12: 处理事件

  最后,但是肯定不是用得最少的,我们将写一些函数处理事件。你可以知道,IE8使用老式的IE事件,所以我们需要检查它。同时,我们将抛出DOM 0事件,就因为我们可以。

签出方法,然后中我们将讨论它:

Dome.prototype.on = (function () {

    if (document.addEventListener) {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.addEventListener(evt, fn, false);

            });

        };

    } else if (document.attachEvent)  {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.attachEvent("on" + evt, fn);

            });

        };

    } else {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el["on" + evt] = fn;

            });

        };

    }

}());

  在这,我们使用了一个立即执行函数表达式,在函数里面我们做了特征检查。如果document.addEventListener存在,我们将使用它;否则我们检查document.attachEvent或者求助于DOM 0事件。注意我们如何返回最后的函数:它将在结束时被赋给Dome.prototype.on。当做特征检测时,非常方便地像这样赋给合适的函数,而不是每次函数运行时都得检查一次。

  off函数用于卸载事件,它与前面非常类似。

Dome.prototype.off = (function () {

    if (document.removeEventListener) {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.removeEventListener(evt, fn, false);

            });

        };

    } else if (document.detachEvent)  {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el.detachEvent("on" + evt, fn);

            });

        };

    } else {

        return function (evt, fn) {

            return this.forEach(function (el) {

                el["on" + evt] = null;

            });

        };

    }

}());

 就是这样!

  我希望你能试一试我们的小小的库,并且能稍稍扩展一点点。

  让我再申明一下,这个教程的目的不是说建议你总是要写一个自己的库。

  有专业的团队在做一个庞大的,稳定的越来越好的库。这里我们只是想让大家看看一个库内部是什么样子的,希望你能在这学到一些东西。

本文转载自:http://code.tutsplus.com/tutorials/build-your-first-javascript-library--net-26796

时间: 2024-11-26 02:02:04

创建你的第一个JavaScript库的相关文章

优雅的创建一个JavaScript库

这篇文章的目的是通过演示一个简单的例子来介绍在JS中实例化和定义一个库的正确方法,以优化他人编写或维护自己的JS库. 在我们深入之前,我做了两点假设: 你知道简单的JavaScript或C语言. 你不打算使用jQuery.通常情况下,一个JavaScript库不需要任何依赖. 首先,我遇到了第一个麻烦,即如何正确的看待一个JavaScript库.在C/C++中,一个库是功能的集合,并且通常不需要很完美的结构.而JavaScript的工作方式有所不同,因此我做了一些研究.最后的结论是,一个Java

基于Grunt构建一个JavaScript库

现在公认的JavaScript典型项目需要运行单元测试,合并压缩.有些还会使用代码生成器,代码样式检查或其他构建工具. Grunt.js是一个开源工具,可以帮助你完成上面的所有步骤.它非常容易扩展,并使用JavaScript书写,所以任何为JavaScript库或项目工作的人都可以按自己的需要扩展它. 本文解释如何使用Grunt.js构建JavaScript库.Grunt.js依赖Node.js和npm,所以第一节解释其是什么,如何安装和使用.如果你对npm有了解,那你可以跳过这一节.第四和第五

开发者必备的 12 个 JavaScript 库

现在 web 设计是最有趣的了,做好 web 设计不仅要熟练使用 Javascript,css 和 html 等,还要有自己的创意设计.为了方便大家发挥自己的创意,就产生了很多 JS 框架,Node.js 扩展等等.有了这些工具,开发者们就能专注于创意设计了,而不用为某个功能而花费太多精力.这里我们介绍的是 12 个开发者们必备的 JavaScript 库,都是一些很基础功能很强大的库.有了这些库,开发者们可以节省很多时间,大大提高开发的效率,所以大家赶紧收藏起来吧:) 1) Headroom.

最新的jQuery插件和JavaScript库

每一个前端开发人员很清楚的重要性和功能的JavaScript库提供.它提供了一个简单的接口,用于构建快速动态的接口,而无需大量的代码. 谢谢你的超级从事jQuery开发者社区,人始终是创造新的和令人惊叹的东西,那里是吨伟大的jQuery插件和JavaScript库在那里,插件和图书馆,涵盖了各种各样的功能. 在这篇文章中我们已经编译列表的JavaScript库为2015年7月,在这篇综述中,我们已经覆盖特征丰富和互动的JavaScript库,为您提供多种功能,能为你建立有效和有用的Web应用提供

推荐15款制作 SVG 动画的 JavaScript 库

在当今时代,SVG是最流行的和正在被众多的设计人员和开发人员使用,创建支持视网膜和响应式的网页设计.绘制SVG不是一个艰巨的任务,因为大量的 JavaScript 库可与 SVG 图像搭配使用.这些JS库帮助设计师和开发人员可以轻松地为他们的项目和Web应用程序创建创新和逼真的图形. 1.Textures.js Textures.js易于改进的数据可视化添加SVG图形.它包括一个巨大的各种纹理,包括直线,圆,路径,甚至自定义模式. 在线预览 2.Circulus.svg Circulus.svg

Baffle.js – 用于实现文本模糊效果的 JavaScript 库

Baffle.js 是一个 JavaScript 库,设计用来模糊和揭开DOM元素的文本. 这些元素可以是一个 CSS 选择器的形式.一个节点列表或者一个单节点. 你也可以传递一个选择对象给插件. 在线演示      立即下载 您可能感兴趣的相关文章 网站开发中很有用的 jQuery 效果[附源码] 分享35个让人惊讶的 CSS3 动画效果演示 十分惊艳的8个 HTML5 & JavaScript 特效 Web 开发中很实用的10个效果[源码下载] 12款经典的白富美型 jQuery 图片轮播插

【转载】写一个js库需要怎样的知识储备和技术程度?

作者:小爝链接:https://www.zhihu.com/question/30274750/answer/118846177来源:知乎著作权归作者所有,转载请联系作者获得授权. 1,如何编写健壮的javascript代码,鲁棒性,简单总结几条我觉得是常识的事:1.1 一个javascript库最好的实现方式是占用最少的命名空间,比如window对象上或者global对象上只占用一个引用.1.2 健壮的js程序对输入都会有完善的类型检查和异常处理,边界值的判断.1.3 对js的几种继承方式要足

获取地理信息的JavaScript 库 -- YQL Geo

本文转自:http://www.iteye.com/news/13781 YQL Geo 是一个 JavaScript 库用来根据地名获取经纬度,或者根据经纬度获取对应的地名等…… 得到当前地址的IP号: yqlgeo.get('217.12.14.240',function(o){ alert(o.place.name + ',' + o.place.country.content + ' (' + o.place.centroid.latitude + ',' + o.place.centr

GifShot - 创建动态 GIF 的 JavaScript 库

GifShot 是一个可以创建流媒体,视频或图像的 GIF 动画的 JavaScript 库.该库的客户端特性使其非常便携,易于集成到几乎任何网站.利用最先进的浏览器 API ,包括 WebRTC ,文件系统,视频,Canvas,Web Workers 和 Base 64 编码,支持超过20个选项. 效果演示     插件下载 您可能感兴趣的相关文章 Web 开发中很实用的10个效果[源码下载] 精心挑选的优秀jQuery Ajax分页插件和教程 12个让人惊叹的的创意的 404 错误页面设计