一步步編寫avalon組件02:分頁組件

本章節,咱們作分頁組件,這是一個很是經常使用的組件。grid, listview都離不開它。所以其各類形態也有。javascript

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

clipboard.png

本章節教授的是一個比較純正的形態,bootstrap風格的那種分頁欄。css

咱們創建一個ms-pager目錄,控制檯下使用npm init初始化倉庫。html

clipboard.png

而後咱們添加dependencies配置項,嘗試使用一些更強大的loader!java

"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模塊……node

編寫模板與VM

此次咱們打算使用boostrap的樣式,所以重心就只有這兩部分。react

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

一個分頁,大概有這麼屬性:jquery

  1. currentPage: 當前頁, 選中它,它應該會高亮,加一個active類名給它。
  2. totalPages: 總頁數
  3. showPages: 要顯示出來的頁數。1萬頁不可能都所有生成出來。
  4. firstText, lastText, prevText, nextText這些按鈕或連接的文本,有的人喜歡文字,有的喜歡圖標,要作成可配置。
  5. onPageClick, 事件回調,它應該在該頁disabled或active時不能觸發事件。但咱們須要將它一層。onPageClick是用戶的方法,而處理disabled, active則是組件的事。所以咱們模仿上一節的彈出層,外包一個cbProxy。

此外是類名,href, title的動態生成。webpack

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

這樣分頁欄的初始形態就出來。最複雜就是中間顯示頁數的計算。git

構建工程

咱們當即檢驗一下咱們的分頁欄好很差使。建一個main.js做爲入口文件github

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 分頁插件
相關文章
相關標籤/搜索