手把手教你用原生JavaScript造轮子(1)——分页器(最后更新:Vue插件版本,本篇Over!)...

日常工作中经常会发现有大量业务逻辑是重复的,而用别人的插件也不能完美解决一些定制化的需求,所以我决定把一些常用的组件抽离、封装出来,形成一套自己的插件库。同时,我将用这个教程系列记录下每一个插件的开发过程,手把手教你如何一步一步去造出一套实用性、可复用性高的轮子。

So, Let's begin!

目前项目使用 ES5及UMD 规范封装,所以在前端暂时只支持
样式如果觉得不满意可自行调整

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

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

    // 分页元素ID(必填)
    var selector = '#pagelist';
    
    // 分页配置
    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模式

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

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

    更新(2018-7-29)

    ES6-环境配置

    2015年,ECMAScript正式发布了它的新版本——ECMAScript6,对JavaScript语言本身来说,这是一次彻彻底底的升级。

    经过这次更新,不仅修复了许多ES5时代留下来的“坑”,更是在原有的语法和规则上增加了不少功能强大的新特性,尽管目前浏览器对新规范支持得并不完善,但经过一些神奇的工具处理后就能让浏览器“认识”这些新东西,并兼容它们了。

    so,我们还有什么理由不用强大的ES6呢?接下来就让我们先来看看这些神奇的工具是怎么使用的吧。

    Babel

    首先,我们需要一个工具来转换ES6的代码,它的芳名叫Babel。
    Babel是一个编译器,负责将源代码转换成指定语法的目标代码,并使它们很好的执行在运行环境中,所以我们可以利用它来编译我们的ES6代码。

    要使用Babel相关的功能,必须先用npm安装它们:(npm及node的使用方法请自行学习)

    npm i babel-cli babel-preset-env babel-core babel-loader babel-plugin-transform-runtime babel-polyfill babel-runtime -D

    安装完成后,我们就可以手动使用命令编译某个目录下的js文件,并输出它们了。

    But,这就是完美方案了吗?显然不是。

    在实际的开发环境中,我们还需要考虑更多东西,比如模块化开发、自动编译和构建等等,所以我们还需要一个更为强大的工具来升级我们的这套构建流程。

    Webpack

    围观群众:我知道了!你是想说Gulp对吧?!

    喂,醒醒!大清亡了!

    在前端框架以及工程化大行其道的今天,想必大家对Webpack、Gulp等工具并不会感到陌生,配合它们我们可以轻松实现一个大型前端应用的构建、打包、发布的流程。
    不过现在是2018年了,三大框架三足鼎立,而Gulp已经稍显老态,作为它的晚辈,一个名叫Webpack的少年正在逐渐崛起。
    这位少年,相信大家在使用Vue、React的过程中已经或多或少接触过它了。简而言之,它和Gulp在项目中的角色是一样的,只不过配置更为简单,构建更为高效,下面就让我们来看看Webpack是怎么使用的吧。

    如果你还没有接触过Webpack,那可以参考官方文档,先对Webpack有一个大致的认识,我们这里不作过多介绍,只讲解它的安装与配置。

    As usual,我们需要安装它:

    npm i webpack webpack-cli webpack-dev-server -D

    使用它也非常简单,只需要建立一个名叫webpack.config.js的配置文件即可:

    const path = require('path');
    
    module.exports = {
      // 模式配置
      mode: 'development',
      // 入口文件
      entry: {},
      // 出口文件
      output: {},
      // 对应的插件
      plugins: [],
      // 处理对应模块
      module: {}
    }

    这个配置文件的主要部分有:入口、出口、插件、模块,在具体配置它们之前,我们可以先理一理我们项目的打包构建流程:

    1. 寻找到./src/es6/目录下面的index.js项目入口文件
    2. 使用Babel编译它及它所引用的所有依赖(如Scss、css文件等)
    3. 压缩编译完成后的js文件,配置为umd规范,重命名为csdwheels.min.js
    4. 清空dist-es6目录
    5. 输出至dist-es6目录下

    要使用清空目录、压缩代码、解析css等功能,我们还需要安装一下额外的包:

    npm i clean-webpack-plugin uglifyjs-webpack-plugin css-loader style-loader node-sass sass-loader

    要在配置中让babel失效,还需要建立一个.babelrc文件,并在其中指定编码规则:

    {
      "presets": ["env"]
    }

    最后,我们就能完成这个配置文件了:

    const path = require('path');
    const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
    const CleanWebpackPlugin = require('clean-webpack-plugin'); //每次构建清理dist目录
    
    module.exports = {
      // 模式配置
      mode: 'development',
      // 入口文件
      entry: {
        pagination: './src/es6/index.js'
      },
      // 出口文件
      output: {
        path: path.resolve(__dirname, 'dist-es6'),
        filename: "csdwheels.min.js",
        libraryTarget: 'umd',
        library: 'csdwheels'
      },
      // 对应的插件
      plugins: [
        new CleanWebpackPlugin(['dist-es6']),
        new UglifyJsPlugin({
          test: /\.js($|\?)/i
        })
      ],
      // 开发服务器配置
      devServer: {},
      // 处理对应模块
      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'
            }]
          }
        ]
      }
    }

    光配置好还不够,我们总需要用命令来运行它吧,在package.json里配置:

    "scripts": {
      "test": "node test/test.js",
      "dev": "webpack-dev-server",
      "build": "webpack && gulp mini && npm run test"
    }

    这里使用dev可以启动一个服务器来展示项目,不过这里我们暂时不需要,而运行npm run build命令就可以同时将我们的./src/es5./src/es6目录下的源码打包好输出到指定目录了。

    不是说好不用Gulp的呢?嘛。。针对ES5的打包工作来说Gulp还是挺好用的,真香警告!

    ES6开发所需要的环境终于配置完成,接下来就让我们开始代码的重构吧!

    ES6-代码重构

    如果你想要入门ES6,强烈推荐阮一峰老师的 教程

    相关的新语法和特性较多,不过要我们的项目要重构为ES6暂时还用不了多少比较高级的特性,你只需要着重看完Class部分即可。

    ES6引入的新特性中,最重要的一个就是Class了。有了它,我们不需要再像以前那样用构造函数去模拟面向对象的写法,因为它是JavaScript原生支持的一种面向对象的语法糖,虽然底层仍然是原型链,不过至少写出来的代码看上去像是那么一回事了。

    拿前面提到的插件模板来说,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();
      }
    }(typeof self !== '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;
    }));

    经过Class这种新语法糖的改造后,它变成了下面这样:

    // ES6 插件模板
    class Plugin {
      constructor(selector, options = {}) {
        this.options = {};
        Object.assign(this.options, options);
        this.init(selector, options);
      }
    
      init(selector, options) {}
    }
    export default Plugin;

    改造后的代码,不仅在语法层面直接支持了构造函数的写法,更是去掉了IIFE这种臃肿的写法,可以说不管是看起来还是写起来都更为清晰流畅了。

    利用内置的 Object.assign()方法,可以直接替换掉我们实现的extend函数,功能可以说完全一样,而且更为强大

    有了新的模板,我们就能直接开始插件代码的重构了,这里只贴上变动比较大的几个地方,其余部分可参考源码

    import '../../../style/pagination/pagination.scss'
    
    class Pagination {
      static CLASS_NAME = {
        ITEM: 'pagination-item',
        LINK: 'pagination-link'
      }
    
      static PAGE_INFOS = [{
          id: "first",
          content: "首页"
        },
        {
          id: "prev",
          content: "前一页"
        },
        {
          id: "next",
          content: "后一页"
        },
        {
          id: "last",
          content: "尾页"
        },
        {
          id: "",
          content: "..."
        }
      ]
    
      constructor(selector, options = {}) {
        // 默认配置
        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) {
        // 分页器元素
        this.pageElement = this.$(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();
      }
    }
    export default Pagination;

    总结起来,这次改造用到的语法就这么几点:

    1. const、let替换var
    2. 用constructor实现构造函数
    3. 箭头函数替换function

    除此之外,在安装了Sass的编译插件后,我们还能直接在这个js文件中把样式import进来,这样打包压缩后的js中也会包含进我们的样式代码,使用的时候就不需要额外再引入样式文件了。
    最后,由于ES6并不支持类的静态属性,所以还需要用到ES7新提案的static语法。我们可以安装对应的babel包:

    npm i babel-preset-stage-0 -D

    安装后,在.babelrc文件中添加它即可:

    {
      "presets": ["env", "stage-0"]
    }

    现在万事俱备,你只需要运行npm run build,然后就可以看到我们打包完成后的csdwheels.min.js文件了。

    打包后,我们还可以发布这个npm包,运行如下命令即可:(有关npm的发布流程,这里就不啰嗦了)

    npm login

    npm publish

    要使用发布后的插件,只需要安装这个npm包,并import对应的插件:

    npm i csdwheels -D
    import { Pagination } from 'csdwheels';

    更新(2018-08-01)

    Vue插件版本

    按照原定开发计划,其实是不想马上更新Vue版本的,毕竟这个系列的“卖点”是原生开发,不过最近用Vue做的项目和自己的博客都恰好用到了分页这个组件,所以我决定一鼓作气把这个插件的Vue版本写出来,正好也利用这个机会学学Vue插件的开发。

    开发规范

    既然是框架,那肯定有它自己的开发规范了,类似于我们自己写的插件一样,它也会给我们提供各式各样的API接口,让我们能定制自己的插件模块。
    简单来说,我们的插件在Vue中需要挂载到全局上,这样才能直接在任何地方引入插件:

    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 }

    vue-wheels-pagination是我们即将要开发的单文件组件,引入后通过install方法把它挂载上去,然后在外部就可以use这个插件了,最后导出这个挂载了我们插件的对象。(如果检测到浏览器环境后,可以直接挂载它)
    这差不多就是一个最简单的插件模板了,更详细的配置可参考官方文档。

    将这个入口用Webpack打包后,就可以在你Vue项目中的main.js中全局加载这个插件了:

    import { VueWheelsPagination } from 'vue-wheels'
    Vue.use(VueWheelsPagination)

    接下来,就让我们来看看用Vue的方式是怎么完成这个分页插件的吧!

    DOM渲染

    利用现代MVVM框架双向绑定的特性,我们已经不必再用原生JS的API去直接操作DOM了,取而代之的,可以在DOM结构上利用框架提供的API间接进行DOM的渲染及交互:

    如上,我们直接在单文件组件的template标签中就完成了这个插件大部分的渲染逻辑。相对原生JS实现的版本,不仅轻松省去了事件监听、DOM操作等步骤,而且让我们能只关注插件本身具体的交互逻辑,可以说大大减轻了开发难度,并提升了页面性能。剩下的数据部分的逻辑及交互处理,在JS中完成即可。

    交互逻辑

    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: '首页',
            prevInfo: '前一页',
            nextInfo: '后一页',
            lastInfo: '尾页'
          }
        }
      },
      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)
        }
      }
    }

    总体分成几个部分:

    1. props属性中对父组件传递的参数进行类型、默认值、是否必填等配置的定义
    2. 计算属性中对分页器本身所需数据进行初始化
    3. 定义操作页码的方法,并向父组件传递当前页码
    4. 在watch属性中监听页码的变化(主要应用于不通过分页而在其他地方改变页码的情况)

    这样,整个分页插件的开发就已经完成了。相信大家可以感觉得到,关于分页逻辑部分的代码量是明显减少了不少的,并且插件本身的逻辑也更清晰,和我们前面一步一步从底层实现起来的版本比较起来,更易拓展和维护了。

    在外层的组件上调用起来大概就像这样:

    export default {
      name: 'app',
      data () {
        return {
          count: 162,
          limit: 5,
          info: {
            firstInfo: '<<',
            prevInfo: '<',
            nextInfo: '>',
            lastInfo: '>>'
          }
        }
      },
      methods: {
        change (pageNumber) {
          console.log(pageNumber)
        }
      }
    }

    传入必填和选填的参数,再监听到子组件冒泡回来的页码值,最后在你自己定义的change()方法里进行跳转等对应的逻辑处理就行了。

    项目的打包流程和上一节提到的差不多,只不过在配置上额外增加了一个本地开发环境服务器的启动,可以参考我的源码。打包完成后,同样可以发布一个npm包,然后就可以在任何Vue项目中引入并使用了。

    后面开发的轮子不一定都会发布Vue版本,因为已经给大家提供了一种重构和包装插件的思路,如果你有自己的需求,可自行利用框架的规范进行插件开发。

    到止为止,我们第一个轮子的开发就算真正结束了,所有源码已同步更新到github,如果大家发现有bug或其他问题,可以回复在项目的issue中,咱们后会有期!(挖坑不填,逃。。

    To be continued...

    参考内容

    • 由匿名函数展开的一系列知识点
    • 自执行函数(IIFE)
    • UMD (Universal Module Definition)
    • 原生JavaScript插件编写指南
    • 如何定义一个高逼格的原生JS插件
    • 如何写一个简单的分页
    • 我总结的js性能优化的小知识
    • 起步 | webpack 中文网
    • webpack 项目构建:(一)基本架构搭建
    • webpack 项目构建:(二)ES6 编译环境搭建
    • Vue-Guide-插件
    • 第一个Vue插件从封装到发布
    • vue封装插件并发布到npm上
    • vue封装第三方插件并发布到npm

    你可能感兴趣的:(手把手教你用原生JavaScript造轮子(1)——分页器(最后更新:Vue插件版本,本篇Over!)...)