Note! This article is out of date. You can view the upgraded project directly.
In my daily work, I often find that a lot of business logic is repeated, and some customization requirements cannot be perfectly solved by using other people’s plug-ins, so I decide to separate and encapsulate some commonly used components to form a set of plug-ins library of my own. In the meantime, I’ll use this tutorial series to document the development process of each plug-in and show you step-by-step how to build a useful, reusable set of wheels.
So, Let’s begin!
At present, the project uses ES5 and UMD standard packaging, so the front-end only supports the introduction of
Pagination Github don’t skint on your Star.
JavaScript modular
To develop a JavaScript plug-in, start with the modularity of JavaScript. What is modularity? The simple idea is to allow JavaScript to organize and maintain code in a holistic way, allowing multiple developers to reference each other’s blocks without causing conflicts. Common modular specifications before ECMAScript6 include CommonJS, AMD, UMD, etc. Since our code was developed using ES5 syntax, we chose UMD specification to organize the code. For the development process of modularity, please refer to:
- A Brief History of JavaScript Modular Programming (2009-2016)
- A brief history of JavaScript module evolution
On top of this module specification, we need a mechanism to load different modules, such as require.js which implements the AMD specification. See this tutorial by Yifeng Ruan:
- Javascript modular programming (iii) : require.js usage
Because the wheel we developed does not involve multi-module loading for the time being, module loading will not be discussed too much for the time being, and readers can expand and learn by themselves.
Getting back to our topic, there are a few other things that need to be added before we start developing.
Self-executing function
There are three ways to define a function in ES5:
- Function declaration
function foo () {}
Copy the code
Functions declared like this are automatically promoted, as are variables, so we can place the function declaration after the statement that calls it:
foo();
function foo () {}
Copy the code
- Functional expression
var foo = function () {}
Copy the code
The right-hand side is actually an anonymous function assigned to a variable that can be called by its name, but unlike the first way, functions declared by expressions are not promoted.
- Use the Function constructor
var foo = new Function(a)Copy the code
Is there a way to declare a function and call it automatically without writing the function name? The answer, yes, is to use self-executing functions. (In fact, MY other article breaking bricks – JS object Oriented introduction has been mentioned)
The self-executing Function corrupt-invoked Function Expression, as the name implies, is the Function Invoked automatically and is sometimes called the Function Expression Invoked Immediately. Its basic form is as follows:
(function () {
console.log('hello')
}());
(function () {
console.log('hello')
})();
Copy the code
Both are equivalent, except that the former makes the code look more like a whole.
As you can see, the effect of these two notations is to define the function inside () and then use () to execute the function, so it is self-executing.
Some of the benefits of IIFE are:
- Avoid contaminating global variables
- Reduce naming conflicts
- Lazy loading
Most importantly, it can create a separate scope, whereas prior to ES6 JavaScript had no block-level scope. Using this, we can easily ensure that variables between modules are not overwritten:
// libA.js
(function(){
var num = 1; }) ();// libB.js
(function(){
var num = 2; }) ();Copy the code
The scopes in the above two file modules are independent and do not affect each other. (If modules want to reference each other, they need to use module loaders, such as the library require.js mentioned above),
Based on this, we can see what an IIFE template that implements the UMD specification looks like:
// if the module has no dependencies, the above pattern can be simplified to
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define([], factory);
} else if (typeof module= = ='object' && module.exports) {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals (root is window)
root.returnExports = factory();
}
}(typeofself ! = ='undefined' ? self : this.function () {
// Just return a value to define the module export.
// This example returns an object, but the module
// can return a function as the exported value.
return {};
}));
Copy the code
As you can see, the UMD specification is compatible with browsers, Node environments, and AMD specifications, so our code can be wrapped in UMD and run in different environments.
Add-in template
The most important aspect of plug-in development is plug-in compatibility. A plug-in should be able to run in at least several different environments simultaneously. Secondly, it also needs to meet the following functions and conditions:
- The scope of the plug-in itself is independent of the user’s current scope, that is, private variables within the plug-in cannot affect the user’s environment variables.
- Plug-ins need to have default setting parameters;
- In addition to the basic functions that have been realized, the plug-in needs to provide some API, through which users can modify the default parameters of the plug-in function, so as to achieve user-defined plug-in effect.
- Plugins support chained calls;
- The plug-in provides a listening entry and listens for the specified element to make the element and the plug-in response look like the plug-in.
We’ve already done the first thing with UMD packaging, now let’s look at the second and third.
Typically, a plug-in will have default parameters and provide some parameters for users to customize some functions. So how to implement, this is actually a matter of object merge, for example:
function extend(o, n, override) {
for (var p in n) {
if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
o[p] = n[p];
}
}
// Default parameters
var options = {
pageNumber: 1.pageShow: 2
};
// User Settings
var userOptions = {
pageShow: 3.pageCount: 10
}
extend(options, userOptions, true);
/ / after the merger
options = {
pageNumber: 1.pageShow: 3.pageCount: 10
}
Copy the code
As shown above, a similar extend function can be used to merge objects, so that our plug-in can set parameters.
The extend function here is a shallow copy, because the user parameters of the plug-in are generally not modified, if you want to achieve a deep copy, please refer to the extend method in jQuery.
Fourth, our plug-in does not need such a function for the time being, so we can not support it for the time being. Fifth in the code we will implement this step by step through the callback function.
To sum up, we can implement a basic plug-in template:
;// If a function does not start with a ";" The end, then there may be grammatical errors
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module= = ='object' && module.exports) {
module.exports = factory();
} else {
root.Plugin = factory();
}
}(typeofself ! = ='undefined' ? self : this.function() {
'use strict';
// tool
function extend(o, n, override) {
for (var p in n) {
if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
o[p] = n[p];
}
}
// polyfill
var EventUtil = {
addEvent: function(element, type, handler) {
// Add a binding
if (element.addEventListener) {
// Add events using the DOM2-level method
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
// Add events using IE methods
element.attachEvent("on" + type, handler);
} else {
// Add events using dom0-level methods
element["on"+ type] = handler; }},// Remove the event
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.datachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null; }},getEvent: function(event) {
// Returns an event object reference
return event ? event : window.event;
},
// Get mouseover and mouseout elements
getRelatedTarget: function(event) {
if (event.relatedTarget) {
return event.relatedTarget;
} else if (event.toElement) {
/ / compatible with Internet explorer -
return event.toElement;
} else if (event.formElement) {
return event.formElement;
} else {
return null; }},getTarget: function(event) {
// Returns the event source target
return event.target || event.srcElement;
},
preventDefault: function(event) {
// Cancel the default event
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false; }},stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true; }},// Get which of the mouse buttons mouseDown or Mouseup pressed or released
getButton: function(event) {
if (document.implementation.hasFeature("MouseEvents"."2.0")) {
return event.button;
} else {
// Map the button attribute under IE model to the button attribute under DOM model
switch (event.button) {
case 0:
case 1:
case 3:
case 5:
case 7:
// Press the main mouse button.
return 0;
case 2:
case 6:
// Press the middle mouse button
return 2;
case 4:
// The second mouse button
return 1; }}},// Get the value that represents the direction of the mouse wheel
getWheelDelta: function(event) {
if (event.wheelDelta) {
return event.wheelDelta;
} else {
return -event.detail * 40; }},// Get the same character encoding across browsers, used in keypress events
getCharCode: function(event) {
if (typeof event.charCode == "number") {
return event.charCode;
} else {
returnevent.keyCode; }}};// plugin construct function
function Plugin(selector, userOptions) {
// Plugin() or new Plugin()
if(! (this instanceof Plugin)) return new Plugin(selector, userOptions);
this.init(selector, userOptions)
}
Plugin.prototype = {
constructor: Plugin,
// default option
options: {},
init: function(selector, userOptions) {
extend(this.options, userOptions, true); }};return Plugin;
}));
Copy the code
There is also an EventUtil object that polyfills compatibility with event registration. See:
- EventUtil – Cross-browser event objects
- Methods and usage in the cross-browser event object ——-EventUtil
At this point, the basic template for a plug-in is roughly in place. In the next section, we can finally begin development of the paging plug-in in earnest!
Thought analysis
It has been said that the essence of a computer is an abstraction of the real world, and that programming is the formulation of rules for that abstraction.
As mentioned above, before actual coding, we generally need to conduct an idea analysis of the requirements effect to be realized, and finally further abstract this idea process into logical code. Let’s take a look at the pagination effect we want to implement. I’ve divided it into two cases, showing and not showing ellipses. Let’s look at the first one first:
// A total of 30 pages
// In the first case, no ellipsis is displayed and a maximum of two page numbers are displayed before and after the current page numberThe current page is1, then display1 2 3 4 5The current page is2, then display1 2 3 4 5The current page is3, then display1 2 3 4 5The current page is4, then display2 3 4 5 6. The current page is15, then display13 14 15 16 17. The current page is27, then display25 26 27 28 29The current page is28, then display26 27 28 29 30The current page is29, then display26 27 28 29 30The current page is30, then display26 27 28 29 30
Copy the code
Each of the above numbers would be a button or hyperlink in practice, but for now, since this is analysis, we can simplify and ignore it, so that the problem becomes a simple string output problem. Let’s define a function:
function showPages (page, total, show) {}Copy the code
The function passes in the following parameters: current page number, total page number, maximum number of pages to display before and after the current page number, then we need to loop this function to print pages:
var total = 30;
for (var i = 1; i <= total; i++) {
console.log(showPages(i, total));
}
Copy the code
ShowPages () = showPages(); showPages() = showPages();
function showPages (page, total, show) {
var str = ' ';
if (page < show + 1) {
for (var i = 1; i <= show * 2 + 1; i++) {
str = str + ' '+ i; }}else if (page > total - show) {
for (var i = total - show * 2; i <= total; i++) {
str = str + ' '+ i; }}else {
for (var i = page - show; i <= page + show; i++) {
str = str + ' '+ i; }}return str.trim();
}
Copy the code
The idea is to spell out the page number in sections, and print the following:
The page with no ellipsis is printed, and then let’s see if the ellipsis is displayed:
// In the second case, ellipsis is displayed. A maximum of two page numbers are displayed before and after the current page numberThe current page is1, then display1 2 3.30The current page is2, then display1 2 3 4.30The current page is3, then display1 2 3 4 5.30The current page is4, then display1 2 3 4 5 6.30The current page is5, then display1.3 4 5 6 7.30. The current page is15, then display1.13 14 15 16 17.30. The current page is26, then display1.24 25 26 27 28.30The current page is27, then display1.25 26 27 28 29 30The current page is28, then display1.26 27 28 29 30The current page is29, then display1.27 28 29 30The current page is30, then display1.28 29 30
Copy the code
You also need to complete the showPages() function:
function showPages(page, length, show) {
var str = ' ';
var preIndex = page - (show + 1);
var aftIndex = page + (show + 1);
if (page < show + 3) {
for (var i = 1; i <= show * 2 + 3; i++) {
if((i ! == preIndex && i ! == aftIndex) || (i ===1 || i === total)) {
str = str + ' ' + i;
} else {
str = str + '... ' + total;
break; }}}else if (page > total - (show + 2)) {
for (var i = total; i >= total - (show * 2 + 2); i--) {
if((i ! == preIndex && i ! == aftIndex) || (i ===1 || i === total)) {
str = i + ' ' + str;
} else {
str = '1... ' + str;
break; }}}else {
for (var i = preIndex + 1; i <= aftIndex - 1; i++) {
str = str + ' ' + i;
}
str = '1... ' + str + '... ' + total;
}
return str.trim();
}
Copy the code
It also adopts the idea of piecewise spelling, and can successfully print the results:
However, a closer look at the above code will find a lot of redundant logic, can be optimized? Here’s a more clever way of thinking about it:
function showPages (page, total, show) {
var str = page + ' ';
for (var i = 1; i <= show; i++) {
if (page - i > 1) {
str = page - i + ' ' + str;
}
if (page + i < total) {
str = str + ' '+ (page + i); }}if (page - (show + 1) > 1) {
str = '... ' + str;
}
if (page > 1) {
str = 1 + ' ' + str;
}
if (page + show + 1 < total) {
str = str + ' ...';
}
if (page < total) {
str = str + ' ' + total;
}
return str;
}
Copy the code
The print is the same, but the code is much simpler.
Basic architecture
A good plug-in, the code must be high reuse, low coupling, easy to expand, so we need to adopt object-oriented method to build the basic framework of the plug-in:
// echo jQuery $()
function $(selector, context) {
context = arguments.length > 1 ? context : document;
return context ? context.querySelectorAll(selector) : null;
}
var Pagination = function(selector, pageOption) {
// Default configuration
this.options = {
curr: 1.pageShow: 2.ellipsis: true.hash: false
};
// Merge the configuration
extend(this.options, pageOption, true);
// Pager element
this.pageElement = $(selector)[0];
// Total number of data
this.dataCount = this.options.count;
// Current page number
this.pageNumber = this.options.curr;
/ / the total number of pages
this.pageCount = Math.ceil(this.options.count / this.options.limit);
/ / rendering
this.renderPages();
// Execute the callback function
this.options.callback && this.options.callback({
curr: this.pageNumber,
limit: this.options.limit,
isFirst: true
});
// Change the page count and trigger the event
this.changePage();
};
Pagination.prototype = {
constructor: Pagination,
changePage: function() {}};return Pagination;
Copy the code
As shown above, a pager object in prototype mode has been constructed. Let’s walk through the above code.
Paging configuration
This splitter provides the following basic parameters:
// Paging element ID (required)
var selector = '#pagelist';
// Paging configuration
var pageOption = {
// Number of data items per page (required)
limit: 5.// Total number of data (usually obtained from the back end, mandatory)
count: 162.// Current page number (optional, default is 1)
curr: 1.// Whether to display ellipsis (optional, default)
ellipsis: true.// The number of pages that can be displayed before and after the current page (optional, default is 2)
pageShow: 2.// Enable location.hash and customize the hash value (off by default)
// If enabled, the url is automatically appended when paging is triggered: #! Hash ={curr} With this, you can locate the page as soon as it loads
hash: false.// The default is once the page is loaded, and then again when the page is switched
callback: function(obj) {
// obj.curr: get the current page number
// obj.limit: Obtain the number of data items displayed on each page
// obj.isFirst: whether to load the page for the first time
// This is not executed for the first time
if(! obj.isFirst) {// do something}}};Copy the code
A call to extend() in the constructor completes the merging of the user argument with the plug-in default argument.
The callback event
Typically, plug-ins need to react to changes in their state (click events, etc.). So we need to do some monitoring of the user’s behavior, which is conventionally called a callback function. In the above code we can see that there is such a paragraph:
// Execute the callback function
this.options.callback && this.options.callback({
curr: this.pageNumber,
limit: this.options.limit,
isFirst: true
});
Copy the code
Isn’t this a little weird, but it’s the same thing as:
if(this.options.callback){
this.options.callback({
curr: this.pageNumber,
limit: this.options.limit,
isFirst: true
});
}
Copy the code
As you’re smart enough to know, the callback here is not something specific, but a reference. Regardless of who the callback points to, we just need to determine if it exists and execute it if it does.
event
From here you need to bind click events to the pagers, completing our changePage() method:
changePage: function() {
var self = this;
var pageElement = self.pageElement;
EventUtil.addEvent(pageElement, "click".function(ev) {
var e = ev || window.event;
var target = e.target || e.srcElement;
if (target.nodeName.toLocaleLowerCase() == "a") {
if (target.id === "prev") {
self.prevPage();
} else if (target.id === "next") {
self.nextPage();
} else if (target.id === "first") {
self.firstPage();
} else if (target.id === "last") {
self.lastPage();
} else if (target.id === "page") {
self.goPage(parseInt(target.innerHTML));
} else {
return;
}
self.renderPages();
self.options.callback && self.options.callback({
curr: self.pageNumber,
limit: self.options.limit,
isFirst: false}); self.pageHash(); }}); }Copy the code
The overall logic should be easy to understand, nothing more than to judge the current click is what, and then execute the corresponding logic operation, but the specific implementation method may be a little strange to some students.
Q: What is target? What is this srcElement? A: This is actually JavaScript event delegate knowledge, you can refer to the following article to learn, here is not repeated.
Js event delegate or event delegate details
With the plugin object, configuration, and event bindings complete, it’s time to finish rendering the DOM nodes shown on our page.
Rendering DOM
The rendering process is an improvement on the string printing functions we encapsulated above, by changing the string to a concrete DOM node and adding it to the page. First we need to complete a createHtml() function:
createHtml: function(elemDatas) {
var self = this;
var fragment = document.createDocumentFragment();
var liEle = document.createElement("li");
var aEle = document.createElement("a");
elemDatas.forEach(function(elementData, index) {
liEle = liEle.cloneNode(false);
aEle = aEle.cloneNode(false);
liEle.setAttribute("class", CLASS_NAME.ITEM);
aEle.setAttribute("href"."javascript:;");
aEle.setAttribute("id", elementData.id);
if(elementData.id ! = ='page') {
aEle.setAttribute("class", CLASS_NAME.LINK);
} else {
aEle.setAttribute("class", elementData.className);
}
aEle.innerHTML = elementData.content;
liEle.appendChild(aEle);
fragment.appendChild(liEle);
});
return fragment;
}
Copy the code
This function simply generates a node:
<li class="pagination-item"><a href="javascript:;" id="page" class="pagination-link current">1</a></li>
Copy the code
Code API involves two performance optimization, the first API is the document. The createDocumentFragment (), its role is to create a temporary placeholder, then the node to be inserted, DOM manipulation can effectively avoid the page redrawn and backflow, reduce the burden of the page, Improve page performance. Refer to the following articles for knowledge:
- JS performance optimization for creating document fragments
- Front-end Performance Optimization Chapter 3 -documentFragment
The second API is cloneNode(). If you need to create many elements, you can use this API to reduce the number of attribute Settings, but you must prepare a template node in the first place, for example:
var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
var el = document.createElement('p');
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
// Replace with:
var frag = document.createDocumentFragment();
var pEl = document.getElementsByTagName('p') [0];
for (var i = 0; i < 1000; i++) {
var el = pEl.cloneNode(false);
el.innerHTML = i;
frag.appendChild(el);
}
document.body.appendChild(frag);
Copy the code
Once this function is complete, it is further encapsulated into two functions that insert nodes :(this step can be omitted)
addFragmentBefore: function(fragment, datas) {
fragment.insertBefore(this.createHtml(datas), fragment.firstChild);
}
addFragmentAfter: function(fragment, datas) {
fragment.appendChild(this.createHtml(datas));
}
Copy the code
The former inserts nodes at the front and the latter at the end. Some constants and repeated operations can also be extracted further:
pageInfos: [{
id: "first".content: "Home page"
},
{
id: "prev".content: "Previous page"
},
{
id: "next".content: "Next page"
},
{
id: "last".content: "Back"
},
{
id: "".content: "..."
}
]
getPageInfos: function(className, content) {
return {
id: "page".className: className,
content: content
};
}
Copy the code
Using the objects and methods encapsulated above, we can modify the first two string functions:
renderNoEllipsis: function() {
var fragment = document.createDocumentFragment();
if (this.pageNumber < this.options.pageShow + 1) {
fragment.appendChild(this.renderDom(1.this.options.pageShow * 2 + 1));
} else if (this.pageNumber > this.pageCount - this.options.pageShow) {
fragment.appendChild(this.renderDom(this.pageCount - this.options.pageShow * 2.this.pageCount));
} else {
fragment.appendChild(this.renderDom(this.pageNumber - this.options.pageShow, this.pageNumber + this.options.pageShow));
}
if (this.pageNumber > 1) {
this.addFragmentBefore(fragment, [
this.pageInfos[0].this.pageInfos[1]]); }if (this.pageNumber < this.pageCount) {
this.addFragmentAfter(fragment, [this.pageInfos[2].this.pageInfos[3]]);
}
return fragment;
}
renderEllipsis: function() {
var fragment = document.createDocumentFragment();
this.addFragmentAfter(fragment, [
this.getPageInfos(CLASS_NAME.LINK + " current".this.pageNumber)
]);
for (var i = 1; i <= this.options.pageShow; i++) {
if (this.pageNumber - i > 1) {
this.addFragmentBefore(fragment, [
this.getPageInfos(CLASS_NAME.LINK, this.pageNumber - i)
]);
}
if (this.pageNumber + i < this.pageCount) {
this.addFragmentAfter(fragment, [
this.getPageInfos(CLASS_NAME.LINK, this.pageNumber + i) ]); }}if (this.pageNumber - (this.options.pageShow + 1) > 1) {
this.addFragmentBefore(fragment, [this.pageInfos[4]]);
}
if (this.pageNumber > 1) {
this.addFragmentBefore(fragment, [
this.pageInfos[0].this.pageInfos[1].this.getPageInfos(CLASS_NAME.LINK, 1)]); }if (this.pageNumber + this.options.pageShow + 1 < this.pageCount) {
this.addFragmentAfter(fragment, [this.pageInfos[4]]);
}
if (this.pageNumber < this.pageCount) {
this.addFragmentAfter(fragment, [
this.getPageInfos(CLASS_NAME.LINK, this.pageCount),
this.pageInfos[2].this.pageInfos[3]]); }return fragment;
}
renderDom: function(begin, end) {
var fragment = document.createDocumentFragment();
var str = "";
for (var i = begin; i <= end; i++) {
str = this.pageNumber === i ? CLASS_NAME.LINK + " current" : CLASS_NAME.LINK;
this.addFragmentAfter(fragment, [this.getPageInfos(str, i)]);
}
return fragment;
}
Copy the code
The logic is exactly the same as the original showPages(), except that it becomes a DOM operation.
At this point, the rendering part of the function is basically wrapped, and finally there are still some functions to operate the page number, relatively simple, here is not explained, can refer to the source code.
Usage scenarios
As you can see, this pager is only responsible for the logic of the page itself, and the specific data requests and renderings need to be done separately. However, this pager can not only be used in the general asynchronous paging, but also directly for a section of known data paging display, the application scenario is as follows:
The front page
The total data is processed in callback, and the current page needs to display the data
The back-end paging
The page number parameter on the URL can be used to locate the specified page number when the page is loaded, and the corresponding data under the specified page number can be requested to obtain the current page number in the callback function. The URL can be changed using window.location.href and the current page number can be used as the URL parameter. Then jump to the page, for example “./test.html? page=”
The plug-in call
Plug-ins are also very convenient to call. First, we introduce relevant CSS and JS files in the page:
<link rel="stylesheet" href="pagination.min.css">
<script type="text/javascript" src="pagination.min.js"></script>
Copy the code
If the style is not satisfactory, you can adjust it yourself
Then insert the HTML structure into the document:
<ol class="pagination" id="pagelist"></ol>
Copy the code
Finally, configure the required and optional parameters to complete the initialization of the split-page plug-in:
// Paging element ID (required)
var selector = '#pagelist';
// Paging configuration
var pageOption = {
// Number of data items per page (required)
limit: 5.// Total number of data (usually obtained from the back end, mandatory)
count: 162.// Current page number (optional, default is 1)
curr: 1.// Whether to display ellipsis (optional, default)
ellipsis: true.// The number of pages that can be displayed before and after the current page (optional, default is 2)
pageShow: 2.// Enable location.hash and customize the hash value (off by default)
// If enabled, the url is automatically appended when paging is triggered: #! Hash ={curr} With this, you can locate the page as soon as it loads
hash: false.// The default is once the page is loaded, and then again when the page is switched
callback: function(obj) {
// obj.curr: get the current page number
// obj.limit: Obtain the number of data items displayed on each page
// obj.isFirst: whether to load the page for the first time
// This is not executed for the first time
if(! obj.isFirst) {// do something}}};// Initialize the page splitter
new Pagination(selector, pageOption);
Copy the code
In addition to the two base modes, you can also enable Hash mode
So, this is the end of the pager plug-in package, isn’t that easy? To tell you the secret, we will gradually try more difficult mods. Stay tuned
To be fair, the overall code quality is mediocre, but I think the logic and structure is fairly clear. There must be a lot of deficiencies in the code, and I hope you can give me more advice!
Update (2018-7-29)
ES6- Environment configuration
In 2015, ECMAScript officially released its new version, ECMAScript6, which is a complete upgrade for the JavaScript language itself.
After this update, not only to repair the many ES5 era “pit”, but also on the original rules of grammar and increase a lot of powerful new features, despite the current browser support for new specification is not perfect, but after some magical tool processing can make the browser “know” these new things, and compatible with them.
So, why not use the mighty ES6? Let’s take a look at how these amazing tools are used.
Babel
First, we need a tool to transform ES6 code, which is called Babel. Babel is a compiler that converts source code into object code for the specified syntax and makes it run well in the runtime environment, so we can use it to compile our ES6 code.
To use babel-related features, you must first install them using NPM (learn how to use NPM and Node)
npm i babel-cli babel-preset-env babel-core babel-loader babel-plugin-transform-runtime babel-polyfill babel-runtime -D
After the installation is complete, we can manually use commands to compile the JS files in a directory and output them.
But, is this the perfect solution? Apparently not.
In a real development environment, there are more things to consider, such as modular development, automated compilation and build, so we need a more powerful tool to upgrade our build process.
Webpack
Onlookers: I know! You mean Gulp, right? !
Hey, wake up! The Qing dynasty is dead!
In today’s world of front-end frameworks and engineering, everyone is familiar with tools such as Webpack and Gulp. With them, we can easily realize the process of building, packaging and publishing a large front-end application. But now that it’s 2018, there are three frames, and Gulp is getting a little old, and a teenager named Webpack is emerging as his junior. Vue, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React, React In short, it plays the same role as Gulp on a project, but is simpler to configure and more efficient to build. Let’s see how Webpack is used.
If you don’t have access to Webpack, you can refer to the official documentation to get a general idea of Webpack. We won’t introduce it too much here, but just explain its installation and configuration.
As usual, we need to install it:
npm i webpack webpack-cli webpack-dev-server -D
It is also very simple to use, just create a configuration file called webpack.config.js:
const path = require('path');
module.exports = {
// Mode configuration
mode: 'development'.// Import file
entry: {},
// Export file
output: {},
// The corresponding plug-in
plugins: [],
// Process the corresponding module
module: {}}Copy the code
The main parts of this configuration file are: entrance, exit, plug-in and module. Before configuring them in detail, we can take a look at the package construction process of our project:
- Looking for to
./src/es6/
Subdirectoryindex.js
Project entry file - Compile it with Babel and all dependencies it references (Scss, CSS files, etc.)
- Zip the js file, configure it as the UMD specification, and rename it as
csdwheels.min.js
- empty
dist-es6
directory - Output to
dist-es6
directory
To clear directories, compress code, parse CSS, and so on, we need to install additional packages:
npm i clean-webpack-plugin uglifyjs-webpack-plugin css-loader style-loader node-sass sass-loader
To disable Babel in the configuration, you also need to create a. Babelrc file in which you specify encoding rules:
{
"presets": ["env"]}Copy the code
Finally, we can complete the configuration file:
const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin'); // Clean up the dist directory each build
module.exports = {
// Mode configuration
mode: 'development'.// Import file
entry: {
pagination: './src/es6/index.js'
},
// Export file
output: {
path: path.resolve(__dirname, 'dist-es6'),
filename: "csdwheels.min.js".libraryTarget: 'umd'.library: 'csdwheels'
},
// The corresponding plug-in
plugins: [
new CleanWebpackPlugin(['dist-es6']),
new UglifyJsPlugin({
test: /\.js($|\?) /i})].// Development server configuration
devServer: {},
// Process the corresponding module
module: {
rules: [{test: /\.js$/.include: path.join(__dirname , 'src/es6'),
exclude: /node_modules/.use: ['babel-loader'] {},test: /\.scss$/.use: [{
loader: 'style-loader'
}, {
loader: 'css-loader'
}, {
loader: 'sass-loader'}]}}Copy the code
It’s not enough to configure it well, we’ll need to run it with a command, in package.json:
"scripts": {
"test": "node test/test.js"."dev": "webpack-dev-server"."build": "webpack && gulp mini && npm run test"
}
Copy the code
Using dev, we can start a server to display the project, but we don’t need it for now. Instead, we can run the NPM run build command to package the source code in both our./ SRC /es5 and./ SRC /es6 directories and export it to the specified directory.
What happened to not using Gulp? Well.. Gulp is quite useful for ES5 packaging, so be warned!
The environment for ES6 development is finally configured, so let’s start refactoring the code.
ES6- Code refactoring
If you want to get started with ES6, ruan yifeng’s tutorial is highly recommended
There are a lot of new syntax and features, but it won’t take much more advanced features to refactor our project into ES6 for now, just focus on the Class section.
One of the most important new features introduced in ES6 is Class. It eliminates the need to use constructors to simulate object-oriented writing, because it’s an object-oriented syntactic sugar that JavaScript natively supports. The bottom layer is still a chain of prototypes, but at least the code looks the same.
Take the example of the plugin template mentioned earlier, which we wrote in ES5:
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module= = ='object' && module.exports) {
module.exports = factory();
} else {
root.Plugin = factory();
}
}(typeofself ! = ='undefined' ? self : this.function() {
'use strict';
// tool
function extend(o, n, override) {
for (var p in n) {
if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
o[p] = n[p];
}
}
// plugin construct function
function Plugin(selector, userOptions) {
// Plugin() or new Plugin()
if(! (this instanceof Plugin)) return new Plugin(selector, userOptions);
this.init(selector, userOptions)
}
Plugin.prototype = {
constructor: Plugin,
// default option
options: {},
init: function(selector, userOptions) {
extend(this.options, userOptions, true); }};return Plugin;
}));
Copy the code
With the new syntactic sugar Class, it looks like this:
// ES6 plug-in template
class Plugin {
constructor(selector, options = {}) {
this.options = {};
Object.assign(this.options, options);
this.init(selector, options);
}
init(selector, options) {}
}
export default Plugin;
Copy the code
The new code not only supports constructors directly at the syntactically level, but also does away with the bloated writing of IIFE, making it look and write clearer and smoother.
The built-in object.assign () method replaces our extend function with exactly the same functionality and is much more powerful
With the new template, we can start refactoring the plug-in code directly, Posting only a few major changes here, and refer to the source code for the rest
import '.. /.. /.. /style/pagination/pagination.scss'
class Pagination {
static CLASS_NAME = {
ITEM: 'pagination-item'.LINK: 'pagination-link'
}
static PAGE_INFOS = [{
id: "first".content: "Home page"
},
{
id: "prev".content: "Previous page"
},
{
id: "next".content: "Next page"
},
{
id: "last".content: "Back"
},
{
id: "".content: "..."}]constructor(selector, options = {}) {
// Default configuration
this.options = {
curr: 1.pageShow: 2.ellipsis: true.hash: false
};
Object.assign(this.options, options);
this.init(selector);
}
changePage () {
let pageElement = this.pageElement;
this.addEvent(pageElement, "click", (ev) => {
let e = ev || window.event;
let target = e.target || e.srcElement;
if (target.nodeName.toLocaleLowerCase() == "a") {
if (target.id === "prev") {
this.prevPage();
} else if (target.id === "next") {
this.nextPage();
} else if (target.id === "first") {
this.firstPage();
} else if (target.id === "last") {
this.lastPage();
} else if (target.id === "page") {
this.goPage(parseInt(target.innerHTML));
} else {
return;
}
this.renderPages();
this.options.callback && this.options.callback({
curr: this.pageNumber,
limit: this.options.limit,
isFirst: false
});
this.pageHash(); }}); } init(selector) {// Pager element
this.pageElement = this.$(selector)[0];
// Total number of data
this.dataCount = this.options.count;
// Current page number
this.pageNumber = this.options.curr;
/ / the total number of pages
this.pageCount = Math.ceil(this.options.count / this.options.limit);
/ / rendering
this.renderPages();
// Execute the callback function
this.options.callback && this.options.callback({
curr: this.pageNumber,
limit: this.options.limit,
isFirst: true
});
// Change the page count and trigger the event
this.changePage(); }}export default Pagination;
Copy the code
To sum up, the syntax used in this modification is as follows:
- Const, let instead of var
- Implement the constructor with constructor
- Arrow function replaces function
In addition, after installing the Sass compiler plug-in, we can import styles directly into the JS file. This way, the packed and compressed JS will also include our style code, so we don’t need to import style files. Finally, because ES6 does not support static attributes for classes, you need to use the static syntax of the new ES7 proposal. We can install the corresponding Babel package:
npm i babel-preset-stage-0 -D
After installation, add it to the.babelrc file:
{
"presets": ["env"."stage-0"]}Copy the code
Now all you need to do is run the NPM run build and see our packed csdWheels.min.js file.
Once packaged, we can also publish the NPM package by running the following command:
npm login
npm publish
To use the published plug-in, simply install the NPM package and import the corresponding plug-in:
npm i csdwheels -D
import { Pagination } from 'csdwheels';
Copy the code
Update 2018-08-01
Vue plug-in version
In accordance with the original development plan, is actually don’t want to update version of the Vue, immediately after all the series of “selling point” is the primary development, but recently made of Vue project and their blogs are just used to paging this component, so I decided to muster to write out a Vue version of this plug-in, just also took the opportunity to learn the Vue plugin development.
The development of specification
Since it is a framework, it must have its own development specification, similar to our own plug-in, it will also provide us with a variety of API interfaces, let us customize our own plug-in module. Simply put, our plug-in needs to be mounted globally in Vue so that it can be imported directly anywhere:
import Pagination from './components/vue-wheels-pagination'
const VueWheelsPagination = {
install (Vue, options) {
Vue.component(Pagination.name, Pagination)
}
}
if (typeof window! = ='undefined' && window.Vue) {
window.Vue.use(VueWheelsPagination)
}
export { VueWheelsPagination }
Copy the code
Vue-wheel-pagination is the single file component we will be developing. After importing it, mount it via install and then use it externally. Finally, export the object in which our plugin is mounted. (If the browser environment is detected, you can mount it directly.) This is about as simple a plug-in template as you can get. See the official documentation for more details.
With this entry wrapped in Webpack, you can load the plugin globally in main.js in your Vue project:
import { VueWheelsPagination } from 'vue-wheels'
Vue.use(VueWheelsPagination)
Copy the code
Next, let’s take a look at how to complete the paging plug-in using Vue.
DOM rendering
Using the bidirectional binding feature of modern MVVM framework, we no longer need to use native JS apis to directly manipulate the DOM. Instead, we can use apis provided by the framework to indirectly render and interact with the DOM structure:
<template lang="html">
<nav class="pagination">
<a href="javascript:;" class="pagination-item first" @click="goFirst()" v-if="pageNumber > 1">{{info.firstInfo}}</a>
<a href="javascript:;" class="pagination-item prev" @click="goPrev()" v-if="pageNumber > 1">{{info.prevInfo}}</a>
<ul class="pagination-list" v-if="ellipsis">
<li class="pagination-item" @click="goFirst()" v-if="pageNumber > 1">1</li>
<li class="pagination-item ellipsis" v-if="pageNumber - (max + 1) > 1">.</li>
<li class="pagination-item"
@click="goPage(pageNumber - pageIndex)"
v-if="pageNumber - pageIndex > 1"
v-for="pageIndex in rPageData"
:key="pageNumber - pageIndex">
{{pageNumber - pageIndex}}
</li>
<li class="pagination-item current" @click="goPage(pageNumber)">{{pageNumber}}</li>
<li class="pagination-item"
@click="goPage(pageNumber + pageIndex)"
v-if="pageNumber + pageIndex < pageCount"
v-for="pageIndex in pageData"
:key="pageNumber + pageIndex">
{{pageNumber + pageIndex}}
</li>
<li class="pagination-item ellipsis" v-if="pageNumber + max + 1 < pageCount">.</li>
<li class="pagination-item" @click="goLast()" v-if="pageNumber < pageCount">{{pageCount}}</li>
</ul>
<ul class="pagination-list" v-if=! "" ellipsis">
<li :class="pageIndex === pageNumber ? 'pagination-item current' : 'pagination-item'"
@click="goPage(pageIndex)"
v-for="pageIndex in pageDataFront"
v-if="pageNumber < max + 1"
:key="pageIndex">
{{pageIndex}}
</li>
<li :class="pageIndex === pageNumber ? 'pagination-item current' : 'pagination-item'"
@click="goPage(pageIndex)"
v-for="pageIndex in pageDataCenter"
v-if="pageNumber > pageCount - max"
:key="pageIndex">
{{pageIndex}}
</li>
<li :class="pageIndex === pageNumber ? 'pagination-item current' : 'pagination-item'"
@click="goPage(pageIndex)"
v-for="pageIndex in pageDataBehind"
v-if="max + 1 <= pageNumber && pageNumber <= pageCount - max"
:key="pageIndex">
{{pageIndex}}
</li>
</ul>
<a href="javascript:;" class="pagination-item next" @click="goNext()" v-if="pageNumber < pageCount">{{info.nextInfo}}</a>
<a href="javascript:;" class="pagination-item last" @click="goLast()" v-if="pageNumber < pageCount">{{info.lastInfo}}</a>
</nav>
</template>
Copy the code
As shown above, we did most of the rendering logic for the plugin directly in the template tag of the single-file component. Relative to the native JS version, not only easy to eliminate the event monitoring, DOM operations and other steps, and let us only focus on the specific interaction logic of the plug-in itself, can be said to greatly reduce the difficulty of development, and improve the page performance. The logic and interactive processing of the rest of the data can be completed in JS.
Interactive logic
export default {
name: 'VueWheelsPagination'.props: {
count: {
type: Number.required: true
},
limit: {
type: Number.required: true
},
curr: {
type: Number.required: false.default: 1
},
max: {
type: Number.required: false.default: 2
},
ellipsis: {
type: Boolean.required: false.default: true
},
info: {
type: Object.required: false.default: {
firstInfo: 'home'.prevInfo: 'Previous page'.nextInfo: 'Next page'.lastInfo: 'back'
}
}
},
data () {
return {
pageNumber: this.curr
}
},
watch: {
curr (newVal) {
this.pageNumber = newVal
}
},
computed: {
pageData () {
let pageData = []
for (let index = 1; index <= this.max; index++) {
pageData.push(index)
}
return pageData
},
rPageData () {
return this.pageData.slice(0).reverse()
},
pageDataFront () {
let pageDataFront = []
for (let index = 1; index <= this.max * 2 + 1; index++) {
pageDataFront.push(index)
}
return pageDataFront
},
pageDataCenter () {
let pageDataCenter = []
for (let index = this.pageCount - this.max * 2; index <= this.pageCount; index++) {
pageDataCenter.push(index)
}
return pageDataCenter
},
pageDataBehind () {
let pageDataBehind = []
for (let index = this.pageNumber - this.max; index <= this.pageNumber + this.max; index++) {
pageDataBehind.push(index)
}
return pageDataBehind
},
pageCount () {
return Math.ceil(this.count / this.limit)
}
},
methods: {
goFirst () {
this.pageNumber = 1
this.$emit('pageChange'.1)
},
goPrev () {
this.pageNumber--
this.$emit('pageChange'.this.pageNumber)
},
goPage (pageNumber) {
this.pageNumber = pageNumber
this.$emit('pageChange'.this.pageNumber)
},
goNext () {
this.pageNumber++
this.$emit('pageChange'.this.pageNumber)
},
goLast () {
this.pageNumber = this.pageCount
this.$emit('pageChange'.this.pageNumber)
}
}
}
Copy the code
The total is divided into several parts:
- The props property defines the types, default values, and mandatory configurations of the parameters passed by the parent component
- Initialize the data required by the pager itself in the compute property
- Defines methods for manipulating the page number and passing the current page number to the parent component
- Listen for page number changes in the Watch property (mainly for changing page numbers elsewhere without pagination)
This completes the development of the paging plug-in. As you can see, the amount of code for the paging logic is significantly reduced, and the logic of the plug-in itself is much clearer and easier to expand and maintain than the previous version we implemented from the ground up.
A call on an outer component might look something like this:
<template>
<div id="app">
<div class="main">
<vue-wheels-pagination @pageChange="change" :count="count" :limit="limit" :info="info"></vue-wheels-pagination>
</div>
</div>
</template>
Copy the code
export default {
name: 'app',
data () {
return {
count: 162.limit: 5.info: {
firstInfo: '< <'.prevInfo: '<'.nextInfo: '>'.lastInfo: '> >'}}},methods: {
change (pageNumber) {
console.log(pageNumber)
}
}
}
Copy the code
Pass in required and optional parameters, listen for the page number values bubbling back from the child component, and then jump to the corresponding logic in your own change() method.
The packaging process for the project is similar to that mentioned in the previous section, but with the addition of a local development environment server startup configuration, refer to my source code. Once packaged, you can also publish an NPM package, which can then be imported and used in any Vue project.
The next wheel of development will not necessarily be released in Vue, as it already provides an idea for refactoring and packaging plug-ins. If you have your own requirements, you can use the framework’s specifications for plug-in development.
By now, the development of our first wheel is really finished. All the source code has been synchronously updated to Github. If you find any bugs or other problems, you can reply to them in the issue of the project. Dig e a hole and run.
To be continued…
Original address: blog
This article is original, reproduced please famous source!
Refer to the content
- A series of knowledge points developed by anonymous functions
- Self-executing functions (IIFE)
- UMD (Universal Module Definition)
- A guide to writing native JavaScript plug-ins
- How to define a high-powered native JS plug-in
- How to write a simple page
- I summarize the js performance optimization of small knowledge
- Start | webpack Chinese website
- Webpack project construction :(I) basic architecture construction
- Webpack project construction :(2) ES6 compilation environment construction
- Vue – Guide – plug-in
- The first Vue plug-in went from package to release
- Vue wraps the plug-in and publishes it to NPM
- Vue wraps third-party plug-ins and publishes them to NPM