转:Build Your First JavaScript Library
2013-11-05 11:43
260 查看
http://net.tutsplus.com/tutorials/javascript-ajax/build-your-first-javascript-library/
As you can see, we’re calling our library Dome, because it’s primarily a DOM library. Yes, it’s lame.
We’ve got a couple of things going on here. First, we have a function; it will eventually be a constructor function for the instances of our library; those objects will wrap our selected or created elements.
Then, we have our
We’re using
Step 3: Creating
Here’s that
I really recommend you dig around inside a few of your favourite libraries.
This is really simple: we just iterate over the elements we selected and stick them onto the new object with numeric indices. Then, we add a
But what’s the point here? Why not just return the elements? We’re wrapping the elements in an object because we want to be able to create methods for the object; these are the methods that will allow us to interact with those elements. This is actually a boiled-down version of the way jQuery does it.
So, now that we have our
Let’s start with a
Of course, the
By doing it this way, the function will be called in the context of our
We also want a
Since the only difference between
One more:
Firstly, the DOM can be rather rough to wrangle for a beginner; it’s a pretty poor excuse for an API.
If building a library were just about writing the code, it wouldn’t be too difficult a job. But as I worked on this project, I found the tougher part was deciding how certain methods should work.
Soon, we’re going to build a
For this project, I’ll return the text of multiple elements as an array, unless there’s only one item in the array; then we’ll just return the text string, not an array with a single item. I think you’ll most often be getting the text of a single element, so we optimize for that case. However, if you’re getting the text of multiple elements, we’ll return something you can work with.
As you might expect, we need to check for a value in
If we’re setting, we’ll do a
The
Like I said: almost identical.
Our
Pretty straightforward, eh?
Now, what about removing classes? To keep it simple, we’ll only allow removing one class at a time.
On every element, we’ll split the
It’s pretty simple, and it’s not a full implementation (doesn’t support the second parameter), but it will work for our purposes.
If the
As you can see, we’ll take two parameters: the name of the element, and an object of attributes. Most of the attributes be applied via our
As you can see, we create the element and send it right into a new
But now that we’re creating new elements, we’ll want to insert them into the DOM, right?
The worst browser we’re dealing is IE8.
The use cases are as these: we might want to append or prepend
one new element to one or more existing elements.
multiple new elements to one or more existing element.
one existing element to one or more existing elements.
multiple existing elements to one or more existing elements.
Note: I’m using “new” to mean elements not yet in the DOM; existing elements are already in the DOM.
Let’s step though it now:
We expect that
If we’re appending the
That
Finally, we’ll actually append the element:
So, altogether, this is what we have:
The
We want to cover the same cases for the
The different when prepending is that if you sequentially prepend a list of elements to another element, they’ll end up in reverse order. Since we can’t
Just iterate through the nodes and call the
As you probably know, IE8 uses the old IE events, so we’ll have to check for that. Also, we’ll throw in the DOM 0 events, just ‘cause we can.
Check out the method, and then we’ll discuss it:
Here, we have an IIFE, and inside it we’re doing feature checking. If
The
Step 1: Creating the Library Boilerplate
We’ll start with some wrapper code, which will contain our whole library. It’s your typical immediately invoked function expression (IIFE).window.dome = (function () { function Dome (els) { } var dome = { get: function (selector) { } }; return dome; }());
As you can see, we’re calling our library Dome, because it’s primarily a DOM library. Yes, it’s lame.
We’ve got a couple of things going on here. First, we have a function; it will eventually be a constructor function for the instances of our library; those objects will wrap our selected or created elements.
Then, we have our
domeobject, which is our actual library object; as you can see, it’s returned at the end there. It’s got an empty
getfunction, which we’ll use to select elements from the page. So, let’s fill that in now.
Step 2: Getting Elements
Thedome.getfunction will take one parameter, but it could be a number of things. If it’s a string, we’ll assume it’s a CSS selector; but we can also take a single DOM Node, or a NodeList.
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); }
We’re using
document.querySelectorAllto simplify the finding of elements: of course, this does limit our browser support, but for this case, that’s okay. If
selectoris not a string, we’ll check for a
lengthproperty. If it exists, we’ll know we have a
NodeList; otherwise, we have a single element and we’ll put that in an array. That’s because we need an array to pass to our call to
Domeat the bottom there; as you can see, we’re returning a new
Domeobject. So let’s go back to that empty
Domefunction and fill it in.
Step 3: Creating Dome
Instances
Here’s that Domefunction:
function Dome (els) { for(var i = 0; i < els.length; i++ ) { this[i] = els[i]; } this.length = els.length; }
I really recommend you dig around inside a few of your favourite libraries.
This is really simple: we just iterate over the elements we selected and stick them onto the new object with numeric indices. Then, we add a
lengthproperty.
But what’s the point here? Why not just return the elements? We’re wrapping the elements in an object because we want to be able to create methods for the object; these are the methods that will allow us to interact with those elements. This is actually a boiled-down version of the way jQuery does it.
So, now that we have our
Domeobject being returned, let’s add some methods to its prototype. I’m going to put those methods right under the
Domefunction.
Step 4: Adding a Few Utilities
The first functions we’re going to write are simple utility functions. Since ourDomeobjects could wrap more than one DOM element, we’re going to need to loop over every element in pretty much every method; so, these utilities will be handy.
Let’s start with a
mapfunction:
Dome.prototype.map = function (callback) { var results = [], i = 0; for ( ; i < this.length; i++) { results.push(callback.call(this, this[i], i)); } return results; };
Of course, the
mapfunction takes a single parameter, a callback function. We’ll loop over the items in the array, collecting whatever is returned from the callback in the
resultsarray. Notice how we’re calling that callback function:
callback.call(this, this[i], i));
By doing it this way, the function will be called in the context of our
Domeinstance, and it will receive two parameters: the current element, and the index number.
We also want a
forEachfunction. This is actually really simple:
Dome.prototype.forEach(callback) { this.map(callback); return this; };
Since the only difference between
mapand
forEachis that
mapneeds to return something, we can just pass our callback to
this.mapand ignore the returned array; instead, we’ll return
thisto make our library chainable. We’ll be using
forEachquite a bit. So, notice that when we return our
this.forEachcall from a function, we’re actually returning
this. For example, these methods actually return the same thing:
Dome.prototype.someMethod1 = function (callback) { this.forEach(callback); return this; }; Dome.prototype.someMethod2 = function (callback) { return this.forEach(callback); };
One more:
mapOne. It’s easy to see what this function does, but the real question is, why do we need it? This requires a bit of what you could call “library philosophy.”
A Short “Philosophical” Detour
Firstly, the DOM can be rather rough to wrangle for a beginner; it’s a pretty poor excuse for an API.
If building a library were just about writing the code, it wouldn’t be too difficult a job. But as I worked on this project, I found the tougher part was deciding how certain methods should work.
Soon, we’re going to build a
textmethod that returns the text of our selected elements. If our
Domeobject wraps several DOM node (
dome.get("li"), for example), what should this return? If you do something similar in jQuery (
$("li").text()), you’ll get a single string with the text of all the elements concatenated together. Is this useful? I don’t think so, but I’m not sure what a better return value would be.
For this project, I’ll return the text of multiple elements as an array, unless there’s only one item in the array; then we’ll just return the text string, not an array with a single item. I think you’ll most often be getting the text of a single element, so we optimize for that case. However, if you’re getting the text of multiple elements, we’ll return something you can work with.
Back to Coding
So, themapOnemethod will simply run
map, and then either return the array, or the single item that was in the array. If you’re still not sure how this is useful, stick around: you’ll see!
Dome.prototype.mapOne = function (callback) { var m = this.map(callback); return m.length > 1 ? m : m[0]; };
Step 5: Working with Text and HTML
Next, let’s add thattextmethod. Just like jQuery, we can pass it a string and set the element’s text, or use no parameters to get the text back.
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; }); } };
As you might expect, we need to check for a value in
textto see if we’re setting or getting. Note that just
if (text)wouldn’t work, because an empty string is a false value.
If we’re setting, we’ll do a
forEachover the elements and set their
innerTextproperty to the
text. If we’re getting, we’ll return the elements’
innerTextproperty. Note our use of the
mapOnemethod: if we’re working with multiple elements, this will return an array; otherwise, it will be just the string.
The
htmlmethod will do pretty much the same thing as
text, except that it will use the
innerHTMLproperty, instead of
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; }); } };
Like I said: almost identical.
Step 6: Hacking Classes
Next up, we want to be able to add and remove classes; so let’s write theaddClassand
removeClassmethods.
Our
addClassmethod will take either a string or an array of class names. To make this work, we need to check the type of that parameter. If it’s an array, we’ll loop over it and create a string of class names. Otherwise, we’ll just add a single space to the front of the class name, so it doesn’t mess with the existing classes on the element. Then, we just loop over the elements and append the new classes to the
classNameproperty.
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; }); };
Pretty straightforward, eh?
Now, what about removing classes? To keep it simple, we’ll only allow removing one class at a time.
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(" "); }); };
On every element, we’ll split the
el.classNameinto an array. Then, we use a while loop to slice out the offending class until
cs.indexOf(clazz)returns -1. We do this to cover the edge case where the same classes has been added to an element more than once: we need to make sure it’s really gone. Once we’re sure we’ve cut out every instance of the class, we join the array with spaces and set it on
el.className.
Step 7: Fixing an IE Bug
The worst browser we’re dealing is IE8. In our little library, there’s only one IE bug that we need to deal with; thankfully, it’s pretty simple. IE8 doesn’t support theArraymethod
indexOf; we use it in
removeClass, so let’s polyfill it:
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; }; }
It’s pretty simple, and it’s not a full implementation (doesn’t support the second parameter), but it will work for our purposes.
Step 8: Adjusting Attributes
Now, we want anattrfunction. This’ll be easy, because it’s practically identical to our
textor
htmlmethods. Like those methods, we’ll be able to both get and set attributes: we’ll take an attribute name and value to set, and just an attribute name to get.
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); }); } };
If the
valhas a value, we’ll loop through the elements and set the selected attribute with that value, using the element’s
setAttributemethod. Otherwise, we’ll use
mapOneto return that attribute via the
getAttributemethod.
Step 9: Creating Elements
We should be able to create new elements, like any good library can. Of course, this would be no good as a method on aDomeinstance, so let’s put it right on our
domeobject.
var dome = { // get method here create: function (tagName, attrs) { } };
As you can see, we’ll take two parameters: the name of the element, and an object of attributes. Most of the attributes be applied via our
attrmethod, but two will get special treatment. We’ll use the
addClassmethod for the
classNameproperty, and the
textmethod for the
textproperty. Of course, we’ll need to create the element and the
Domeobject first. Here’s all that in action:
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; }
As you can see, we create the element and send it right into a new
Domeobject. Then, we deal with the attributes. Notice that we have to delete the
classNameand
textattributes after working with them. This keeps them from being applied as attributes when we loop over the rest of the keys in
attrs. Of course, we end by returning the new
Domeobject.
But now that we’re creating new elements, we’ll want to insert them into the DOM, right?
Step 10: Appending and Prepending Elements
Next up, we’ll writeappendand
prependmethods, Now, these are actually a bit tricky functions to write, mainly because of the multiple use cases. Here’s what we want to be able to do:
dome1.append(dome2); dome1.prepend(dome2);
The worst browser we’re dealing is IE8.
The use cases are as these: we might want to append or prepend
one new element to one or more existing elements.
multiple new elements to one or more existing element.
one existing element to one or more existing elements.
multiple existing elements to one or more existing elements.
Note: I’m using “new” to mean elements not yet in the DOM; existing elements are already in the DOM.
Let’s step though it now:
Dome.prototype.append = function (els) { this.forEach(function (parEl, i) { els.forEach(function (childEl) { }); }); };
We expect that
elsparameter to be a
Domeobject. A complete DOM library would accept this as a node or nodelist, but we won’t do that. We have to loop over each of our elements, and then inside that, we loop over each of the elements we want to append.
If we’re appending the
elsto more than one element, we need to clone them. However, we don’t want to clone the nodes the first time they’re appended, only subsequent times. So we’ll do this:
if (i > 0) { childEl = childEl.cloneNode(true); }
That
icomes from the outer
forEachloop: it’s the index of the current parent element. If we aren’t appending to the first parent element, we’ll clone the node. This way, the actual node will go in the first parent node, and every other parent will get a copy. This works well, because the
Domeobject that was passed in as an argument will only have the original (uncloned) nodes. So, if we’re only appending a single element to a single element, all the nodes involved will be part of their respective
Domeobjects.
Finally, we’ll actually append the element:
parEl.appendChild(childEl);
So, altogether, this is what we have:
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);
});
});
};
The prepend
Method
We want to cover the same cases for the prependmethod, so the method is pretty very similar:
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); } }); };
The different when prepending is that if you sequentially prepend a list of elements to another element, they’ll end up in reverse order. Since we can’t
forEachbackwards, I’m going through the loop backwards with a
forloop. Again, we’ll clone the node if this isn’t the first parent we’re appending to.
Step 11: Removing Nodes
For our last node manipulation method, we want to be able to remove nodes from the DOM. Easy, really:Dome.prototype.remove = function () { return this.forEach(function (el) { return el.parentNode.removeChild(el); }); };
Just iterate through the nodes and call the
removeChildmethod on each element’s
parentNode. The beauty here (all thanks to the DOM) is that this
Domeobject will still work fine; we can use any method we want on it, including appending or prepending it back into the DOM. Nice, eh?
Step 12: Working with Events
Last, but certainly not least, we’re going to write a few functions for event handlers.As you probably know, IE8 uses the old IE events, so we’ll have to check for that. Also, we’ll throw in the DOM 0 events, just ‘cause we can.
Check out the method, and then we’ll discuss it:
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; }); }; } }());
Here, we have an IIFE, and inside it we’re doing feature checking. If
document.addEventListenerexists, we’ll use that; otherwise, we’ll check for
document.attachEventor fall back to DOM 0 events. Notice how we’re returning the final function from the IIFE: that’s what will end up being assigned to
Dome.prototype.on. When doing feature detection, it’s really handy to be able to assign the appropriate function like this, instead of checking for the features each time the function is run.
The
offfunction, which unhooks event handlers, is pretty much identical:
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; }); }; } }());
That’s It!
相关文章推荐
- [Poi] Build and Analyze Your JavaScript Bundles with Poi
- Dojo: Using the Dojo JavaScript Library to Build Ajax Applications
- Using the Dojo JavaScript Library to Build Ajax Applications一书翻译完毕
- Creating your own JavaScript Library
- Android小程序(1)--Build Your First App(a)
- Build your Dojo-based Javascript Application and deployed via CDN
- Dojo入门——《Dojo_.Using.the.Dojo.JavaScript.Library.to.Build.Ajax.Applications》读书笔记
- Tutorial: Build Your First Tensorflow Android App
- Hello World! Build Your First iPhone App
- CV-Build your first deep learning network-识别一只猫
- 【转】Build Your own Simplified AngularJS in 200 Lines of JavaScript
- Build Your First Mobile App With Ionic 2 & Angular 2 - Part 1
- How To Build Your First F**king STAF Service
- Build Your First Mobile App With Ionic 2 & Angular 2 - Part 2
- Using the Dojo JavaScript Library to Build Ajax Applications一书翻译完毕
- Build Your First Mobile App With Ionic 2 & Angular 2 - Part 3
- Microsoft Azure Tutorial: Build your first movie inventory web app with just a few lines of code
- Using the Dojo JavaScript Library to Build Ajax Applications一书翻译完毕
- Build Your First Mobile App With Ionic 2 & Angular 2 - Part 4
- 【转载】How to build and run your first deep learning network