Ruby's Louvre

每天学习一点点算法

导航

一步步编写avalon组件02:分页组件

本章节,我们做分页组件,这是一个非常常用的组件。grid, listview都离不开它。因此其各种形态也有。

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

本章节教授的是一个比较纯正的形态,bootstrap风格的那种分页栏。

我们建立一个ms-pager目录,控制台下使用npm init初始化仓库。

clipboard.png

然后我们添加dependencies配置项,尝试使用一些更强大的loader!

  "dependencies": {
    "file-loader":"~0.9.0",
    "url-loader": "0.5.7",
    "node-sass": "^3.8.0",
    "sass-loader": "^3.2.2",
    "style-loader": "~0.13.1",
    "css-loader": "~0.8.0",
    "raw-loader":"~0.5.1",
    "html-minify-loader":"~1.1.0",
    "webpack": "^1.13.1"
  },

然后npm install,安装几百个nodejs模块……

编写模板与VM

这次我们打算使用boostrap的样式,因此重心就只有这两部分。

<ul class="pagination">
    <li class="first" 
        ms-class='{disabled: @currentPage === 1}'>
        <a ms-attr='{href:@getHref("first"),title:@getTitle("first")}'
           ms-click='cbProxy($event, "first")'
           >
            {{@firstText}}
        </a>
    </li>
    <li class="prev" 
        ms-class='{disabled: @currentPage === 1}'>
        <a ms-attr='{href:@getHref("prev"),title:@getTitle("prev")}'
           ms-click='cbProxy($event, "prev")'
           >
            {{@prevText}}
        </a>
    </li>
    <li ms-for='page in @pages' 
        ms-class='{active: page === @currentPage}' >
        <a ms-attr='{href:@getHref(page),title:@getTitle(page)}'
           ms-click='cbProxy($event, page)'
           >
            {{page}}
        </a>
    </li>
    <li class="next" 
        ms-class='{disabled: @currentPage === @totalPages}'>
        <a ms-attr='{href:@getHref("next"),title: @getTitle("next")}'
           ms-click='cbProxy($event, "next")'
           >
            {{@nextText}}
        </a>
    </li>
    <li class="last" 
        ms-class='{disabled: @currentPage === @totalPages}'>
        <a ms-attr='{href:@getHref("last"),title: @getTitle("last")}'
           ms-click='cbProxy($event, "last")'
           >
            {{@lastText}}
        </a>
    </li>
</ul>

一个分页,大概有这么属性:

  1. currentPage: 当前页, 选中它,它应该会高亮,加一个active类名给它。
  2. totalPages: 总页数
  3. showPages: 要显示出来的页数。1万页不可能都全部生成出来。
  4. firstText, lastText, prevText, nextText这些按钮或链接的文本,有的人喜欢文字,有的喜欢图标,要做成可配置。
  5. onPageClick, 事件回调,它应该在该页disabled或active时不能触发事件。但我们需要将它一层。onPageClick是用户的方法,而处理disabled, active则是组件的事。因此我们模仿上一节的弹出层,外包一个cbProxy。

此外是类名,href, title的动态生成。


var avalon = require('avalon2')

avalon.component('ms-pager', {
    template: require('./template.html'),
    defaults: {
        getHref: function (href) {
            return href
        },
        getTitle: function (title) {
            return title
        },
        showPages: 5,
        pages: [],
        totalPages: 15,
        currentPage: 1,
        firstText: 'First',
        prevText: 'Previous',
        nextText: 'Next',
        lastText: 'Last',
        onPageClick: avalon.noop,//让用户重写
        cbProxy: avalon.noop, //待实现
        onInit: function (e) {
            var a = getPages.call(this, this.currentPage)
            this.pages = a.pages
            this.currentPage = a.currentPage
        }
    }
})
function getPages(currentPage) {
    var pages = []
    var s = this.showPages
    var total = this.totalPages
    var half = Math.floor(s / 2)
    var start = currentPage - half + 1 - s % 2
    var end = currentPage + half

    // handle boundary case
    if (start <= 0) {
        start = 1;
        end = s;
    }
    if (end > total) {
        start = total - s + 1
        end = total
    }

    var itPage = start;
    while (itPage <= end) {
        pages.push(itPage)
        itPage++
    }

    return {currentPage: currentPage, pages: pages};
}

这样分页栏的初始形态就出来。最复杂就是中间显示页数的计算。

构建工程

我们立即检验一下我们的分页栏好不好使。建一个main.js作为入口文件

var avalon = require('avalon2')
require('./index')
avalon.define({
    $id: 'test'
})

module.exports = avalon //注意这里必须返回avalon,用于webpack output配置

建立一个page.html,引入bootstrap的样式

<!DOCTYPE html>
<html>
    <head>
        <title>分页栏</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <link href="http://netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css" rel="stylesheet">
        <script src="./dist/index.js"></script>
    </head>
    <body ms-controller="test">
        <wbr ms-widget="{is:'ms-pager'}" />
    </body>
</html>

然后建webpack.config开始构建工程:

var webpack = require('webpack');

var path = require('path');


function heredoc(fn) {
    return fn.toString().replace(/^[^\/]+\/\*!?\s?/, '').
            replace(/\*\/[^\/]+$/, '').trim().replace(/>\s*</g, '><')
}
var api = heredoc(function () {
    /*
     avalon的分页组件
     
     使用
     兼容IE6-8
     <wbr ms-widget="[{is:'ms-pager'}, @config]"/>
     只支持现代浏览器(IE9+)
     <ms-pager ms-widget="@config">
     </ms-pager>
     */
})

module.exports = {
    entry: {
        index: './main'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
        libraryTarget: 'umd',
        library: 'avalon'
    }, //页面引用的文件
    plugins: [
        new webpack.BannerPlugin('分页 by 司徒正美\n' + api)
    ],
    module: {
        loaders: [
            //ExtractTextPlugin.extract('style-loader', 'css-loader','sass-loader')
            //http://react-china.org/t/webpack-extracttextplugin-autoprefixer/1922/4
            // https://github.com/b82/webpack-basic-starter/blob/master/webpack.config.js 
            {test: /\.html$/, loader: 'raw!html-minify'}
        ]
    },
    'html-minify-loader': {
        empty: true, // KEEP empty attributes
        cdata: true, // KEEP CDATA from scripts
        comments: true, // KEEP comments
        dom: {// options of !(htmlparser2)[https://github.com/fb55/htmlparser2]
            lowerCaseAttributeNames: false, // do not call .toLowerCase for each attribute name (Angular2 use camelCase attributes)
        }
    },
    resolve: {
        extensions: ['.js', '', '.css']
    }
}


执行webpack --watch,打包后打开页面:

clipboard.png

优化与打磨

目前还没有加入事件。但加入事件也是轻而易举的事,但这个事件有点特别,它分别要作用第一页,最后一页,前一页,后一页及中间页上。这要传入不同的参数。此外,它还要排除disabled状态与active状态的页码。虽然当我们点击页码时,页码上已经有disabled, active 这样的类名,但这要访问元素节点,这与MVVM的理念不一致。因此我们要另寻他法。此时,我们再看一下我们的模板,发现类名的生成部分太混乱,需要抽象一下。把添加了disabled与active 的页面存放起来,这样以后就不用访问元素节点了。

我们抽象出一个toPage方法,用于将first, last, prev, next转换页码

    toPage: function (p) {
            var cur = this.currentPage
            var max = this.totalPages
            switch (p) {
                case 'first':
                    return 1
                case 'prev':
                    return Math.max(cur - 1, 0)
                case 'next':
                    return Math.min(cur + 1, max)
                case 'last':
                    return max
                default:
                    return p
            }
        },

然后添加一个$buttons对象,这是用于存放first, last, prev, next的disabled状态。之所以用$开头,那是因为这样做就不用转换为子VM,提高性能。

抽象一个isDisabled方法

 isDisabled: function (name, page) {
    return this.$buttons[name] = (this.currentPage === page)
 },

那么页面的对应位置就可以改成disabled: @isDisabled('first', 1)

然后优化getHref方法,内部调用toPage方法,这样就能看到地址栏的hash变化。

getHref: function(){
   return '#page-' + this.toPage(a)
}

实现cbProxy。大家看到我命名的方式是不是很怪,什么XXXProxy, isXXX。那是从java的设计模式过来的。

cbProxy: function (e, p) {
    if (this.$buttons[p] || p === this.currentPage) {
        e.preventDefault()
        return //disabled, active不会触发
    }
    var cur = this.toPage(p)
    var obj = getPages.call(this, cur)
    this.pages = obj.pages
    this.currentPage = obj.currentPage
    return this.onPageClick(e, p)
},

重写onInit,方便它直接从地址栏得到当前参数。

  onInit: function () {
        var cur = this.currentPage
        var match = /(?:#|\?)page\-(\d+)/.exec(location.href)
        
        if (match && match[1]) {
            var cur = ~~match[1]
            if (cur < 0 || cur > this.totalPages) {
                cur = 1
            }
        }
        var obj = getPages.call(this, cur)
        this.pages = obj.pages
        this.currentPage = obj.currentPage
    }

当然,有的用户会重写getHref方法,地址栏的参数也一样。因此最好这个正则也做成可配置。

rpage : /(?:#|\?)page\-(\d+)/

注意,avalon2.1以下有一个BUG(2.1.2已经修复),会将VM中的正则转换一个子VM,因此需要大家打开源码,修改其isSkip方法

var rskip = /function|window|date|regexp|element/i

function isSkip(key, value, skipArray) {
    // 判定此属性能否转换访问器
    return key.charAt(0) === '$' ||
            skipArray[key] ||
            (rskip.test(avalon.type(value))) ||
            (value && value.nodeName && value.nodeType > 0)
}

然后我们再打包一下:
图片描述

接着是样式问题。我最开始说过,我们是用bootstrap样式,但我并不需要整个库,那么在这里将pagination的相关部分扒下来就是。

建立一个style.scss文件

//
// Pagination (multiple pages)
// --------------------------------------------------
$gray-base:              #000 !default;
$gray-light:             lighten($gray-base, 46.7%) !default; // #777
$gray-lighter:           lighten($gray-base, 93.5%) !default; // #eee
$brand-primary:         darken(#428bca, 6.5%) !default; // #337ab7
//** Global textual link color.
$link-color:            $brand-primary !default;
//** Link hover color set via `darken()` function.
$link-hover-color:      darken($link-color, 15%) !default;
$border-radius-base:        4px !default;

$line-height-large:         1.3333333 !default; // extra decimals for Win 8.1 Chrome
$border-radius-large:       6px !default;


$padding-base-vertical:     6px !default;
$padding-base-horizontal:   12px !default;


$font-size-base:          14px !default;

//** Unit-less `line-height` for use in components like buttons.
$line-height-base:        1.428571429 !default; // 20/14
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
$line-height-computed:    floor(($font-size-base * $line-height-base)) !default; // ~20px


$cursor-disabled:                not-allowed !default;

$pagination-color:                     $link-color !default;
$pagination-bg:                        #fff !default;
$pagination-border:                    #ddd !default;

$pagination-hover-color:               $link-hover-color !default;
$pagination-hover-bg:                  $gray-lighter !default;
$pagination-hover-border:              #ddd !default;

$pagination-active-color:              #fff !default;
$pagination-active-bg:                 $brand-primary !default;
$pagination-active-border:             $brand-primary !default;

$pagination-disabled-color:            $gray-light !default;
$pagination-disabled-bg:               #fff !default;
$pagination-disabled-border:           #ddd !default;




// Single side border-radius

@mixin border-right-radius($radius) {
  border-bottom-right-radius: $radius;
     border-top-right-radius: $radius;
}
@mixin border-left-radius($radius) {
  border-bottom-left-radius: $radius;
     border-top-left-radius: $radius;
}

.pagination {
  display: inline-block;
  padding-left: 0;
  margin: $line-height-computed 0;
  border-radius: $border-radius-base;

  > li {
    display: inline; // Remove list-style and block-level defaults
    > a,
    > span {
      position: relative;
      float: left; // Collapse white-space
      padding: $padding-base-vertical $padding-base-horizontal;
      line-height: $line-height-base;
      text-decoration: none;
      color: $pagination-color;
      background-color: $pagination-bg;
      border: 1px solid $pagination-border;
      margin-left: -1px;
    }
    &:first-child {
      > a,
      > span {
        margin-left: 0;
        @include border-left-radius($border-radius-base);
      }
    }
    &:last-child {
      > a,
      > span {
        @include border-right-radius($border-radius-base);
      }
    }
  }

  > li > a,
  > li > span {
    &:hover,
    &:focus {
      z-index: 2;
      color: $pagination-hover-color;
      background-color: $pagination-hover-bg;
      border-color: $pagination-hover-border;
    }
  }

  > .active > a,
  > .active > span {
    &,
    &:hover,
    &:focus {
      z-index: 3;
      color: $pagination-active-color;
      background-color: $pagination-active-bg;
      border-color: $pagination-active-border;
      cursor: default;
    }
  }

  > .disabled {
    > span,
    > span:hover,
    > span:focus,
    > a,
    > a:hover,
    > a:focus {
      color: $pagination-disabled-color;
      background-color: $pagination-disabled-bg;
      border-color: $pagination-disabled-border;
      cursor: $cursor-disabled;
    }
  }
}

然后在index.js加上

require('./style.scss')

然后在webpack.config.js加上

 {test: /\.scss$/, loader: "style!css!sass"}

我们再尝试将样式独立成一个请求,有效利用页面缓存。

npm install extract-text-webpack-plugin --save-dev

clipboard.png

修改构建工具:

var webpack = require('webpack');

var path = require('path');

function heredoc(fn) {
    return fn.toString().replace(/^[^\/]+\/\*!?\s?/, '').
            replace(/\*\/[^\/]+$/, '').trim().replace(/>\s*</g, '><')
}
var api = heredoc(function () {
    /*
     avalon的分页组件
    getHref: 生成页面的href
    getTitle: 生成页面的title
    showPages: 5 显示页码的个数
    totalPages: 15, 总数量 
    currentPage: 1, 当前面
    firstText: 'First',
    prevText: 'Previous',
    nextText: 'Next',
    lastText: 'Last',
    onPageClick: 点击页码的回调
     
     使用
     兼容IE6-8
     <wbr ms-widget="[{is:'ms-pager'}, @config]"/>
     只支持现代浏览器(IE9+)
     <ms-pager ms-widget="@config">
     </ms-pager>
     */
})
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var cssExtractor = new ExtractTextPlugin('/[name].css');

module.exports = {
    entry: {
        index: './main'
    },
    output: {
        path: path.join(__dirname, 'dist'),
        filename: '[name].js',
        libraryTarget: 'umd',
        library: 'avalon'
    }, //页面引用的文件
    plugins: [
        new webpack.BannerPlugin('分页 by 司徒正美\n' + api)
    ],
    module: {
        loaders: [
            //http://react-china.org/t/webpack-extracttextplugin-autoprefixer/1922/4
            // https://github.com/b82/webpack-basic-starter/blob/master/webpack.config.js 
            {test: /\.html$/, loader: 'raw!html-minify'},
            {test: /\.scss$/, loader: cssExtractor.extract( 'css!sass')}

        ]
    },
    'html-minify-loader': {
        empty: true, // KEEP empty attributes
        cdata: true, // KEEP CDATA from scripts
        comments: true, // KEEP comments
        dom: {// options of !(htmlparser2)[https://github.com/fb55/htmlparser2]
            lowerCaseAttributeNames: false, // do not call .toLowerCase for each attribute name (Angular2 use camelCase attributes)
        }
    },
    plugins: [
        cssExtractor
    ],
    resolve: {
        extensions: ['.js', '', '.css']
    }
}

修改页面的link为

  <link href="./dist/index.css" rel="stylesheet"/>

clipboard.png

但这时我们的CSS与JS还没有压缩,这个很简单,

webpack -p

于是dist目录下的js, css全部压成一行了!

最后大家可以在这里下到这个工程

相关链接

  1. jQuery分页插件
  2. 12个基于 jQuery 框架的 Ajax 分页插件

posted on 2016-06-29 11:27  司徒正美  阅读(5405)  评论(17编辑  收藏  举报