你是否曾惊叹于React的魔力?是否曾好奇Dojo是如何运作的?是否曾对jQuery的巧妙操作感到好奇?在本教程中,我们将潜入幕后,尝试构建一个超简化的jQuery版本。
我们几乎每天都在使用JavaScript库。无论是实现算法、提供API抽象还是操作DOM,库在大多数现代网站中都执行许多功能。
在本教程中,我们将尝试从头开始构建一个这样的库(当然,这是一个简化的版本)。我们将创建一个用于DOM操作的库,类似于jQuery。是的,这很有趣,但在你兴奋之前,让我澄清几点:
prepend
方法只在你传递给它们我们的库实例时才有效;它们不适用于原始DOM节点或节点列表。我们将从模块本身开始。我们将使用ECMAScript模块(ESM),这是一种在Web上导入和导出代码的现代方法。
export class Dome { constructor(selector) { } }
如你所见,我们导出一个名为Dome
的类,其构造函数将接受一个参数,但它可以是多种类型。如果它是一个字符串,我们将假设它是一个CSS选择器,但我们也可以接受单个DOM节点或document.querySelectorAll
的结果来简化元素查找。如果它具有length
属性,我们将知道我们拥有一个节点列表。我们将把这些元素存储在this.elements
中,Dome
对象可以包装多个DOM元素,我们几乎需要在每种方法中循环遍历每个元素,因此这些实用程序将非常方便。
让我们从一个map
函数开始,它接受一个参数,一个回调函数。我们将循环遍历数组中的项目,收集回调函数返回的内容,Dome
实例将接收两个参数:当前元素和索引号。
我们还需要一个forEach
方法,默认情况下,我们可以简单地将调用转发到mapOne
。很容易看出这个函数的作用,但真正的问题是,为什么我们需要它?这需要一点你可能称之为“库理念”的东西。
如果构建库只是编写代码,那将不是一项太难的工作。但在从事这个项目时,我发现更难的部分是决定某些方法应该如何工作。
很快,我们将构建一个Dome
对象,它包装了多个DOM节点($("li").text()
),你将得到一个包含所有元素文本连接在一起的单个字符串。这有用吗?我认为没有,但我不知道更好的返回值是什么。
对于这个项目,我将把多个元素的文本作为数组返回,除非数组中只有一个项目;然后我们只返回文本字符串,而不是包含单个项目的数组。我认为你最常获取单个元素的文本,所以我们对此情况进行了优化。但是,如果你正在获取多个元素的文本,我们将返回你可以使用的内容。
因此,mapOne
将首先调用map
,然后返回数组或数组中的单个项目。如果你仍然不确定这如何有用,请继续关注:你将看到!
mapOne(callback) { const m = this.map(callback); return m.length > 1 ? m : m[0]; };
接下来,让我们添加text
方法来查看我们是在设置还是获取。请注意,这只是遍历元素并设置它们的文本。如果我们正在获取,我们将返回元素的mapOne
方法:如果我们正在处理多个元素,这将返回一个数组;否则,它将只是一个字符串。
html
方法与text
方法几乎相同,只是它将使用innerHTML
。
html(html) { if (typeof html !== "undefined") { this.forEach(function (el) { el.innerHTML = html; }); return this; } else { return this.mapOne(function (el) { return el.innerHTML; }); } }
就像我说的:几乎相同。
接下来,我们要能够添加和删除类,所以让我们编写addClass
和removeClass
方法。
我们的addClass
方法将在每个元素上使用classList.add
方法。当传递字符串时,只添加该类,当传递数组时,我们将遍历数组并添加其中包含的所有类。
addClass(classes) { return this.forEach(function (el) { if (typeof classes !== "string") { for (const elClass of classes) { el.classList.add(elClass); } } else { el.classList.add(classes); } }); }
很简单,对吧?
现在,删除类呢?为此,你几乎要做同样的事情,只是使用classList.remove
方法。
接下来,让我们添加attr
函数。这将很容易,因为它与我们的html
方法几乎相同。像这些方法一样,我们将能够同时获取和设置属性:我们将接受一个属性名称和值来设置,只接受一个属性名称来获取。
attr(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
方法。否则,我们将使用getAttribute
方法。
我们应该能够创建新元素,任何好的库都可以做到这一点。当然,这作为Dome
类的方法是没有意义的。
export function create(tagName,attrs) { }
如你所见,我们将接受两个参数:元素的名称和属性对象。大多数属性将通过我们的attr
方法应用,文本内容将通过text
方法应用于Dome
对象。以下是所有这些的实际操作:
export function create(tagName, attrs) { let el = new Dome([document.createElement(tagName)]); if (attrs) { for (let key in attrs) { if (attrs.hasOwnProperty(key)) { el.attr(key, attrs[key]); } } } return el; }
如你所见,我们创建元素并将其直接发送到新的Dome
对象中。
但是现在我们正在创建新元素,我们将希望将它们插入到DOM中,对吧?
接下来,我们将编写append
和prepend
方法。这些函数有点棘手,主要是因为有多种用例。以下是我们想要能够做的事情:
dome1.append(dome2); dome1.prepend(dome2);
我们可能想要附加或前置:
我使用“新”来表示尚未在DOM中的元素;现有元素已在DOM中。让我们现在逐步讲解:
append(els) { }
我们期望els
是一个Dome
对象。一个完整的DOM库会将其作为节点或节点列表接受,但我们不会这样做。我们必须遍历我们的每个元素,然后在其中,我们遍历我们想要附加的每个元素。
如果我们正在附加,则来自作为参数传入的外部Dome
对象的i
将只包含原始(未克隆的)节点。因此,如果我们只将单个元素附加到单个元素,则所有涉及的节点都将是它们各自的prepend
方法的一部分。
为了完整起见,让我们添加一个remove
方法。这将非常简单,因为我们只需要使用removeChild
方法。为了使事情更简单,我们将使用forEach
循环反向遍历,我将使用removeChild
方法反向遍历循环,每个元素的Dome
对象仍然可以正常工作;我们可以使用任何我们想要的方法,包括将其附加或前置回DOM。不错,对吧?
最后但并非最不重要的是,我们将编写一些事件处理程序函数。
查看on
方法,然后我们将讨论它:
on(evt, fn) { return this.forEach(function (el) { el.addEventListener(evt, fn, false); }); }
这很简单。我们只需遍历元素并使用addEventListener
方法。off
函数(它取消挂钩事件处理程序)几乎相同:
off(evt, fn) { return this.forEach(function (el) { el.removeEventListener(evt, fn, false); }); }
要使用Dome
,只需将其放入脚本并导入它。
import {Dome, create} from "./dome.js"
从那里,你可以像这样使用它:
new Dome("li") ...
确保你导入它的脚本是ES模块。
我希望你能尝试一下我们的小型库,甚至可以扩展它一点。正如我前面提到的,我已经把它放在GitHub上了。随意分叉它,玩耍,并发送拉取请求。
让我再次澄清一下:本教程的目的并不是建议你应该总是编写自己的库。有专门的团队在共同努力,使大型的、成熟的库尽可能好。这里的目的是让你对库内部可能发生的事情有所了解;我希望你在这里学到了一些技巧。
我强烈建议你在你的一些最喜欢的库中四处挖掘。你会发现它们并不像你想象的那么神秘,而且你可能会学到很多东西。以下是一些不错的起点:
这篇文章已更新,其中包含Jacob Jackson的贡献。Jacob是一位网络开发者、技术作家、自由职业者和开源贡献者。
以上是构建您的第一个JavaScript库的详细内容。更多信息请关注PHP中文网其他相关文章!