Статьи

Создайте свою первую библиотеку JavaScript

Когда-нибудь восхищались магией Mootools ? Когда-нибудь задумывались, как это делает Додзё ? Вы когда-нибудь интересовались гимнастикой jQuery ? В этом уроке мы собираемся пробраться за кулисы и попробовать свои силы в создании супер-простой версии вашей любимой библиотеки.

Мы используем библиотеки JavaScript почти каждый день. Когда вы только начинаете, иметь что-то вроде jQuery – это здорово, в основном из-за DOM. Во-первых, DOM может быть довольно грубым для новичка; это довольно плохое оправдание для API. Во-вторых, это даже не одинаково для всех браузеров.

Мы оборачиваем элементы в объект, потому что хотим иметь возможность создавать методы для объекта.

В этом уроке мы собираемся сделать (решительно поверхностный) удар по созданию одной из этих библиотек с нуля. Да, это будет весело, но прежде чем вы будете слишком взволнованы, позвольте мне прояснить несколько моментов:

  • Это не будет полностью полнофункциональная библиотека. О, у нас есть солидный набор методов для написания, но это не jQuery. Мы сделаем достаточно, чтобы дать вам хорошее представление о проблемах, с которыми вы столкнетесь при создании библиотек.
  • Мы не собираемся обеспечивать полную совместимость с браузерами здесь. То, что мы пишем сегодня, должно работать в Internet Explorer 8+, Firefox 5+, Opera 10+, Chrome и Safari.
  • Мы не собираемся освещать все возможные варианты использования нашей библиотеки. Например, наши методы append и prepend будут работать, только если вы передадите им экземпляр нашей библиотеки; они не будут работать с необработанными узлами DOM или списками узлов.

Еще одна вещь: хотя мы не будем писать тесты для этой библиотеки, я сделал это при первой разработке. Вы можете получить библиотеку и тесты на Github .


Мы начнем с некоторого кода-обертки, который будет содержать всю нашу библиотеку. Это ваше типичное выражение, вызываемое сразу (IIFE) .

01
02
03
04
05
06
07
08
09
10
11
12
13
window.dome = (function () {
    function Dome (els) {
         
    }
     
    var dome = {
        get: function (selector) {
         
        }
    };
     
    return dome;
}());

Как видите, мы называем нашу библиотеку Dome, потому что это в первую очередь библиотека DOM. Да, это хромает.

У нас здесь происходит пара вещей. Во-первых, у нас есть функция; в конечном итоге это будет функция конструктора для экземпляров нашей библиотеки; эти объекты обернут наши выбранные или созданные элементы.

Затем у нас есть объект dome , который является фактическим библиотечным объектом; как вы видите, он вернулся в конце там. У него есть пустая функция get , которую мы будем использовать для выбора элементов на странице. Итак, давайте заполним это сейчас.


Функция dome.get принимает один параметр, но это может быть несколько вещей. Если это строка, мы предполагаем, что это селектор CSS; но мы также можем взять один DOM Node или NodeList.

01
02
03
04
05
06
07
08
09
10
11
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 ; в противном случае у нас есть один элемент, и мы поместим его в массив. Это потому, что нам нужен массив для передачи нашего вызова Dome внизу; как видите, мы возвращаем новый объект Dome . Итак, давайте вернемся к этой пустой функции Dome и заполните ее.


Вот эта функция Dome :

1
2
3
4
5
6
function Dome (els) {
    for(var i = 0; i < els.length; i++ ) {
        this[i] = els[i];
    }
    this.length = els.length;
}

Я действительно рекомендую вам покопаться в нескольких ваших любимых библиотеках.

Это действительно просто: мы просто перебираем выбранные элементы и прикрепляем их к новому объекту с числовыми индексами. Затем мы добавляем свойство length .

Но какой здесь смысл? Почему бы просто не вернуть элементы? Мы оборачиваем элементы в объект, потому что хотим иметь возможность создавать методы для объекта; это методы, которые позволят нам взаимодействовать с этими элементами. На самом деле это упрощенная версия того, как это делает jQuery.

Итак, теперь, когда у нас есть возвращаемый объект Dome , давайте добавим несколько методов к его прототипу. Я собираюсь поместить эти методы прямо под функцию Dome .


Первые функции, которые мы собираемся написать, это простые вспомогательные функции. Поскольку наши объекты Dome могут обернуть более одного элемента DOM, нам нужно будет зацикливаться на каждом элементе практически во всех методах; Итак, эти утилиты будут удобны.

Давайте начнем с функции map :

1
2
3
4
5
6
7
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 принимает один параметр, функцию обратного вызова. Мы будем перебирать элементы в массиве, собирая все, что возвращается из обратного вызова в массиве results . Обратите внимание, как мы вызываем эту функцию обратного вызова:

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

Делая это таким образом, функция будет вызываться в контексте нашего экземпляра Dome , и она получит два параметра: текущий элемент и индексный номер.

Нам также нужна функция forEach . Это на самом деле очень просто:

1
2
3
4
Dome.prototype.forEach(callback) {
    this.map(callback);
    return this;
};

Поскольку единственное различие между map и forEach заключается в том, что map должна что-то возвращать, мы можем просто передать наш обратный вызов this.map и проигнорировать возвращенный массив; вместо этого мы вернем this чтобы сделать нашу библиотеку цепью. Мы будем использовать forEach совсем немного. Итак, обратите внимание, что когда мы возвращаем наш вызов this.forEach из функции, мы фактически возвращаем this . Например, эти методы фактически возвращают одно и то же:

1
2
3
4
5
6
7
8
Dome.prototype.someMethod1 = function (callback) {
    this.forEach(callback);
    return this;
};
 
Dome.prototype.someMethod2 = function (callback) {
    return this.forEach(callback);
};

Еще один: mapOne . Легко увидеть, что делает эта функция, но реальный вопрос в том, зачем она нам нужна? Это требует немного того, что вы могли бы назвать «библиотечной философией».

Во-первых, DOM может быть довольно грубым для новичка; это довольно плохое оправдание для API.

Если бы создание библиотеки было просто написанием кода, это не было бы слишком сложной работой. Но когда я работал над этим проектом, я обнаружил, что сложнее было решить, как должны работать определенные методы.

Вскоре мы собираемся создать text метод, который возвращает текст наших выбранных элементов. Если наш объект Dome dome.get("li") несколько узлов DOM (например, dome.get("li") ), что это должно вернуть? Если вы сделаете нечто подобное в jQuery ( $("li").text() ), вы получите одну строку с текстом всех элементов, соединенных вместе. Это полезно? Я так не думаю, но я не уверен, что будет лучшим возвращаемым значением.

Для этого проекта я верну текст нескольких элементов в виде массива, если в массиве нет только одного элемента; тогда мы просто вернем текстовую строку, а не массив с одним элементом. Я думаю, что вы чаще всего будете получать текст одного элемента, поэтому мы оптимизируем для этого случая. Однако, если вы получаете текст из нескольких элементов, мы вернем то, с чем вы можете работать.

Таким образом, метод mapOne просто запустит map , а затем либо вернет массив, либо единственный элемент, который был в массиве. Если вы все еще не уверены, насколько это полезно, останьтесь вокруг: вы увидите!

1
2
3
4
Dome.prototype.mapOne = function (callback) {
    var m = this.map(callback);
    return m.length > 1 ?
};

Далее давайте добавим этот text метод. Как и в случае с jQuery, мы можем передать ему строку и установить текст элемента или не использовать параметры для возврата текста.

01
02
03
04
05
06
07
08
09
10
11
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 .

01
02
03
04
05
06
07
08
09
10
11
12
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;
        });
    }
};

Как я уже сказал: почти идентично.


Далее мы хотим иметь возможность добавлять и удалять классы; так что давайте removeClass методы addClass и removeClass .

Наш метод addClass будет принимать либо строку, либо массив имен классов. Чтобы это работало, нам нужно проверить тип этого параметра. Если это массив, мы зациклим его и создадим строку с именами классов. В противном случае мы просто добавим один пробел в начале имени класса, чтобы он не мешал существующим классам в элементе. Затем мы просто зацикливаемся на элементах и ​​добавляем новые классы в свойство className .

01
02
03
04
05
06
07
08
09
10
11
12
13
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;
    });
};

Довольно просто, а?

Теперь, как насчет удаления классов? Для простоты мы будем разрешать удалять только один класс за раз.

01
02
03
04
05
06
07
08
09
10
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) класс-нарушитель, пока cs.indexOf(clazz) вернет -1. Мы делаем это, чтобы охватить крайний случай, когда одни и те же классы были добавлены к элементу более одного раза: нам нужно убедиться, что он действительно исчез. Убедившись, что мы вырезали каждый экземпляр класса, мы объединяем массив с пробелами и устанавливаем его в el.className .


Худший браузер, с которым мы имеем дело, это IE8. В нашей маленькой библиотеке есть только одна ошибка IE, с которой нам нужно иметь дело; К счастью, это довольно просто. IE8 не поддерживает метод Array indexOf ; мы используем его в removeClass , поэтому давайте заполним его:

01
02
03
04
05
06
07
08
09
10
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;
    };
}

Это довольно просто, и это не полная реализация (не поддерживает второй параметр), но это будет работать для наших целей.


Теперь нам нужна функция attr . Это будет легко, потому что он практически идентичен нашим методам text или html . Подобно этим методам, мы сможем как получить, так и установить атрибуты: мы возьмем имя и значение атрибута для установки и просто имя атрибута для получения.

01
02
03
04
05
06
07
08
09
10
11
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 элемента. В противном случае мы будем использовать mapOne для возврата этого атрибута через метод getAttribute .


Мы должны иметь возможность создавать новые элементы, как любая хорошая библиотека. Конечно, это не годится в качестве метода для экземпляра Dome , поэтому давайте поместим его прямо на наш объект Dome .

1
2
3
4
5
6
var dome = {
    // get method here
    create: function (tagName, attrs) {
 
    }
};

Как видите, мы возьмем два параметра: имя элемента и объект атрибутов. Большинство атрибутов будут применены с помощью нашего метода attr , но два будут подвергнуты специальной обработке. Мы будем использовать метод addClass для свойства className и text метод для свойства text . Конечно, нам нужно сначала создать элемент и объект Dome . Вот все, что в действии:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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, верно?


Далее мы напишем методы append и prepend Теперь, на самом деле, это довольно сложные функции, в основном из-за множественных вариантов использования. Вот что мы хотим сделать:

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

Худший браузер, с которым мы имеем дело, это IE8.

Варианты использования следующие: мы можем добавить или добавить

  • один новый элемент для одного или нескольких существующих элементов.
  • несколько новых элементов для одного или нескольких существующих элементов.
  • один существующий элемент для одного или нескольких существующих элементов.
  • несколько существующих элементов для одного или нескольких существующих элементов.

Примечание: я использую «новый» для обозначения элементов, которых еще нет в DOM; существующие элементы уже находятся в DOM.

Давайте сделаем это сейчас:

1
2
3
4
5
6
7
Dome.prototype.append = function (els) {
    this.forEach(function (parEl, i) {
        els.forEach(function (childEl) {
         
        });
    });
};

Мы ожидаем, что параметр els будет объектом Dome . Полная библиотека DOM приняла бы это как узел или список узлов, но мы не будем этого делать. Мы должны циклически перебирать каждый из наших элементов, а затем внутри него мы зацикливаемся над каждым из элементов, которые хотим добавить.

Если мы добавляем els к нескольким элементам, нам нужно их клонировать. Однако мы не хотим клонировать узлы в первый раз, когда они добавляются, а только в последующие. Итак, мы сделаем это:

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

То, что i пришел из внешнего цикла forEach : это индекс текущего родительского элемента. Если мы не добавляем первый родительский элемент, мы клонируем узел. Таким образом, фактический узел перейдет в первый родительский узел, а каждый другой родитель получит копию. Это хорошо работает, потому что объект Dome который был передан в качестве аргумента, будет иметь только исходные (неклонированные) узлы. Итак, если мы добавляем только один элемент к одному элементу, все задействованные узлы будут частью их соответствующих объектов Dome .

Наконец, мы добавим элемент:

1
parEl.appendChild(childEl);

Итак, в целом, вот что мы имеем:

01
02
03
04
05
06
07
08
09
10
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 , поэтому метод очень похож:

1
2
3
4
5
6
7
8
Dome.prototype.prepend = function (els) {
    return this.forEach(function (parEl, i) {
        for (var j = els.length -1; j > -1; j–) {
            childEl = (i > 0) ?
            parEl.insertBefore(childEl, parEl.firstChild);
        }
    });
};

Разница при добавлении заключается в том, что если вы последовательно добавляете список элементов к другому элементу, они заканчиваются в обратном порядке. Поскольку мы не можем forEach обратном направлении, я возвращаюсь назад в цикл с помощью цикла for . Опять же, мы клонируем узел, если это не первый родитель, к которому мы добавляем.


Для нашего последнего метода манипулирования узлами мы хотим иметь возможность удалять узлы из DOM. Легко, правда:

1
2
3
4
5
Dome.prototype.remove = function () {
    return this.forEach(function (el) {
        return el.parentNode.removeChild(el);
    });
};

Просто переберите все узлы и вызовите метод removeChild для каждого parentNode элемента. Прелесть здесь (все благодаря DOM) в том, что этот объект Dome будет работать нормально; мы можем использовать любой метод, который захотим, включая добавление или добавление его обратно в DOM. Хорошо, а?


Наконец, но не в последнюю очередь, мы собираемся написать несколько функций для обработчиков событий.

Как вы, наверное, знаете, IE8 использует старые события IE, поэтому мы должны это проверить. Кроме того, мы добавим события DOM 0, просто потому, что можем.

Проверьте метод, и тогда мы обсудим это:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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. Обратите внимание, как мы возвращаем последнюю функцию из IIFE: это то, что в итоге будет назначено для Dome.prototype.on . При обнаружении функций очень удобно иметь возможность назначать соответствующую функцию, подобную этой, вместо проверки функций при каждом запуске функции.

Функция off , которая отцепляет обработчики событий, в значительной степени идентична:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
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 для кода, который мы написали выше. Не стесняйтесь раскошелиться, поиграйте и отправьте запрос на получение.

Позвольте мне еще раз уточнить: смысл этого урока не в том, чтобы предлагать вам всегда писать собственные библиотеки.

Есть целые группы людей, работающих вместе, чтобы сделать большие, устоявшиеся библиотеки как можно лучше. Смысл здесь состоял в том, чтобы немного взглянуть на то, что может происходить внутри библиотеки; Я надеюсь, что вы подобрали несколько советов здесь.

Я действительно рекомендую вам покопаться в нескольких ваших любимых библиотеках. Вы обнаружите, что они не такие загадочные, как вы могли подумать, и вы, вероятно, многому научитесь. Вот несколько отличных мест для начала: