首页 web前端 js教程 超赞的动手创建JavaScript框架的详细教程_javascript技巧

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

May 16, 2016 pm 03:52 PM
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 &#63; 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) &#63; 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;
   });
  };
 }
}());
登录后复制

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
2 周前 By 尊渡假赌尊渡假赌尊渡假赌
仓库:如何复兴队友
4 周前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒险:如何获得巨型种子
4 周前 By 尊渡假赌尊渡假赌尊渡假赌

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

如何评估Java框架商业支持的性价比 如何评估Java框架商业支持的性价比 Jun 05, 2024 pm 05:25 PM

评估Java框架商业支持的性价比涉及以下步骤:确定所需的保障级别和服务水平协议(SLA)保证。研究支持团队的经验和专业知识。考虑附加服务,如升级、故障排除和性能优化。权衡商业支持成本与风险缓解和提高效率。

PHP 框架的轻量级选项如何影响应用程序性能? PHP 框架的轻量级选项如何影响应用程序性能? Jun 06, 2024 am 10:53 AM

轻量级PHP框架通过小体积和低资源消耗提升应用程序性能。其特点包括:体积小,启动快,内存占用低提升响应速度和吞吐量,降低资源消耗实战案例:SlimFramework创建RESTAPI,仅500KB,高响应性、高吞吐量

golang框架文档最佳实践 golang框架文档最佳实践 Jun 04, 2024 pm 05:00 PM

编写清晰全面的文档对于Golang框架至关重要。最佳实践包括:遵循既定文档风格,例如Google的Go编码风格指南。使用清晰的组织结构,包括标题、子标题和列表,并提供导航。提供全面准确的信息,包括入门指南、API参考和概念。使用代码示例说明概念和使用方法。保持文档更新,跟踪更改并记录新功能。提供支持和社区资源,例如GitHub问题和论坛。创建实际案例,如API文档。

如何为不同的应用场景选择最佳的golang框架 如何为不同的应用场景选择最佳的golang框架 Jun 05, 2024 pm 04:05 PM

根据应用场景选择最佳Go框架:考虑应用类型、语言特性、性能需求、生态系统。常见Go框架:Gin(Web应用)、Echo(Web服务)、Fiber(高吞吐量)、gorm(ORM)、fasthttp(速度)。实战案例:构建RESTAPI(Fiber),与数据库交互(gorm)。选择框架:性能关键选fasthttp,灵活Web应用选Gin/Echo,数据库交互选gorm。

PHP 框架的学习曲线与其他语言框架相比如何? PHP 框架的学习曲线与其他语言框架相比如何? Jun 06, 2024 pm 12:41 PM

PHP框架的学习曲线取决于语言熟练度、框架复杂性、文档质量和社区支持。与Python框架相比,PHP框架的学习曲线更高,而与Ruby框架相比,则较低。与Java框架相比,PHP框架的学习曲线中等,但入门时间较短。

Java框架的性能比较 Java框架的性能比较 Jun 04, 2024 pm 03:56 PM

根据基准测试,对于小型、高性能应用程序,Quarkus(快速启动、低内存)或Micronaut(TechEmpower优异)是理想选择。SpringBoot适用于大型、全栈应用程序,但启动时间和内存占用稍慢。

golang框架开发实战详解:问题答疑 golang框架开发实战详解:问题答疑 Jun 06, 2024 am 10:57 AM

在Go框架开发中,常见的挑战及其解决方案是:错误处理:利用errors包进行管理,并使用中间件集中处理错误。身份验证和授权:集成第三方库并创建自定义中间件来检查凭据。并发处理:利用goroutine、互斥锁和通道来控制资源访问。单元测试:使用gotest包,模拟和存根进行隔离,并使用代码覆盖率工具确保充分性。部署和监控:使用Docker容器打包部署,设置数据备份,通过日志记录和监控工具跟踪性能和错误。

golang框架性能比较:做出明智选择的指标 golang框架性能比较:做出明智选择的指标 Jun 05, 2024 pm 10:02 PM

选择Go框架时,关键性能指标(KPI)包括:响应时间、吞吐量、并发能力和资源使用。通过基准测试和比较框架的KPI,开发人员可以根据应用程序需求进行明智的选择,考虑预期负载、性能关键部分和资源限制。

See all articles