JavaScript 프레임워크 생성을 위한 멋진 실습 상세 튜토리얼_javascript 팁

WBOY
풀어 주다: 2016-05-16 15:52:09
원래의
1272명이 탐색했습니다.

Mootools가 정말 대단하다고 생각하시나요? 도장이 어떻게 구현되는지 알고 싶으신가요? JQuery 팁이 궁금하신가요? 이 튜토리얼에서는 프레임워크 뒤에 숨은 비밀을 살펴본 다음 여러분이 선호하는 프레임워크의 간단한 버전을 직접 구축해 보겠습니다.

우리는 거의 매일 다양한 JavaScript 프레임워크를 사용합니다. 처음 시작하면 편리한 DOM(Document Object Model) 조작을 통해 JQuery와 같은 작업이 기분 좋게 느껴집니다. 그 이유는 다음과 같습니다. 우선 DOM은 초보자가 이해하기에는 너무 어렵습니다. 물론 API가 이해하기 어렵다는 것은 좋은 일이 아닙니다. 둘째, 브라우저 간의 호환성 문제가 매우 고민스럽습니다.

  • 객체에 메소드를 추가할 수 있기를 원하기 때문에 요소를 객체로 래핑합니다.
  • 이 튜토리얼에서는 이러한 프레임워크 중 하나를 처음부터 구현해 보겠습니다. 예, 재미있을 것입니다. 하지만 너무 흥분하기 전에 몇 가지 사항을 명확히 하고 싶습니다.
  • 이는 완전한 기능을 갖춘 프레임워크는 아닙니다. 예, 작성할 내용이 많지만 JQuery도 아닙니다. 하지만 우리가 하려는 일은 실제로 프레임워크를 작성하는 것이 어떤 것인지 맛보게 해줄 것입니다.
  • 완전한 호환성을 보장할 의도는 없습니다. 우리가 작성할 프레임워크는 Internet Explorer 8, Firefox 5, Opera 10, Chrome 및 Safari에서 작동합니다.
  • 우리 프레임워크는 가능한 모든 기능을 다루지는 않습니다. 예를 들어, 추가 및 미리 추가 방법은 프레임워크의 인스턴스를 전달한 경우에만 작동하며 기본 DOM 노드 및 노드 목록은 사용하지 않습니다.

또한: 튜토리얼에서는 프레임워크에 대한 테스트 사례를 작성하지 않을 것이지만 처음 개발할 때 이미 작성했습니다. Github에서 프레임워크와 테스트 케이스 코드를 얻을 수 있습니다.


1단계: 프레임 템플릿 만들기

전체 프레임워크를 수용할 래퍼 코드부터 시작하겠습니다. 전형적인 즉시함수(IIFE)입니다.

window.dome = (function () {
 function Dome (els) {
 }
 var dome = {
  get: function (selector) {
  }
 };
 return dome;
}());
로그인 후 복사

보시다시피 저희 프레임워크는 기본 DOM 프레임워크이기 때문에 돔이라고 부릅니다. 예, 기본적으로 (lame은 "절름발이", "불완전함"을 의미하며 dom + lame은 돔과 같습니다).

이미 뭔가가 있습니다. 먼저 함수가 있습니다. 이는 프레임의 객체 인스턴스를 구성하는 생성자가 됩니다. 해당 객체에는 우리가 선택하고 생성한 요소가 포함됩니다.

그런 다음 프레임 개체인 돔 개체가 있는데, 이는 함수의 반환 값으로 함수 호출자에게 최종적으로 반환되는 것을 볼 수 있습니다(주석: window.dome에 할당됨). 페이지에서 요소를 선택하는 데 사용할 빈 get 함수도 있습니다. 그럼 코드를 채워보겠습니다.

2단계: 요소 가져오기

Dome의 get 함수에는 매개변수가 하나만 있지만 여러 가지가 있을 수 있습니다. 문자열인 경우 CSS(Cascading Style Sheets) 선택기라고 가정하지만 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을 사용하여 단순히 요소를 선택합니다. 물론 브라우저 호환성이 제한되지만 이 상황에서는 여전히 허용됩니다. 선택기가 문자열 유형이 아닌 경우 길이 속성을 확인합니다. 존재한다면 노드 목록을 얻는다는 것을 알 수 있습니다. 그렇지 않으면 단일 요소이므로 배열에 넣습니다. 이는 아래의 Dome에 배열을 전달하기 때문입니다. 새로운 Dome 객체를 반환한 것을 볼 수 있습니다. Dome 함수로 돌아가서 코드로 채워보겠습니다.

3단계: 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;
   });
  };
 }
}());
로그인 후 복사

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!