本章節,我們做分頁組件,這是一個非常常用的組件。grid, listview都離不開它。因此其各種形態也有。
本章節教授的是一個比較純正的形態,bootstrap風格的那種分頁欄。
我們建立一個ms-pager目錄,控制台下使用npm init初始化倉庫。
然后我們添加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>
一個分頁,大概有這么屬性:
- currentPage: 當前頁, 選中它,它應該會高亮,加一個active類名給它。
- totalPages: 總頁數
- showPages: 要顯示出來的頁數。1萬頁不可能都全部生成出來。
- firstText, lastText, prevText, nextText這些按鈕或鏈接的文本,有的人喜歡文字,有的喜歡圖標,要做成可配置。
- 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,打包后打開頁面:
優化與打磨
目前還沒有加入事件。但加入事件也是輕而易舉的事,但這個事件有點特別,它分別要作用第一頁,最后一頁,前一頁,后一頁及中間頁上。這要傳入不同的參數。此外,它還要排除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
修改構建工具:
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"/>
但這時我們的CSS與JS還沒有壓縮,這個很簡單,
webpack -p
於是dist目錄下的js, css全部壓成一行了!
最后大家可以在這里下到這個工程
相關鏈接