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:

  1. 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
  1. 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.

  1. 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:

  1. 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.
  2. Plug-ins need to have default setting parameters;
  3. 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.
  4. Plugins support chained calls;
  5. 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:

  1. Looking for to./src/es6/Subdirectoryindex.jsProject entry file
  2. Compile it with Babel and all dependencies it references (Scss, CSS files, etc.)
  3. Zip the js file, configure it as the UMD specification, and rename it ascsdwheels.min.js
  4. emptydist-es6directory
  5. Output todist-es6directory

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:

  1. Const, let instead of var
  2. Implement the constructor with constructor
  3. 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:

  1. The props property defines the types, default values, and mandatory configurations of the parameters passed by the parent component
  2. Initialize the data required by the pager itself in the compute property
  3. Defines methods for manipulating the page number and passing the current page number to the parent component
  4. 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