原文:Build Your First JavaScript Library
你是否曾对魔幻般的Mootools感到惊奇,是否曾想知道Dojo的内部机制,亦是否曾好奇于jQuery的巧妙?在本课程,我们将去了解它背后的原理,并尝试动手去创建一个非常简单的库。
我们几乎每天都在使用JavaScript库。当你刚开始的时候,像用jQuery是非常爽的,主要是因为DOM。首先对新手来讲DOM是比较难操作的,因为它是非常简劣的API,其次它没有兼容所有浏览器。
在本课程,我们将从零开始创建一个库。感觉非常有趣吧,但你先不要激动,让我申明几点:
- 这不是功能完整的库。我们是有一套方法要写,但它不是jQuery,不过我们做的足够使你体验到你将来在创建库时会遇到的各种问题。
- 这个项目不会兼容所有浏览器。我们写的代码会在以下浏览器中正常运行:IE8+、Firefox 5+、Opera 10+、Chrome和Safari。
- 不会涵盖所有使用我们库的情况。例如,我们的append和prepend方法只能接受我们库的实例,如果传递它原生DOM节点或节点列表它不会执行。
- 我们也不会为这库写测试用例,因为在我第一次开发它的时候已经做了这个工作。你可以通过Github获得库和测试用例。
Step1 创建库样板
开始,我们写些封装代码,这代码将包含整个库。
window.dome = (function () { function Dome (els) { } var dome = { get: function (selector) { } }; return dome; }());
我们把库命名为Dome,因为他只主要是一个DOM库,是的,它并不完整。
这里我们做了两件事。首先我们命名了一个函数,它最终是我们库实例的构造函数;这些对象会封装我们选择或者创建的元素。
然后,我们定义了dome对象,它是我们真正的库对象;可以看到,它在最后被返回。这对象有一个空的get函数,它将用于从页面选择元素。现在,我们来填充它吧。
Step2 获取元素
dome.get()接收一个参数,它可以是各种类型的值。如果参数是字符串,我们假定它是css选择器。也可以接收单个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 不是字符串,我们检查它是否存在length属性,如果存在我们接收到的是NodeList,否则,我们接收到是单一的元素,我们会将其放入数组。这是因 为当我们调用底部 Dom时需要传递给它一个数组。可以看到,我们返回了一个新的Dome对象。现在我们返回到Dome函数,并填充它。
Step3 创建Dome实例
这是函数Dome:
function Dome (els) { for(var i = 0; i < els.length; i++ ) { this[i] = els[i]; } this.length = els.length; }
非常简单,我们遍历了选择的元素,并将它放入带有数字索引的新对象,然后给这个对象添加了length属性。
注意点,为什么不直接返回元素?我们把这些元素封装在对象里是因为想要能为对象创建方法,这些方法使我们可以和这些元素发生交互操作。
现在,返回了一个Dome对象,我们给它的原型添加些方法,我将这些方法写在Dome函数的正下方。
Step4 添加一些工具函数
首先,我们来添加一些简单的工具函数 ,由于我们的Dome对象可以包含多个Dom元素,我们几乎在每个方法中遍历每个元素,所以有了这些方法将非常方便。
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接收一个回调函数。我们将遍历数组中的每一项,将callback返回的任何值存入results数组,注意我们是如何调用callback的:
callback.call(this, this[i], i));
通过这种方式,回调函数将在Dome实例上下文中调用,它接收两个参数:元素和索引数。
我们还需要一个函数forEach:
Dome.prototype.forEach = function(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。很容易看出这个function是做什么的,但问题是,我们为什么需要它?这需要一些你可以称为"库哲学"的东西来解释。
简短的"哲学"绕道
如果创建一个库只是写代码,那并不是什么难事。但是我在做这项工程时,我发现艰难的是考虑这些方法的工作方式。
我 们马上要创建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]; };
step5处理Text和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; }); } };
如你可能预见的,在text里我们需要检验值来看看我们是要设置值还是获取值。注意不能写if(text),因为空字符串是一个错值。
如果我们要设置,我们会对每个元素执行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; }); } };
step6:调整 Class
下一步,我们想要添加和移除样式,所以让我们来写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循环去切割 出不合法的class直到 cs.indexOf('clazz)返回-1。我们这样做覆盖了边缘情况当同样的类曾被重复添加到一个元素里。一旦我们确定我们已经切割出样式的每个情 况,我们用空格连接这个数组,并把它设置到el.className。
step7:处理一处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; }; }
相当简单吧,它并不是完整的实现(不支持第二个参数),不过我们的目的达到了。
step8:调整属性
现在我们想要一个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方法设置所选属性为那个值。否则我j我们用mapOne通过getAttribute方法来返回那个属性值。
step9:创建元素
我们应该能创建新元素,像任何优秀的库一样。当然,把它作为一个Dome实例的方法不好,所以我们把他放到我们的dome对象里。
var dome = { // get method here create: function (tagName, attrs) { } };
你可以看到,我们要接收两个参数,一个是元素的名字,一个是属性对象。大部分属性能通过方法attr应用,但两个需要特殊处理。我们用方法addClass来处理className属性。当然我们首先需要创建元素和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中剩下的键时又被调用。当然,我们以返回新的Dome对象结束。
我们现在在创建新元素,我们想把他们插入到DOM,对吧?
step10:添加元素
接着,我们写append 和prepend方法。现在需要写一些巧妙的函数,主要因为有多种使用情况。这是我们想要能做到的:
dome1.append(dome2); dome1.prepend(dome2);
使用情况如下,我们可能想追加或向前添加
-- 一个新元素到一个或多个现有元素
-- 多个新元素到一个或多个现有元素
-- 一个现有元素到一个或多个现有元素
-- 多个现有元素到一个或多个现有元素
注意:我这里说的新是指不存在DOM的元素,现有元素是指已经在DOM的元素。
我们开始吧:
Dome.prototype.append = function (els) { this.forEach(function (parEl, i) { els.forEach(function (childEl) { }); }); };
我们希望参数els是一个Dome对象,一个完整的DOM库应该将它作为节点或节点列表接收,我们不那样做。我们循环每个元素,然后在那里面继续循环想要插入的每个元素。
如果我们想添加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); } }); };
向前添加的不同点是当你按需向前添加一列元素到另一个元素,他们会按相反的顺序结束。当然我们不能用forEach反过来。
step11:移除节点
我们最后的操作节点方法是希望能从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级事件。注意我们是如何从IIFE返回最终函数:在将其指派给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; }); }; } }());
结束
我希望你试试我们的小型库,甚至做些扩展。像我先前提到的,我已经将他传到Github,包括对以上代码的Jasmine单元测试,你可以frok他,运行它。
”再次申明:本课程的目的不是建议你总应该写自己的库。“
这里有专门的团队一起工作让它强大,使库建的尽可能强大。这里的要点只是提了几点可能在库里发生的,我希望你有所获得。
我建议你在你喜欢的一些库里研究。你会发现他们不再如此神秘,可能你已经想到。
【这是第一篇翻译文章,接着会继续翻译前端相关的一些有趣的文章,欢迎一起讨论。】
相关推荐
jQuery是一款广泛应用于Web开发的JavaScript库,它极大地简化了JavaScript的DOM操作、事件处理、动画设计以及Ajax交互。这个资料包是为初学者准备的,旨在帮助你们快速掌握jQuery的基本用法和最新特性。 **jQuery...
"用XMLHttpRequest和Struts实现AJAX(译)[转]" 这个标题揭示了本文的主题,即如何结合XMLHttpRequest对象(一种在JavaScript中实现异步数据通信的技术)与Struts(一个Java Web开发框架)来创建基于AJAX...
4. **模板和库**:Dreamweaver中的模板和库功能可以帮助你创建和管理重复使用的网页元素或布局,提高工作效率。你可以创建自己的模板,也可以使用预设的模板。 5. **响应式设计**:随着移动设备的普及,响应式设计...
第1章 成功应用程序的模式 1.1 设计模式释义 1.1.1 起源 1.1.2 必要性 1.1.3 有效性 1.1.4 局限性 1.2 设计原则 1.2.1 常见设计原则 1.2.2 S.O.L.I.D.设计原则 1.3 Fowler的企业设计模式 1.3.1 分层 ...
在网页设计和开发中,`div`元素是一个非常基础且重要的组成部分...在实际项目中,还可以结合CSS预处理器(如Sass或Less)、JavaScript库(如jQuery)或现代前端框架(如React、Vue或Angular)来更高效地处理这些任务。
在Laravel中,"积垢包"(可能是"Package"的误译)通常指的是第三方开发者创建的可重复使用的代码库,这些库可以通过Composer(PHP的依赖管理工具)安装,并通过服务提供者和服务绑定集成到Laravel项目中。...
在发行包(SWFUpload v2)中含有2个版本的Flash控件(swfupload_f8.swf 与wfupload_f9.swf),其中第一个版本拥有最佳的兼容性,但是为此损失了部分功能;而第二个版本提供了一些附加的功能但是损失了兼容性。 ...
标题中的“kodluyoruzilkrepo”很可能是指一个编程学习平台——Kodluyoruz(译为“我们正在编码”)的初学者项目仓库。在这个项目中,用户首次接触并实践了代码回购(Repository)的概念,这是在版本控制工具如Git中...
例如,你可以使用 `luaL_newstate` 创建一个 Lua 状态机,然后通过 `luaL_openlibs` 打开标准库。 标签 "lua C++" 显示了这个项目的核心技术栈,即 Lua 和 C++ 的结合。这种结合允许开发者利用 C++ 的高性能和系统...
5. `node_modules` - 可能包含了项目所依赖的第三方JavaScript库或框架。 6. `README.md` - 介绍项目背景、如何运行以及其它相关信息的Markdown文件。 7. `.gitignore` - 指定版本控制系统忽略的文件和目录。 8. `...
1. **AI翻译技术**:小译翻译的核心是基于深度学习的自然语言处理(NLP)模型,如Transformer或BERT等,这些模型经过大规模语料库训练,能够理解并生成高质量的翻译。了解这些模型的工作原理和优化方法,对于深入...
- **知识点**: 进入一个站点时看到的第一页往往称之为主页。 - **解析**: 正确。通常情况下,访问一个网站时首先看到的页面被称为主页。 - **正确理解**: 主页是网站的入口页面,通常包含了网站的主要导航和基本信息...
他还提到了“-перваяработы”(第一份工作)和“-Сайтрезюмеспортфолиоисертификатами”(带有简历、作品集和证书的网站),这表明这个网站不仅包含他的项目,还可能...
**拉纳React**,这个名字可能是对React.js的另一种译称,React是Facebook开发的一个用于构建用户界面的JavaScript库,尤其适合构建单页应用。它主要关注视图层,但也可以与Redux、MobX等库结合使用,实现数据管理和...
图形用户界面(GUI)设计是Java应用的一部分,Java提供了AWT和Swing库来创建交互式图形界面。Applet是Java的一种小型应用程序,通常嵌入到网页中,但随着Web技术的发展,现代Web开发更多使用JavaScript和HTML5。 ...
1. **创建WebView**:Cordova通过一个内置的WebView(如UIWebView或WKWebView)加载HTML、CSS和JavaScript代码,提供应用的用户界面。 2. **原生功能访问**:通过JavaScript接口调用Objective-C编写的插件,实现对...
2. 前端界面:利用HTML、CSS和JavaScript创建用户友好的界面,提供动态效果,提升用户体验。 3. PHP与数据库交互:PHP作为服务器端脚本语言,负责处理用户请求,与数据库进行数据交换,实现业务逻辑。 三、系统...
3. **克隆应用**:克隆应用通常是指创建一个现有应用的复制品,但可能包含自定义功能或改进。在这个项目中,开发者可能复刻了Google Fit的主要功能,比如数据记录、数据分析和同步,以展示React Native和TypeScript...
1. **Ion5**:这可能是指Ionic Framework的第五个主要版本。Ionic是一个开源的HTML5移动应用框架,它基于Angular(可能是指这里的"角度11",即Angular 11)和Web组件,用于构建跨平台的原生和 Progressive Web Apps ...