A detailed tutorial on creating a JavaScript framework with superb likes.
I think Mootools is incredible? Want to know how Dojo is implemented? Curious about JQuery's skills? In this tutorial, we will explore the secrets behind the framework and try to build a simple version of your favorite framework.
We use various JavaScript frameworks almost every day. When you get started, convenient DOM (Document Object Model) operations make you think JQuery is a great thing. This is because: first, DOM is too difficult for beginners to understand; of course, it is not a good thing for an API to understand. Second, the compatibility problem between browsers is very disturbing.
- We wrap elements into objects because we want to be able to add methods to objects.
- In this tutorial, we will try to implement one of these frameworks from scratch. Yes, it will be interesting, but before you get too excited, I 'd like to clarify a few points:
- This will not be a fully functional framework. Indeed, we have to write a lot of things, but it is not JQuery. However, what we will do will make you feel like writing a framework.
- We do not intend to guarantee full compatibility. The framework we will write can work on Internet Explorer 8 +, Firefox 5 +, Opera 10 +, Chrome and Safari.
- Our framework will not cover all possible features. For example, our append and preappend methods work only when you pass it to an instance of our framework. We do not use native DOM nodes and node lists.
In addition, although we will not write test cases for our framework in the tutorial, I have done it well when I first developed it. You can get the framework and test case code from Github.
Step 1: create a framework Template
We will start with some packaging code that will accommodate our entire framework. This is a typical immediate function (IIFE ).
window.dome = (function () { function Dome (els) { } var dome = { get: function (selector) { } }; return dome;}());
As you can see, our framework is called dome because it is a basic DOM framework. That's right, basically (lame has the meaning of "lame" and "incomplete", dom plus lame equals dome.
We already have something. First, we have a function. It will be the constructor of the object instance of the constructor framework. Those objects will contain the elements we select and create.
Then, we have a dome object, which is our framework object. You can see that it is finally returned to the function caller as the return value of the function. dome ). There is also an empty get function. We will use it to select elements from the page. So let's fill in the code.
Step 2: obtain elements
Dome's get function has only one parameter, but it can be a lot of things. If it is a string, we assume it is a CSS (stacked style sheet) selector. However, we may also get a list of DOM nodes or DOM nodes.
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 use document. querySelectorAll for simple selection elements: Of course, this will limit the compatibility of our browsers, but this is acceptable. If selector is not of the string type, we will check its length attribute. If it exists, we will know that we get a node list; otherwise, it is a separate element, and we put it in an array. This is because we need to pass an array to Dome next. As you can see, we return a new Dome object. Let's return to the Dome function and fill it with code.
Step 3: Create a Dome instance
Here is the Dome function:
function Dome (els) { for(var i = 0; i < els.length; i++ ) { this[i] = els[i]; } this.length = els.length;}
I strongly recommend that you study some of your favorite frameworks in depth.
This is very simple: We just traverse all the elements of els and store them in a new object indexed by numbers. Then we added a length attribute.
But what does it mean? Why not directly return elements? Because: we wrap elements into objects because we want to be able to add methods to objects. These methods allow us to traverse these elements. In fact, this is the concentrated version of JQuery's solution.
Our Dome object has been returned. Now let's add some methods for its prototype. I will write those methods directly under the Dome function.
Step 4: add several utilities
The first batch of functions to be added are some simple tool functions. Since a Dome object may contain at least one DOM element, we need to traverse all elements in almost every method. In this way, these tools will be powerful.
We start with a map function:
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, this map function has an input parameter and a callback function. We traverse all elements of the Dome object and collect the return values of the callback function to the result set. Note how we call the callback function:
callback.call(this, this[i], i));
In this way, the function is called in the context of the Dome instance, and the function receives two parameters: the current element and the element number.
We also want a foreach function. In fact, this is simple:
Dome.prototype.forEach(callback) { this.map(callback); return this;};
Because the difference between the map function and the foreach function is that map needs to return something, we can just pass the callback to this. map then ignores the returned array; instead, we will return this to chain our library. Foreach is frequently called. Therefore, when a function callback is returned, the Dome instance is returned. For example, the following method actually returns the Dome instance:
Dome.prototype.someMethod1 = function (callback) { this.forEach(callback); return this;};Dome.prototype.someMethod2 = function (callback) { return this.forEach(callback);};
Another one is mapOne. It's easy to know what this function is, but the real question is, why does it need it? This requires something that we call "database philosophy.
A brief explanation of "Philosophy"
- First, for a beginner, DOM is very confusing; its API is not perfect.
If building a database is just writing code, it is not difficult. But when I develop this library, I find that the imperfect parts determine the implementation method of a certain number of methods.
Soon, we will construct a text method that returns the text of the selected element. If the Dome object contains multiple DOM nodes (such as dome. get ("li"), what will be returned? If you write it as simple as jQuery ($ ("li"). text (), you will get a string that is directly spliced by the text of all elements. Is it useful? I don't think there is any better way.
For this project, I will return the text of multiple elements in an array. Unless there is only one element in the array, I will return only one text string, instead of an array containing one element. I think you will often get the text of a single element, so we optimized that situation. However, if you want to get the text of multiple elements, you will also use our response.
Back to code
Then, the mapOne method simply runs the map function and then returns an array or an element in an array. If you are still not sure how this works, stick to it and you will see it!
Dome.prototype.mapOne = function (callback) { var m = this.map(callback); return m.length > 1 ? m : m[0];};
Step 2: process Text and HTML
Next, let's add the text method. Just like jQuery, we can pass a string value, set the text value of the node element, or get the returned text value through the no-argument method.
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 expected, when we set (setting) or get (getting) value, we need to check the value of text. Note that if the justif (text) method does not work, it is because the text is a null string and is an incorrect value.
If we set (setting), but use a forEach to traverse elements, set their innerText attribute. If we get (getting), The innerText attribute of the element is returned. When using the mapOne method, note that if we are processing multiple elements, an array will be returned; otherwise, a string will be returned.
If the html method uses the innerHTML attribute instead of the innerText, it will handle text-related matters more elegantly.
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; }); }};
As I said: almost the same.
Step 6: Modify the class
Next, we want to operate the class, so we can add addClass () and removeClass (). The addClass () parameter is an array of class names or names. To implement dynamic parameters, we need to determine the parameter type. If the parameter is an array, traverse the array and add these class names to the element. If the parameter is a string, add the class name directly. Make sure that the original class name is not messed up.
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; });};
Intuitive, right? Hey
Now, it is as simple as writing removeClass. However, only one class name can be deleted 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(" "); });};
For each element, we split el. className into a string array. Then we use a while loop connection until the return value of cs. indexOf (clazz) is greater than-1. Join the result to el. className.
Step 7: fix a BUG caused by IE
The worst browser we are dealing with is IE8. in this small library, only one BUG caused by IE needs to be fixed. And, thanks to God, fixing it is very simple. IE8 does not support the Array method indexOf; we need to use it in the removeClass method. Let's complete 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 looks very simple, and it is not fully implemented (the second parameter is not supported), but it can achieve our goal.
Step 2: Adjust attributes
Now, we want an attr function. This is easy because it is almost the same as the text or html method. Like these methods, we can set and get attributes: we will set the name and value of an attribute and get the value only through the parameter name.
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 parameter has a value, we traverse the element and set the attribute value through the setAttribute method of the element. In addition, we will use mapOne to return the parameters obtained through the getAttribute method.
Step 2: Create an element
Like any good framework, we should also be able to create elements. Of course, there is no good method in the Demo instance, so let's add the method to the demo project.
var dome = { // get method here create: function (tagName, attrs) { }};
As you can see, we need two parameters: the element name and a parameter object. Most attributes are used through our arrt method, but tagName and attrs have special treatment. We use the addClass method for the className attribute and the text method for the text attribute. Of course, we must first create elements and Demo objects. The following are all functions:
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 shown above, an element is created and sent to the new Dmoe object. Next, we process all the attributes. Note: after using the className and text attributes, We have to delete them. This ensures that other keys can be used when we traverse them. Of course, we finally return this new Demo object.
We have created new elements. We want to insert these elements into the DOM, right?
Step 2: Appending and Prepending
Next, we will add the tail and header methods. Considering multiple scenarios, implementing these methods may be tricky. The following is what we want to achieve:
dome1.append(dome2);dome1.prepend(dome2);
IE8 is a wonderful thing for us.
Tail or header addition includes the following scenarios:
- Add a single new element to one or more existing elements
- Add multiple new elements to one or more existing elements
- Add a single existing element to one or more existing elements
- Add multiple existing elements to one or more existing elements
Note: The "new element" indicates that the node element in the DOM has not been added. The "existing element" indicates that the node element already exists in the DOM.
Now let's implement it step by step:
Dome.prototype.append = function (els) { this.forEach(function (parEl, i) { els.forEach(function (childEl) { }); });};
Assume that the els parameter is a DOM object. A fully functional DOM library should be able to process node or node sequence (nodelist), but we do not need it now. First, traverse the elements (parent element) to be added, and then traverse the elements (child elements) to be added in this loop ).
If you want to add multiple parent elements to a child element, You need to clone the child element (avoid removing the last addition operation from the last operation ). However, there is no need to clone it when it is added for the first time. You only need to clone it in other loops. Therefore, the process is as follows:
if (i > 0) { childEl = childEl.cloneNode(true);}
Variable I comes from the outer forEach loop: it represents the serial number of the parent element. The first parent element adds the child element, while other parent elements add the clone of the target child element. Because the child elements passed in as parameters are not cloned, all nodes can respond when a single child element is added to a single parent element.
Finally, add the following elements:
parEl.appendChild(childEl);
Therefore, when combined, we get the following implementation:
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 Method
Implementing the prepend method based on the same logic is actually quite simple.
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 difference is that when multiple elements are added, the added order is reversed. Therefore, you cannot use a forEach loop instead of a reverse for loop. Similarly, the target child element needs to be cloned when it is added to a non-first parent element.
Step 2: delete a node
To delete the last node from the dom, you only need:
Dome.prototype.remove = function () { return this.forEach(function (el) { return el.parentNode.removeChild(el); });};
You only need to delete the child node method through node iteration and calling on their parent node. It is better that the dom object still works normally (thanks to the object model in the document ). We can use the method we want to use on it, including inserting and pre-Inserting back to DOM. It's pretty, isn't it?
Step 2: event handling
Finally, it is the most important part. We need to write several event processing functions.
As you know, IE8 still uses the old IE event, so we need to detect it. At the same time, we must be prepared to use DOM level 0 events.
View the following method and we will discuss it later:
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 use the immediate execution function (IIFE), And we perform Feature Detection within the function. If the document. addEventListener method exists, we use it. In addition, we also detect document. attachEvent. If not, we use the DOM level 0 method. Please note how to return the final function from the immediate execution function: it will be allocated to Dome. prototype. on. During feature detection, this method is more convenient to allocate suitable methods than to detect each function running.
The event unbinding method off is similar to the on method :.
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; }); }; }}());