>웹 프론트엔드 >JS 튜토리얼 >기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

php是最好的语言
php是最好的语言원래의
2018-07-27 16:52:424071검색

이 글에서는 실용적이고 재사용이 가능한 바퀴 세트를 만드는 방법을 알려드립니다. 페이징 구성 및 기본 매개변수에 대한 설명 일상 업무에서 실제로 많은 수의 비즈니스가 반복되고 다른 사람의 플러그인을 사용하면 일부 사용자 정의 요구 사항을 잘 해결할 수 없으므로 일반적으로 사용되는 일부 구성 요소를 분리하기로 결정했습니다. 이를 캡슐화하여 자체 플러그인 라이브러리를 형성합니다. 여기서는 이 튜토리얼 시리즈를 사용하여 각 플러그인의 개발 과정을 기록하겠습니다

자, 시작하겠습니다!

현재 프로젝트는 ES5 및 UMD 표준 패키징을 사용하므로 프런트엔드는 <script> </ code> 태그가 도입되는 방식은 향후 ES6를 사용하여 점진적으로 리팩토링될 예정입니다<code><script></script>标签的引入方式,未来会逐步用 ES6 进行重构

演示地址:pagination
Github:csdwheels
不要吝啬你的Star哦~(〃'▽'〃)

기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

JavaScript模块化

要开发一个JavaScript的插件,首先要从JavaScript的模块化讲起。
什么是模块化?简单的说就是让JavaScript能够以一个整体的方式去组织和维护代码,当多人开发时可以互相引用对方的代码块又不造成冲突。
ECMAScript6标准之前常见的模块化规范有:CommonJSAMDUMD等,因为我们的代码暂时是采用ES5语法进行开发,所以我们选用UMD的规范来组织代码。
关于模块化的发展过程可以参考:

  • JavaScript模块化编程简史(2009-2016)

  • JavaScript模块演化简史

在这种模块规范的标准之上,我们还需要一种机制来加载不同的模块,例如实现了AMD规范的require.js,其用法可以参考阮一峰写的这篇教程:

  • Javascript模块化编程(三):require.js的用法

因为我们开发的轮子暂时不涉及到多模块加载,所以模块的加载暂时不予过多讨论,读者可自己进行拓展学习。

回到我们的主题上,在正式开发之前,还需要补充一点其他方面的知识。

自执行函数

定义一个函数,ES5一般有三种方式:

  • 函数声明

function foo () {}

这样声明的函数和变量一样,会被自动提升,所以我们可以把函数声明放在调用它的语句后面:

foo();
function foo () {}
  • 函数表达式

var foo = function () {}

右边其实是一个匿名函数,只不过赋值给了一个变量,我们可以通过这个变量名来调用它,但是和第一种方式不同的是,通过表达式声明的函数不会被提升。

  • 使用Function构造函数

var foo = new Function ()

那么有没有一种办法,可以不写函数名,直接声明一个函数并自动调用它呢?
答案肯定的,那就是使用自执行函数。(实际上我的另一篇文章打砖块——js面向对象初识中就曾提到过)

自执行函数Immediately-Invoked Function Expression,顾名思义,就是自动执行的函数,有的地方也称为立即调用的函数表达式。
它的基本形式如下:

(function () {
    console.log(&#39;hello&#39;)
}());

(function () {
    console.log(&#39;hello&#39;)
})();
两种写法是等效的,只不过前者让代码看起来更像是一个整体。

可以看到,这两种写法的作用其实就是在()内定义函数,然后又使用()来执行该函数,因此它就是自执行的。

IIFE的一些好处如下:

  • 避免污染全局变量

  • 减少命名冲突

  • 惰性加载

最重要的一点,它可以创建一个独立的作用域,而在ES6之前JavaScript是没有块级作用域的。
利用这一点,我们可以很轻松的保证多个模块之间的变量不被覆盖了:

// libA.js
(function(){
  var num = 1;
})();

// libB.js
(function(){
    var num = 2;
})();

上面这两个文件模块中的作用域都是独立的,互不影响。(如果模块之间想要互相引用,就需要用到模块的加载器了,例如上面提到的require.js데모 주소: pagination
Github: csdwheels
인색하지마 너의 별~(〃'▽'〃)

기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

JavaScript 모듈화

JavaScript 플러그인을 개발하려면 먼저 JavaScript 모듈화부터 시작해야 합니다.
모듈화란 무엇입니까? 간단히 말해서, JavaScript는 여러 사람이 개발할 때 충돌을 일으키지 않고 서로의 코드 블록을 참조할 수 있도록 전체적인 방식으로 코드를 구성하고 유지 관리할 수 있습니다.
ECMAScript6 표준 이전의 일반적인 모듈 사양에는 CommonJS, AMD, UMD 등이 있습니다. 우리 코드는 현재 ES5 구문을 사용하여 개발되었으므로 코드 구성을 위해 UMD 사양을 선택합니다.
모듈화 개발 과정은 다음을 참조하세요.

    JavaScript 모듈러 프로그래밍의 간략한 역사(2009-2016)
  1. JavaScript의 간략한 역사 JavaScript 모듈의 진화
  2. 이 모듈 사양 표준 외에도 AMD 사양을 구현하는 require.js와 같은 다양한 모듈을 로드하는 메커니즘도 필요합니다. 사용법은 이 튜토리얼을 참조하세요. 작성자: Ruan Yifeng:

      🎜🎜Javascript 모듈형 프로그래밍(3): require.js 사용🎜🎜
    🎜우리가 개발하는 휠에는 다중 모듈이 포함되지 않기 때문입니다. loading 모듈은 당분간 너무 많이 다루지 않을 것이며, 독자들은 스스로 연구를 확장할 수 있습니다. 🎜🎜본 주제로 돌아가서, 정식 개발에 앞서 지식의 다른 측면을 추가해야 합니다. 🎜🎜자체 실행 함수🎜🎜ES5에서 함수를 정의하는 방법은 일반적으로 세 가지가 있습니다: 🎜
      🎜🎜함수 선언🎜🎜
    // if the module has no dependencies, the above pattern can be simplified to
    (function (root, factory) {
        if (typeof define === &#39;function&#39; && define.amd) {
            // AMD. Register as an anonymous module.
            define([], factory);
        } else if (typeof module === &#39;object&#39; && 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();
      }
    }(typeof self !== &#39;undefined&#39; ? 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 {};
    }));
    🎜여기서 선언된 함수 way는 변수와 동일하며 자동으로 승격되므로 이를 호출하는 문 뒤에 함수 선언을 넣을 수 있습니다: 🎜
    function extend(o, n, override) {
        for (var p in n) {
            if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
            o[p] = n[p];
        }
    }
    
    // 默认参数
    var options = {
        pageNumber: 1,
        pageShow: 2
    };
    
    // 用户设置
    var userOptions = {
        pageShow: 3,
        pageCount: 10
    }
    
    extend(options, userOptions, true);
    
    // 合并后
    options = {
        pageNumber: 1,
        pageShow: 3,
        pageCount: 10
    }
      🎜🎜Function 표현식🎜🎜
    ;// JavaScript弱语法的特点,如果前面刚好有个函数没有以";"结尾,那么可能会有语法错误
    (function(root, factory) {
      if (typeof define === &#39;function&#39; && define.amd) {
        define([], factory);
      } else if (typeof module === &#39;object&#39; && module.exports) {
        module.exports = factory();
      } else {
        root.Plugin = factory();
      }
    }(typeof self !== &#39;undefined&#39; ? self : this, function() {
      &#39;use strict&#39;;
    
      // 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) {
          // 添加绑定
          if (element.addEventListener) {
            // 使用DOM2级方法添加事件
            element.addEventListener(type, handler, false);
          } else if (element.attachEvent) {
            // 使用IE方法添加事件
            element.attachEvent("on" + type, handler);
          } else {
            // 使用DOM0级方法添加事件
            element["on" + type] = handler;
          }
        },
        // 移除事件
        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) {
          // 返回事件对象引用
          return event ? event : window.event;
        },
        // 获取mouseover和mouseout相关元素
        getRelatedTarget: function(event) {
          if (event.relatedTarget) {
            return event.relatedTarget;
          } else if (event.toElement) {
            // 兼容IE8-
            return event.toElement;
          } else if (event.formElement) {
            return event.formElement;
          } else {
            return null;
          }
        },
        getTarget: function(event) {
          //返回事件源目标
          return event.target || event.srcElement;
        },
        preventDefault: function(event) {
          //取消默认事件
          if (event.preventDefault) {
            event.preventDefault();
          } else {
            event.returnValue = false;
          }
        },
        stoppropagation: function(event) {
          //阻止事件流
          if (event.stoppropagation) {
            event.stoppropagation();
          } else {
            event.canceBubble = false;
          }
        },
        // 获取mousedown或mouseup按下或释放的按钮是鼠标中的哪一个
        getButton: function(event) {
          if (document.implementation.hasFeature("MouseEvents", "2.0")) {
            return event.button;
          } else {
            //将IE模型下的button属性映射为DOM模型下的button属性
            switch (event.button) {
              case 0:
              case 1:
              case 3:
              case 5:
              case 7:
                //按下的是鼠标主按钮(一般是左键)
                return 0;
              case 2:
              case 6:
                //按下的是中间的鼠标按钮
                return 2;
              case 4:
                //鼠标次按钮(一般是右键)
                return 1;
            }
          }
        },
        //获取表示鼠标滚轮滚动方向的数值
        getWheelDelta: function(event) {
          if (event.wheelDelta) {
            return event.wheelDelta;
          } else {
            return -event.detail * 40;
          }
        },
        // 以跨浏览器取得相同的字符编码,需在keypress事件中使用
        getCharCode: function(event) {
          if (typeof event.charCode == "number") {
            return event.charCode;
          } else {
            return event.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;
    }));
    🎜오른쪽은 실제로는 익명 함수는 그냥 변수에 할당한 것 뿐입니다. 이 변수 ​​이름을 통해 호출할 수 있지만 첫 번째 방법과 달리 표현식을 통해 선언된 함수는 승격되지 않습니다. 🎜
      🎜🎜함수 생성자를 사용하세요🎜🎜
    // 总共30页
    // 第一种情况:不显示省略号,当前页码前后最多显示2个页码
    当前页码为 1,那么显示 1 2 3 4 5
    当前页码为 2,那么显示 1 2 3 4 5
    当前页码为 3,那么显示 1 2 3 4 5
    当前页码为 4,那么显示 2 3 4 5 6
    ...
    当前页码为 15,那么显示 13 14 15 16 17
    ...
    当前页码为 27,那么显示 25 26 27 28 29
    当前页码为 28,那么显示 26 27 28 29 30
    当前页码为 29,那么显示 26 27 28 29 30
    当前页码为 30,那么显示 26 27 28 29 30
    🎜그렇다면 함수 이름을 쓰지 않고 직접 함수를 선언하고 자동으로 호출할 수 있는 방법이 있을까요?
    대답은 '예'입니다. 즉, 자체 실행 기능을 사용하는 것입니다. (실제로 내 다른 기사 Breaking Bricks - A First Introduction to JS Object-Oriented에서 언급한 적이 있습니다.) 🎜🎜자체 실행 함수 즉시 호출 함수 표현식은 이름에서 알 수 있듯이 자동으로 실행된 함수는 어떤 곳에서는 함수 표현식이라고 즉시 호출되기도 합니다.
    기본 형태는 다음과 같습니다. 🎜
    function showPages (page, total, show) {
    
    }
    🎜두 가지 작성 방법은 동일하지만 전자를 사용하면 코드가 더 전체처럼 보입니다. 🎜이 두 가지 작성 방법의 기능은 실제로 () 내에 함수를 정의한 다음 ()를 사용하여 함수를 실행하는 것이므로 자체 실행되는 것을 볼 수 있습니다. 🎜🎜IIFE의 몇 가지 이점은 다음과 같습니다. 🎜
      🎜🎜전역 변수 오염 방지🎜🎜🎜🎜이름 충돌 감소🎜🎜🎜🎜지연 로딩🎜🎜
    🎜The 가장 중요한 것은 독립적인 범위를 생성할 수 있다는 점이며, ES6 이전에는 JavaScript에는 블록 수준 범위가 없었습니다.
    이를 사용하면 여러 모듈 간의 변수가 덮어쓰여지지 않도록 쉽게 확인할 수 있습니다. 🎜
    var total = 30;
    for (var i = 1; i <= total; i++) {
        console.log(showPages(i, total));
    }
    🎜위 두 파일 모듈의 범위는 독립적이며 서로 영향을 미치지 않습니다. (모듈이 서로 참조하려면 require.js 및 위에서 언급한 다른 라이브러리와 같은 모듈 로더를 사용해야 합니다.) 🎜🎜이를 바탕으로 우리는 무엇을 볼 수 있는지 살펴보겠습니다. UMD 사양을 구현한 IIFE 템플릿은 다음과 같습니다. 🎜
    function showPages (page, total, show) {
        var str = &#39;&#39;;
        if (page < show + 1) {
            for (var i = 1; i <= show * 2 + 1; i++) {
                str = str + &#39; &#39; + i;
            }
        } else if (page > total - show) {
            for (var i = total - show * 2; i <= total; i++) {
                str = str + &#39; &#39; + i;
            }
        } else {
            for (var i = page - show; i <= page + show; i++) {
                str = str + &#39; &#39; + i;
            }
        }
        return str.trim();
    }
    🎜 UMD 사양이 브라우저, Node 환경 및 AMD 사양과 동시에 호환되므로 우리 코드를 UMD로 패키징한 후 다른 환경에서 사용할 수 있음을 알 수 있습니다. . 실행 중입니다. 🎜🎜플러그인 템플릿🎜🎜플러그인 개발에서 가장 중요한 점은 플러그인의 호환성입니다. 플러그인은 최소한 여러 환경에서 동시에 실행될 수 있어야 합니다. 둘째, 다음 기능과 조건도 충족해야 합니다. 🎜🎜🎜🎜플러그인의 범위는 사용자의 현재 범위와 독립적입니다. 즉, 플러그인 내부의 개인 변수는 사용자의 환경 변수에 영향을 미칠 수 없습니다. 🎜🎜🎜🎜 플러그인에는 기본 설정 매개변수가 있어야 합니다.🎜
  3. 插件除了具备已实现的基本功能外,需提供部分API,使用者可以通过该API修改插件功能的默认参数,从而实现用户自定义插件效果;

  4. 插件支持链式调用;

  5. 插件需提供监听入口,及针对指定元素进行监听,使得该元素与插件响应达到插件效果。

第一点我们利用UMD包装的方式已经实现了,现在来看看第二和第三点。

通常情况下,一个插件内部会有默认参数,并且会提供一些参数让用户实现部分功能的自定义。那么怎么实现呢,这其实就是一个对象合并的问题,例如:

function extend(o, n, override) {
    for (var p in n) {
        if (n.hasOwnProperty(p) && (!o.hasOwnProperty(p) || override))
        o[p] = n[p];
    }
}

// 默认参数
var options = {
    pageNumber: 1,
    pageShow: 2
};

// 用户设置
var userOptions = {
    pageShow: 3,
    pageCount: 10
}

extend(options, userOptions, true);

// 合并后
options = {
    pageNumber: 1,
    pageShow: 3,
    pageCount: 10
}

如上,采用一个类似的extend函数就可以实现对象的合并了,这样我们插件也就实现了设置参数的功能。

这里的extend函数为浅拷贝,因为插件的用户参数一般是不会修改的,如果想实现深拷贝可参考jQuery中extend的实现方法。

第四点我们插件暂时不需要这样的功能,可以暂时不支持它。第五点在代码中我们会通过回调函数去逐步实现它。

综上,我们就可以实现出一个基础的插件模板了:

;// JavaScript弱语法的特点,如果前面刚好有个函数没有以";"结尾,那么可能会有语法错误
(function(root, factory) {
  if (typeof define === &#39;function&#39; && define.amd) {
    define([], factory);
  } else if (typeof module === &#39;object&#39; && module.exports) {
    module.exports = factory();
  } else {
    root.Plugin = factory();
  }
}(typeof self !== &#39;undefined&#39; ? self : this, function() {
  &#39;use strict&#39;;

  // 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) {
      // 添加绑定
      if (element.addEventListener) {
        // 使用DOM2级方法添加事件
        element.addEventListener(type, handler, false);
      } else if (element.attachEvent) {
        // 使用IE方法添加事件
        element.attachEvent("on" + type, handler);
      } else {
        // 使用DOM0级方法添加事件
        element["on" + type] = handler;
      }
    },
    // 移除事件
    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) {
      // 返回事件对象引用
      return event ? event : window.event;
    },
    // 获取mouseover和mouseout相关元素
    getRelatedTarget: function(event) {
      if (event.relatedTarget) {
        return event.relatedTarget;
      } else if (event.toElement) {
        // 兼容IE8-
        return event.toElement;
      } else if (event.formElement) {
        return event.formElement;
      } else {
        return null;
      }
    },
    getTarget: function(event) {
      //返回事件源目标
      return event.target || event.srcElement;
    },
    preventDefault: function(event) {
      //取消默认事件
      if (event.preventDefault) {
        event.preventDefault();
      } else {
        event.returnValue = false;
      }
    },
    stoppropagation: function(event) {
      //阻止事件流
      if (event.stoppropagation) {
        event.stoppropagation();
      } else {
        event.canceBubble = false;
      }
    },
    // 获取mousedown或mouseup按下或释放的按钮是鼠标中的哪一个
    getButton: function(event) {
      if (document.implementation.hasFeature("MouseEvents", "2.0")) {
        return event.button;
      } else {
        //将IE模型下的button属性映射为DOM模型下的button属性
        switch (event.button) {
          case 0:
          case 1:
          case 3:
          case 5:
          case 7:
            //按下的是鼠标主按钮(一般是左键)
            return 0;
          case 2:
          case 6:
            //按下的是中间的鼠标按钮
            return 2;
          case 4:
            //鼠标次按钮(一般是右键)
            return 1;
        }
      }
    },
    //获取表示鼠标滚轮滚动方向的数值
    getWheelDelta: function(event) {
      if (event.wheelDelta) {
        return event.wheelDelta;
      } else {
        return -event.detail * 40;
      }
    },
    // 以跨浏览器取得相同的字符编码,需在keypress事件中使用
    getCharCode: function(event) {
      if (typeof event.charCode == "number") {
        return event.charCode;
      } else {
        return event.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;
}));

这里还使用到了一个EventUtil对象,它主要是针对事件注册的一些兼容性做了一些polyfill封装,具体原理可以参阅:

  • EventUtil——跨浏览器的事件对象

  • 跨浏览器的事件对象-------EventUtil 中的方法及用法

到此,一个插件的基本模板就大致成型了。下一节,我们终于可以正式开始分页插件的开发了!

思路分析

有人说计算机的本质就是对现实世界的抽象,而编程则是对这个抽象世界规则的制定。

正如上面这句话所说,在实际编码之前我们一般需要对要实现的需求效果进行一个思路的分析,最后再进一步把这个思路过程抽象为有逻辑的代码。
我们先看一下要实现的分页效果是什么样的,我把它分成两种情况,显示和不显示省略号的,首先来看第一种:

// 总共30页
// 第一种情况:不显示省略号,当前页码前后最多显示2个页码
当前页码为 1,那么显示 1 2 3 4 5
当前页码为 2,那么显示 1 2 3 4 5
当前页码为 3,那么显示 1 2 3 4 5
当前页码为 4,那么显示 2 3 4 5 6
...
当前页码为 15,那么显示 13 14 15 16 17
...
当前页码为 27,那么显示 25 26 27 28 29
当前页码为 28,那么显示 26 27 28 29 30
当前页码为 29,那么显示 26 27 28 29 30
当前页码为 30,那么显示 26 27 28 29 30

虽然上面每一个数字在实际应用中都是一个按钮或超链接,但现在既然是分析,我们不妨就把它简化并忽略,于是这个问题就变成了一个简单的字符串输出题。
我们先定义一个函数:

function showPages (page, total, show) {

}

函数传入的参数分别为:当前页码、总页码数、当前页码面前后最多显示页码数,然后我们需要循环调用这个函数打印分页:

var total = 30;
for (var i = 1; i <= total; i++) {
    console.log(showPages(i, total));
}

这样从页码为1到最后一页的结果就全输出了,最后我们需要完成showPages()函数:

function showPages (page, total, show) {
    var str = &#39;&#39;;
    if (page < show + 1) {
        for (var i = 1; i <= show * 2 + 1; i++) {
            str = str + &#39; &#39; + i;
        }
    } else if (page > total - show) {
        for (var i = total - show * 2; i <= total; i++) {
            str = str + &#39; &#39; + i;
        }
    } else {
        for (var i = page - show; i <= page + show; i++) {
            str = str + &#39; &#39; + i;
        }
    }
    return str.trim();
}

思路是分段拼出页码,打印结果如下:
기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

不显示省略号的页码正常输出了,然后我们来看显示省略号的情况:

// 第二种情况:显示省略号,当前页码前后最多显示2个页码
当前页码为 1,那么显示 1 2 3 ... 30
当前页码为 2,那么显示 1 2 3 4 ... 30
当前页码为 3,那么显示 1 2 3 4 5 ... 30
当前页码为 4,那么显示 1 2 3 4 5 6 ... 30
当前页码为 5,那么显示 1 ... 3 4 5 6 7 ... 30
...
当前页码为 15,那么显示 1 ... 13 14 15 16 17 ... 30
...
当前页码为 26,那么显示 1 ... 24 25 26 27 28 ... 30
当前页码为 27,那么显示 1 ... 25 26 27 28 29 30
当前页码为 28,那么显示 1 ... 26 27 28 29 30
当前页码为 29,那么显示 1 ... 27 28 29 30
当前页码为 30,那么显示 1 ... 28 29 30

同样需要完成showPages()函数:

function showPages(page, length, show) {
    var str = &#39;&#39;;
    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 + &#39; &#39; + i;
            } else {
                str = str + &#39; ... &#39; + 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 + &#39; &#39; + str;
            } else {
                str = &#39;1 ... &#39; + str;
                break;
            }
        }
    } else {
        for (var i = preIndex + 1; i <= aftIndex - 1; i++) {
            str = str + &#39; &#39; + i;
        }
        str = &#39;1 ... &#39; + str + &#39; ... &#39; + total;
    }
    return str.trim();
}

同样也是采用分段拼的思路,能成功打印出结果:
기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?

但是仔细看看上面的代码会发现有大量重复冗余的逻辑了,能不能优化呢?下面是一种更为取巧的思路:

function showPages (page, total, show) {
    var str = page + &#39;&#39;;
    for (var i = 1; i <= show; i++) {
        if (page - i > 1) {
            str = page - i + &#39; &#39; + str;
        }
        if (page + i < total) {
            str = str + &#39; &#39; + (page + i);
        }
    }
    if (page - (show + 1) > 1) {
        str = &#39;... &#39; + str;
    }
    if (page > 1) {
        str = 1 + &#39; &#39; + str;
    }
    if (page + show + 1 < total) {
        str = str + &#39; ...&#39;;
    }
    if (page < total) {
        str = str + &#39; &#39; + total;
    }
    return str;
}

打印结果是一样的,但代码却大为精简了。

基本架构

一个好的插件,代码一定是高复用、低耦合、易拓展的,因此我们需要采用面向对象的方法来搭建这个插件的基本架构:

// 模仿jQuery $()
function $(selector, context) {
    context = arguments.length > 1 ? context : document;
    return context ? context.querySelectorAll(selector) : null;
}

var Pagination = function(selector, pageOption) {
    // 默认配置
    this.options = {
        curr: 1,
        pageShow: 2,
        ellipsis: true,
        hash: false
    };
    // 合并配置
    extend(this.options, pageOption, true);
    // 分页器元素
    this.pageElement = $(selector)[0];
    // 数据总数
    this.dataCount = this.options.count;
    // 当前页码
    this.pageNumber = this.options.curr;
    // 总页数
    this.pageCount = Math.ceil(this.options.count / this.options.limit);
    // 渲染
    this.renderPages();
    // 执行回调函数
    this.options.callback && this.options.callback({
        curr: this.pageNumber,
        limit: this.options.limit,
        isFirst: true
    });
    // 改变页数并触发事件
    this.changePage();
};

Pagination.prototype = {
    constructor: Pagination,
    changePage: function() {}
};

return Pagination;

如上,一个采用原型模式的分页器对象就搭建完成了,下面我们对上面的代码进行一一讲解。

分页配置

本分页器提供如下基本参数:

// 分页元素ID(必填)
var selector = &#39;#pagelist&#39;;

// 分页配置
var pageOption = {
  // 每页显示数据条数(必填)
  limit: 5,
  // 数据总数(一般通过后端获取,必填)
  count: 162,
  // 当前页码(选填,默认为1)
  curr: 1,
  // 是否显示省略号(选填,默认显示)
  ellipsis: true,
  // 当前页前后两边可显示的页码个数(选填,默认为2)
  pageShow: 2,
  // 开启location.hash,并自定义hash值 (默认关闭)
  // 如果开启,在触发分页时,会自动对url追加:#!hash值={curr} 利用这个,可以在页面载入时就定位到指定页
  hash: false,
  // 页面加载后默认执行一次,然后当分页被切换时再次触发
  callback: function(obj) {
    // obj.curr:获取当前页码
    // obj.limit:获取每页显示数据条数
    // obj.isFirst:是否首次加载页面,一般用于初始加载的判断

    // 首次不执行
    if (!obj.isFirst) {
      // do something
    }
  }
};

在构造函数里调用extend()完成了用户参数与插件默认参数的合并。

回调事件

通常情况下,在改变了插件状态后(点击事件等),插件需要作出一定的反应。因此我们需要对用户行为进行一定的监听,这种监听习惯上就叫作回调函数。
在上面代码中我们可以看到有这么一段:

// 执行回调函数
this.options.callback && this.options.callback({
    curr: this.pageNumber,
    limit: this.options.limit,
    isFirst: true
});

这种写法是不是有点奇怪呢,其实它相当于:

if(this.options.callback){
    this.options.callback({
        curr: this.pageNumber,
        limit: this.options.limit,
        isFirst: true
    });
}

想必聪明的你已经明白了吧,这里的callback并不是某个具体的东西,而是一个引用。不管callback指向谁,我们只需要判断它有没有存在,如果存在就执行它。

事件绑定

接下来需要对分页器进行点击事件的绑定,也就是完成我们的changePage()方法:

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();
        }
    });
}

整体的逻辑大家应该都能轻松看懂,无非就是判断当前点击的是什么,然后执行对应的逻辑操作,但具体的实现方式有的同学可能会有一点陌生。

Q:这个target是啥?这个srcElement又是啥?
A:这其实是JavaScript事件委托方面的知识,大家可以参考如下文章进行学习,这里不再赘述。

js中的事件委托或是事件代理详解

插件对象、配置完成了,事件也绑定了,那接下来就应该完成我们页码上显示的DOM节点的渲染了。

渲染DOM

渲染的过程其实就是对上面我们封装的那几个字符串打印函数的改进,把字符串改为具体的DOM节点,然后添加进页面即可。
首先我们需要完成一个createHtml()函数:

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 !== &#39;page&#39;) {
      aEle.setAttribute("class", CLASS_NAME.LINK);
    } else {
      aEle.setAttribute("class", elementData.className);
    }
    aEle.innerHTML = elementData.content;
    liEle.appendChild(aEle);
    fragment.appendChild(liEle);
  });
  return fragment;
}

这个函数的作用很简单,就是生成一个节点:

<li class="pagination-item"><a href="javascript:;" id="page" class="pagination-link current">1</a></li>

代码中有涉及到两个性能优化的API,第一个API是document.createDocumentFragment(),它的作用是创建一个临时占位符,然后存放那些需要插入的节点,可以有效避免页面进行DOM操作时的重绘和回流,减小页面的负担,提升页面性能。相关知识点,可参阅以下文章:

  • JS性能优化之创建文档碎片

  • 前端性能优化第三篇-documentFragment

第二个API是cloneNode(),如果需要创建很多元素,就可以利用这个API来减少属性的设置次数,不过必须先提前准备一个样板节点,例如:

var frag = document.createDocumentFragment();
for (var i = 0; i < 1000; i++) {
    var el = document.createElement(&#39;p&#39;);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);
//替换为:
var frag = document.createDocumentFragment();
var pEl = document.getElementsByTagName(&#39;p&#39;)[0];
for (var i = 0; i < 1000; i++) {
    var el = pEl.cloneNode(false);
    el.innerHTML = i;
    frag.appendChild(el);
}
document.body.appendChild(frag);

完成这个函数后,再进一步封装成两个插入节点的函数:(这一步可省略)

addFragmentBefore: function(fragment, datas) {
  fragment.insertBefore(this.createHtml(datas), fragment.firstChild);
}

addFragmentAfter: function(fragment, datas) {
  fragment.appendChild(this.createHtml(datas));
}

前者在最前插入节点,后者在最后插入节点。
一些常量和重复操作也可以进一步抽取:

pageInfos: [{
    id: "first",
    content: "首页"
  },
  {
    id: "prev",
    content: "前一页"
  },
  {
    id: "next",
    content: "后一页"
  },
  {
    id: "last",
    content: "尾页"
  },
  {
    id: "",
    content: "..."
  }
]

getPageInfos: function(className, content) {
  return {
    id: "page",
    className: className,
    content: content
  };
}

利用上面封装好的对象和方法,我们就可以对最开始那两个字符串函数进行改造了:

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;
}

逻辑和最开始的showPages()完全一样,只是变成了DOM的操作而已。

至此,渲染部分的函数基本也封装完成,最后还剩一些操作页码的函数,比较简单,这里就不作讲解了,可自行参考源码。

使用场景

相信大家也看出来了,此分页器只负责分页本身的逻辑,具体的数据请求与渲染需要另外去完成。
不过,此分页器不仅能应用在一般的异步分页上,还可直接对一段已知数据进行分页展现,使用场景如下:

前端分页

在callback里对总数据进行处理,然后取出当前页需要展示的数据即可

后端分页

利用url上的页码参数,可以在页面载入时就定位到指定页码,并且可以同时请求后端指定页码下对应的数据 在callback回调函数里取得当前页码,可以使用window.location.href改变url,并将当前页码作为url参数,然后进行页面跳转,例如"./test.html?page="

插件调用

插件的调用也非常方便,首先,我们在页面引入相关的CSS、JS文件:

<link rel="stylesheet" href="pagination.min.css">
<script type="text/javascript" src="pagination.min.js"></script>
样式如果觉得不满意可自行调整

然后将HTML结构插入文档中:

<ol class="page-navigator" id="pagelist"></ol>

最后,将必填、选填的参数配置好即可完成本分页插件的初始化:

// 分页元素ID(必填)
var selector = &#39;#pagelist&#39;;

// 分页配置
var pageOption = {
  // 每页显示数据条数(必填)
  limit: 5,
  // 数据总数(一般通过后端获取,必填)
  count: 162,
  // 当前页码(选填,默认为1)
  curr: 1,
  // 是否显示省略号(选填,默认显示)
  ellipsis: true,
  // 当前页前后两边可显示的页码个数(选填,默认为2)
  pageShow: 2,
  // 开启location.hash,并自定义hash值 (默认关闭)
  // 如果开启,在触发分页时,会自动对url追加:#!hash值={curr} 利用这个,可以在页面载入时就定位到指定页
  hash: false,
  // 页面加载后默认执行一次,然后当分页被切换时再次触发
  callback: function(obj) {
    // obj.curr:获取当前页码
    // obj.limit:获取每页显示数据条数
    // obj.isFirst:是否首次加载页面,一般用于初始加载的判断

    // 首次不执行
    if (!obj.isFirst) {
      // do something
    }
  }
};

// 初始化分页器
new Pagination(selector, pageOption);
在两种基础模式之上,还可以开启Hash模式

那么,整个分页器插件的封装到这里就全部讲解完毕了,怎么样,是不是觉得还挺简单?偷偷告诉你,接下来我们会逐渐尝试点更有难度的插件哦!敬请期待~~

平心而论,整体的代码质量虽然一般,但是逻辑和结构我觉得还是写得算比较清晰的吧。代码的不足之处肯定还有很多,也希望各位看官多多指教!

To be continued...apache php mysql

参考内容

  • 由匿名函数展开的一系列知识点

  • 自执行函数(IIFE)

  • UMD (Universal Module Definition)

  • 原生JavaScript插件编写指南

  • 如何定义一个高逼格的原生JS插件

  • 如何写一个简单的分页

  • 我总结的js性能优化的小知识

相关文章:

原生JS绑定滑轮滚动事件兼容常见浏览器_javascript技巧

原生javascript实现图片轮播效果代码

相关视频:

弹指间学会JavaScript 教程

위 내용은 기본 JavaScript를 사용하여 바퀴를 만드세요. 이 기사를 읽고 나면 그 방법을 알게 되셨나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.