超赞的动手创建JavaScript框架的详细教程

觉得Mootools不可思议?想知道Dojo是如何实现的?对JQuery的技巧感到好奇?在这篇教程里,我们将探寻框架背后的秘密,然后试着自己动手建立一个你所喜爱的框架的简易版本。

我们几乎每天都在使用各种各样的JavaScript框架。当你刚入门的时候,方便的DOM(文档对象模型)操作让你觉得JQuery这样的东西非常棒。这是因为:首先,对于新手来说DOM太难理解了;当然,对于一个API来说难以理解可不是什么好事。其次,浏览器间的兼容性问题非常令人困扰。

  • 我们将元素包装成对象是因为我们想要能够为对象添加方法。
  • 在这个教程里,我们将试着从头实现这些框架之一。是的,这会很有趣,不过在你太过兴奋前我要澄清几点:
  • 这不会是一个功能很完善的框架。的确,我们要写很多东西,但它还算不上JQuery。可是我们将要做的事情会让你体验到在真正编写框架的感觉。
  • 我们不打算保证全方位的兼容性。我们将要编写的框架能够在 Internet Explorer 8+、Firefox 5+、Opera 10+、Chrome和Safari上工作。
  • 我们的框架不会覆盖到所有可能的功能。比如说,我们的append和preappend方法只有在你传给它一个我们框架的实例时才能工作;我们不会用原生的DOM节点和节点列表。

另外:尽管在教程中我们不会为我们的框架编写测试用例,但是我已经在第一次开发它的时候做好了。你可以从 Github上获取框架和测试用例的代码。

第一步: 创建框架模板

我们将从一些包装代码开始,它将容纳我们的整个框架。这是典型的立即函数(IIFE).

window.dome = (function () {
 function Dome (els) {
 }
 var dome = {
  get: function (selector) {
  }
 };
 return dome;
}());

你可以看到,我们的框架叫做dome,因为它是一个基本的DOM框架。没错,基本(lame有“瘸子”、“不完整”的意思,dom加lame等于dome)的。

我们已经有了一些东西。 首先,我们有了一个函数;它将成为构造框架的对象实例的构造函数;那些对象将会包含我们选择和创建的元素。

然后,我们有了一个dome对象,它就是我们的框架对象;你可以看到它最终作为函数的返回值返回给了函数调用者(译注:赋值给了window.dome)。这里还有一个空的get函数,我们将用它从页面里选取元素。那么,我们来填充代码吧。

第二步: 获取元素

dome的get函数只有一个参数,但是它可以是很多东西。如果它一个string(字符串),我们将假定它是一个CSS(层叠样式表)选择器;不过我们也可能得到一个DOM节点或者DOM节点列表。

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来简单的选择元素:当然,这将限制我们的浏览器兼容性,不过对于这种情况还是可以接受的。如果selector不是string类型,我们将检查它的length属性。如果存在,我们就知道我们得到的是一个节点列表;否则,就是一个单独的元素,我们将它放到一个数组里。这是因为我们要在下面向Dome传递一个数组。你可以看到,我们返回了一个新的Dome对象。让我们回到Dome函数并且为它填充代码。

第三步: 创建Dome实例

这是Dome函数:

function Dome (els) {
 for(var i = 0; i < els.length; i++ ) {
  this[i] = els[i];
 }
 this.length = els.length;
}

我强烈建议你去深入研究一些你喜欢的框架

这非常简单:我们只是遍历了els的所有元素,并且把它们存储在一个以数字为索引的新对象里。然后我们添加了一个length属性。

但是这有什么意义呢?为什么不直接返回元素?因为:我们将元素包装成对象是因为我们想要能够为对象添加方法;这些方法能够让我们遍历这些元素。实际上这正是JQuery的解决方案的浓缩版。

我们的Dome对象已经返回了,现在让我们来为它的原型(prototype)添加一些方法。我会直接把那些方法写在Dome函数下面。

第四步:添加几个实用工具

要添加的第一批功能是些简单的工具函数。由于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函数有一个入参,一个回调函数。我们遍历Dome对象所有元素,收集回调函数的返回值到结果集中。注意我们是怎样调用回调函数的:

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

通过这种方式,函数将在Dome实例的上下文中被调用,并且函数接收到两个参数:当前元素和元素序号。

我们也想要一个foreach函数。事实上这很简单:

Dome.prototype.forEach(callback) {
 this.map(callback);
 return this;
};

由于map函数和foreach函数之间的不同仅仅是map需要返回些东西,我们可以仅仅将回调传给this.map然后忽略返回的数组;代替返回的是,我们将返回this,来使我们的库呈链式。foreach会被频繁的调用,所以,注意当一个函数的回调被返回,事实上,返回的是Dome实例。例如,下面的方法事实上就返回了Dome实例:

Dome.prototype.someMethod1 = function (callback) {
 this.forEach(callback);
 return this;
};
Dome.prototype.someMethod2 = function (callback) {
 return this.forEach(callback);
};

还有一个:mapOne。很容易就知道这个函数是做什么的,但是真正的问题是,为什么需要它?这就需要一些我们称之为"库哲学"的东西了。
一个简短的"哲学"阐释

  • 首先,对于一个初学者来说,DOM很让人纠结;它的API不完善。

如果构建一个库仅仅是写代码,那就不是什么难事。但是当我开发这个库时,我发现那些不完善的部分决定了一定数量的方法的实现方式。

很快,我们要去构建一个返回被选择元素的文本的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步: 处理Text和HTML

接着,让我们来添加文本方法。就像jQuery,我们可以传递一个string值,设置节点元素的text值,或者通过无参方法得到返回的text值。

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;
  });
 }
};

如你所料,当我们设置(setting)或者得到(getting)value值时,需要检查text的值。要注意的是如果justif(文本)方法不起作用,是因为text为空字符串是一个错误的值。

如果我们设置(setting)时,可是使用一个forEach 遍历元素,设置它们的innerText属性。如果我们得到(getting)时,返回元素的innerText属性。在使用mapOne方法是要注意:如果我们正在处理多个元素,将返回一个数组;其他的则还是一个字符串。

如果html方法使用innerHTML属性而不是innerText,它将会更优雅的处理涉及text文本的事情。

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;
  });
 }
};

就像我说过的:几乎相同的。

第六步: 修改类

下一步,我们想对class进行操作,所以添加能addClass()和removeClass()。addClass()的参数是一个class名称或者名称的数组。为了实现动态参数,我们需要对参数的类型进行判断。如果参数是一个数组,那么遍历这个数组,将元素添加上这些class名称,如果参数是一个字符串,则直接加上这个class名称。函数需要确保不将原来的class名称弄乱。

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;
 });
};

很直观吧?嘿嘿

现在,写下removeClass(),同样简单。不过每次只允许删除一个class名称。

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。我们将得到的结果join成el.className。

第七步: 修复一个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);
  });
 }
};

如果形参有一个值,我们将遍历元素并通过元素的setAttribute方法设置属性值。另外,我们将使用mapOne返回通过getAttribute方法得到参数。

第9步: 创建元素

像任何一个优秀的框架一样,我们也应该能够创建元素。当然,在Demo实例中没有一个好的方法,所以让我们来把方法加入到demo工程中。

var dome = {
 // get method here
 create: function (tagName, attrs) {
 }
};

正如你所看到的:我们需要两个形参:元素名,和一个参数对象。大多数的属性通过我们的arrt方法被使用,但是tagName和attrs却有特殊待遇。我们为className属性使用addClass方法,为text属性使用text方法。当然,我们首先要创建元素,和Demo对象。下面就是所有的作用:

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;
}

如上,我们创建了元素,将他发送到新的Dmoe对象中。接着,我们处理所有属性。注意:当使用完className和text属性后,我们不得不删除他们。这将保证当我们遍历其他的键时,它们还能被使用。当然,我们最终通过返回这个新的Demo对象。

我们创建了新的元素,我们想要将这些元素插入到DOM,对吧?

第10步:尾部添加(Appending)与头部添加(Prepending)元素

下一步,我们来实现尾部添加与头部添加方法。考虑到多种场景,实现这些方法可能有些棘手。下面是我们的想要达到的效果:

dome1.append(dome2);
dome1.prepend(dome2);

IE8对我们来说就是一奇葩。

尾部添加或头部添加,包括以下几种场景:

  • 单个新元素添加至单个或多个已存在元素中
  • 多个新元素添加至单个或多个已存在元素中
  • 单个已存在元素添加至单个或多个已存在元素中
  • 多个已存在元素添加至单个或多个已存在元素中

注意:这里的”新元素“表示还未加入DOM中节点元素,”已存在元素“指已存在于DOM中的节点元素。
现在让我们一步步来实现之:

Dome.prototype.append = function (els) {
 this.forEach(function (parEl, i) {
  els.forEach(function (childEl) {
  });
 });
};

假设参数els是一个DOM对象。一个功能完备的DOM库应该能处理节点(node)或节点序列(nodelist),但现在我们不作要求。首先遍历需要被添加进的元素 (父元素),再在这个循环中遍历将被添加的元素 (子元素)。
    如果将一个子元素添加至多个父元素,需要克隆子元素(避免最后一次操作会移除上一次添加操作)。可是,没必要在初次添加的时候就克隆,只需要在其它循环中克隆就可以了。因此处理如下:

if (i > 0) {
 childEl = childEl.cloneNode(true);
}

变量i来自外层forEach循环:它表示父级元素的序列号。第一个父元素添加的是子元素本身,而其他父元素添加的都是目标子元素的克隆。因为作为参数传入的子元素是未被克隆的,所以,当将单个子元素添加至单个父元素时,所有的节点都是可响应的。
最后,真正的添加元素操作:

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);
  }
 });
};

不同点在于添加多个元素时,添加后的顺序会被反转。所以不能采用forEach循环,而是用倒序的for循环代替。同样的,在添加至非第一个父元素时需克隆目标子元素。

第十一步: 删除节点

对于我们最后一个节点的操作方法,从dom中删除这些节点,很简单,只需要:


Dome.prototype.remove = function () {
 return this.forEach(function (el) {
  return el.parentNode.removeChild(el);
 });
};

只需要通过节点的迭代和在他们的父节点调用删除子节点方法。比较好的是这个dom对象依然正常工作(感谢文档对象模型吧)。我们可以在它上面使用我们想使用的方法,包括插入,预插回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;
   });
  };
 }
}());

在这里,我们用到了立即执行函数(IIFE),在函数内我们做了特性检测。如果document.addEventListener方法存在,我们就使用它;另外我们也检测document.attachEvent,如果没有就使用DOM 0级方法。请注意我们如何从立即执行函数中返回最终函数:其最后会被分配到Dome.prototype.on。在做特性检测时,与每次运行函数时检测相比,这样的方式分配适合的方法更加方便。

事件解绑方法off与on方法类似:.

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;
   });
  };
 }
}());
(0)

相关推荐

  • 10个新的最有前途的JavaScript框架

    1. SproutCore SproutCore 苹果对SproutCore的解释为"开源,平台无关,类Cocoa的JavaScript框架,用于创建具有桌面应用程序外观和操作感的Web应用程序." SproutCore demonstrations: Photos, Sample Controls 2. Spry Spry 是Adobe 的 Ajax framework. Spry的目的是成为实现Ajax的一种简单方式,对HTML.CSS和JavaScript体验据有入门级水平的设计

  • javascript函数库-集合框架

    Classes: Collections Arrays ArrayList SortedList extends ArrayList HashMap HashSet */ /**************** Collections NOTE:sort() return a new List ****************/ function Collections(){} Collections.sort=function(){ if(arguments.length==1){  var s=

  • JavaScript框架编程第1/2页

    使用JavaScript框架 在讲述 window 对象的时候,我们提到过,一个框架内的网页也是 window 对象,也就是说,Frame 对象也是 window 对象.用最容易理解的话说,每一个 HTML 文件占用一个 window 对象,包括定义框架的网页("框架网页").在 IE 里用"<iframe>"标记在文档中插入的框架也是 window 对象,但是用"包含网页"的方法(在 HTML 中显示为"<!--we

  • 超赞的动手创建JavaScript框架的详细教程

    觉得Mootools不可思议?想知道Dojo是如何实现的?对JQuery的技巧感到好奇?在这篇教程里,我们将探寻框架背后的秘密,然后试着自己动手建立一个你所喜爱的框架的简易版本. 我们几乎每天都在使用各种各样的JavaScript框架.当你刚入门的时候,方便的DOM(文档对象模型)操作让你觉得JQuery这样的东西非常棒.这是因为:首先,对于新手来说DOM太难理解了:当然,对于一个API来说难以理解可不是什么好事.其次,浏览器间的兼容性问题非常令人困扰. 我们将元素包装成对象是因为我们想要能够为

  • 使用IDEA搭建SSM框架的详细教程(spring + springMVC +MyBatis)

    1 框架组成 Spring SpringMVC MyBatis 2 所需工具 Mysql 8.0.15 ​数据库管理系统,创建数据库 Tomcat 8.5.51 ​用于部署web项目 Maven 3.6.1 ​项目构建.项目依赖管理 lombok 1.18.10(可用可不用工具) ​用于类注解创建setter.getter.无参构造.全参构造.toString等函数 ​注:只导入依赖,不安装插件是不起作用的 3 搭建步骤 3.1 新建一个空Maven项目,填写项目相关信息,完成 3.2 添加we

  • maven加入spring框架的详细教程

    使用步骤: 下载maven工具 maven官网 maven官网下载 2. 解压 3.设置代理 maven默认从国外网站下载jar包,很慢.可以使用阿里的代理. <mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>

  • 从零搭建SpringBoot2.X整合Redis框架的详细教程

    最近也不知道写啥,看之前写过Kafka整合Springboot的文章,大家反响还挺热烈的,嘿嘿嘿,就感觉帮助到大家了还挺好的,也算是达到了自己的目的,正好,今天业务模块是springboot整合redis,因为之前做过,所以有现成的代码,cv一下之后就可以了,所以时间比较多,那就给大家整理一下Springboot整合Redis的代码实现吧,从项目搭建到源码实现,下面全都有,耐心看完,相信会对你有所帮助的 好了,话不多说,我们开始吧,同样的,还是建议能够自己在自己的PC端实现一下 个人公众号:Ja

  • IDEA 2020.3.X 创建scala环境的详细教程

    IDEA 2020.3.X 创建scala环境(默认jdk,maven环境已配置) 一.查看idea中scala兼容版本 二.下载scala 官网下载:https://plugins.jetbrains.com/plugin/1347-scala 下载完成后不需解压 三.下载scala SDK 下载地址:https://www.scala-lang.org/download/ 下载完成后需要解压 四.等待下载成功后返回idea 手动安装: 选择刚刚下载的压缩包 点击ok,然后apply,ok即可

  • 在SpringBoot中整合使用Netty框架的详细教程

    Netty是一个非常优秀的Socket框架.如果需要在SpringBoot开发的app中,提供Socket服务,那么Netty是不错的选择. Netty与SpringBoot的整合,我想无非就是要整合几个地方 让netty跟springboot生命周期保持一致,同生共死 让netty能用上ioc中的Bean 让netty能读取到全局的配置 整合Netty,提供WebSocket服务 这里演示一个案例,在SpringBoot中使用Netty提供一个Websocket服务. servlet容器本身提

  • C#使用Thrift作为RPC框架入门详细教程

    前言 本文将介绍由 Facebook 开发的远程服务调用框架 Apache Thrift,它采用接口描述语言定义并创建服务,支持可扩展的跨语言服务开发,所包含的代码生成引擎可以在多种语言中,如 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, Smalltalk 等创建高效的.无缝的服务,其传输数据采用二进制格式,相对 XML 和 JSON 体积更小,对于高并发.大数据量和多语言的环境更有优势.本文将详细介绍 Thri

  • ASP.NET创建三层架构图解详细教程

    1.新建项目 2.创建Visual Studio解决方案 3.再创建项目 4.选择类库类型 5.依次创建bll(业务逻辑层),dal(数据访问层)和model(模型层也可以叫实体层) 6.添加一个网站 7.选择相应的类型 8.修改名称 9.设为启动项目 10.结构如下 11. 生成model 12.在dal中引用model 13.选择model引用 14.看一下 15.dal还可以引用其他类库,如DBUtility 16.数据库帮助类库 17.model不引用任何类库 18.底层类库在上层类库中

  • numpy创建神经网络框架

    目录 神经网络框架使用方法及设计思想 项目介绍 框架介绍 神经网络框架使用方法及设计思想 在框自己手写架上基本模仿pytorch,用以学习神经网络的基本算法,如前向传播.反向传播.各种层.各种激活函数 采用面向对象的思想进行编程,思路较为清晰 想要神经网络的同学们可以参考一下 代码大体框架较为清晰,但不否认存在丑陋的部分,以及对于pytorch的拙劣模仿 项目介绍 MINST_recognition: 手写数字识别,使用MINST数据集 训练30轮可以达到93%准确度,训练500轮左右达到95%

  • 创建与框架无关的JavaScript插件

    JavaScript 中的插件使我们能够扩展语言,以实现所需的某些强大(或不够强大)的功能.插件/库本质上是打包的代码,可以使我们免于一遍又一遍地编写相同的东西(功能). 在 JavaScript 生态系统中,有数百个框架,这些框架中的每一个都为我们提供了一个创建插件的系统,以便为框架添加新的东西. 如果你看一下 NPM 注册表,几乎所有的 JavaScript 插件都是在那里发布的,你会看到有超过一百万个插件以简单库和框架的形式发布. 为每个框架创建插件的方式可能会有很大不同.例如,Vue.j

随机推荐