Skip to content

基于canjs的分页组件开发

jnotnull edited this page Oct 29, 2014 · 8 revisions

关于canjs的资料少之又少,本文旨在抛砖引玉,希望越来越多的人能够看到它的优点,分享出自己的开发经验。本文参考的是canjs中自带的分页组件的例子,本身对于分页的分析不是文章主旨,本文提供的只是canjs的一个入门。另外本人也是初学canjs,如有错误还请指出。

#1、前期准备

##依赖管理steal

steal是一个js模块加载器,可以支持ES6, AMD, CommonJS, LESS和CSS等模块的加载。使用它能够进行方便快速的开发。它是StealJS工程的的一部分,StealJS提供了强大的构建能力。看个例子就知道它很简单了: 引入steal:

<script src="../../lib/steal/steal.js"></script>

使用steal管理依赖:

steal("can/util", "can/component", "can/map/setter", "can/util/fixture", "can/model", function (can) {
    //write you code here.
});

模板引擎Mustache

模板引擎Mustache不仅支持js,同时也支持其他多种语言。它和EJS模板最大的不同是EJS使用<% %>,而Mustache使用{{}}。

{{ }} 转义后输出
{{{ }}} 非转义输出
{{! }} 注释

关于Mustache的详细说明可以参考下:http://canjs.com/guides/Mustache.html

2、定义Map

canjs中的map是一个可观察对象,支持对map中的属性变化的监视。如果一个属性使用attr来设置取值,而这个取值和之前的不一样,则会触发事件名称为change的事件和名称为属性的事件。同时map中的属性支持set和get方法,以自己定义规则来触发取值变化:

can.Map.extend({
    count: Infinity,
    offset: 0,
    limit: 5,
    next: function () {
        this.attr('offset', this.offset + this.limit);
    },
    setOffset: function (newOffset) {
        return newOffset < 0 ?0 :Math.min(newOffset, !isNaN(this.count - 1) ? this.count - 1 : Infinity);
    }
}

通过定义map,我们可以封装一个简单的分页对象中的涉及到的几个属性:总页数、记录游标和每页记录数。

var Paginate = can.Map.extend({
  count: Infinity,
  offset: 0,
  limit: 5,
  next: function () {
    this.attr('offset', this.offset + this.limit);
  },
  prev: function () {
    this.attr('offset', this.offset - this.limit);
  },
  setOffset: function (newOffset) {
    return newOffset < 0 ?
      0 :
      Math.min(newOffset, !isNaN(this.count - 1) ?
        this.count - 1 :
        Infinity);
  },
  setCount: function (newCount, success, error) {
    return newCount < 0 ? 0 : newCount;
  }
});

初始化时候,map自己会调用set方法去注入各个默认值。如下是map中的_attrs的部分源码:

// Add remaining props.
for (prop in props) {
	// Ignore _cid.
	if (prop !== "_cid") {
		newVal = props[prop];
		this._set(prop, newVal, true);
	}
}

然后绑定了onchange事件:

// `batchTrigger` change events.
this.bind('change', can.proxy(this._changes, this));

3、定义模板

Mustache的模板引入机制和React非常类似,使用如下方式即可:

<div id='out'></div>
<script id="appMustache" type='text/mustache'>
  <app>
	
  </app>
</script>

<script type='text/javascript'> 
steal("can/util", "can/component", "can/map/setter", "can/util/fixture",
  "can/model", function (can) {
  
  $("#out").html(can.view("appMustache", {}))

});

$("#out").html(can.view("appMustache", {}))为入口,通过can.view("appMustache", {})会启动解析。canjs的解析器位于parser.js文件中,对于mustache的解析工作会委托给mustache.js。解析的原理就是把当前的模板按照逻辑拆分成数组后,然后替换里面的变量、块之后重新组装。

#4、定义组件 对于下面的app标签,我们定义一个app组件。

can.Component.extend({
  tag: "app",
  scope: function () {
	return {
	  paginate: new Paginate({
		limit: 5
	  }),
	  websitesDeferred: function () {
		var params = {
		  limit: this.attr('paginate.limit'),
		  offset: this.attr('paginate.offset')
		},
		  websitesDeferred = Website.findAll(params),
		  self = this;

		websitesDeferred.then(function (websites) {
		  self.attr('paginate.count', websites.count);
		});

		return websitesDeferred;
	  }
	}
  }
});

这个组件比较简单,定义了两个属性,第一个就是对应的tag,取值为app,另一个就是scope。正如angularjs中的scope是试图和模型间的连接剂一样,canjs的scope也充当了类似的角色,scope中返回的属性都对应在试图的模板中。为了表现数据,我们定义如下模板:

<script id="appMustache" type='text/mustache'>
  <app>
	<grid deferredData='websitesDeferred'>
	  {{#each items}}
		<tr>
		  <td width='40%'>{{name}}</td>
		  <td width='70%'>{{url}}</td>
		</tr>
	  {{/each}}
	</grid>
	<next-prev paginate='paginate'></next-prev>
	<page-count page='paginate.page' count='paginate.pageCount'></page-count>
  </app>
</script>    

模板解析的时候,会读取标签的属性值,到对应的组件中去查找取值。比如page-count下的page,则会到paginate下面查找到page,如果发现属性取值是方法,则会运行该方法以获得返回值。另外要注意的是,page-count标签中的page属性对应paginate中的page属性,所以务必保持一致。

page: function (newVal) {
	if (newVal === undefined) {
	  return Math.floor(this.attr('offset') / this.attr('limit')) + 1;
	} else {
	  this.attr('offset', (parseInt(newVal, 10) - 1) * this.attr('limit'));
	}
  }

如果newVal没有传入,则表示读取,如果传入,则表示是设定取值。结束后如果发现有依赖,则会进行数据绑定:

// We don't need to listen to the compute `change` if it doesn't have any dependencies
if (!compute.hasDependencies) {
    compute.unbind("change", handler);
} else {
    // Make sure we unbind (there's faster ways of doing this)
can.bind.call(el, "removed", function () {
    compute.unbind("change", handler);
});
// Setup the two-way binding
twoWayBindings[name] = computeData;
}

看最后一行代码twoWayBindings[name] = computeData;关于数据绑定我们在后面会详细论述,这里暂且跳过。

上面的模板中我们看到了page-count标签,我们写一个对一个的组件,如下代码:

can.Component.extend({
  tag: "page-count",
  template: 'Page <span>{{page}}</span> of <span>{{count}}</span>.'
});

可以看到这里还有一个模板template。那这个模板template和上面定义的模板<page-count page='paginate.page' count='paginate.pageCount'></page-count>是啥关系呢。对了,template的内容会嵌套在page-count标签的内部,形成这样: <page-count page='paginate.page' count='paginate.pageCount'>Page <span>{{page}}</span> of <span>{{count}}</span>.</page-count>,暂时不知道是否还有其他嵌套方式。

定义了页数展示信息组件后,我们再定义一个前进后退按钮来查看分页:

can.Component.extend({
  tag: "next-prev",
  template: 
	'<a href="javascript://"' + 
	  'class="prev {{#paginate.canPrev}}enabled{{/paginate.canPrev}}" can-click="paginate.prev"><<</a>' + 
	'<a href="javascript://"' + 
	  'class="next {{#paginate.canNext}}enabled{{/paginate.canNext}}" can-click="paginate.next">>></a>'
});

这里注意到{{#paginate.canPrev}}enabled{{/paginate.canPrev}},它是一个判断逻辑,{{#}}开始,{{/}}结束,can-click绑定了paginate的prev和next方法。

下面我们再看下最重要的组件实现grid:

can.Component.extend({
  tag: "grid",
  scope: {
	items: [],
	waiting: true
  },
  template: "<table><tbody><content></content></tbody></table>",
  events: {
	init: function () {debugger;
	  this.A();
	},
	"{scope} deferreddata": "A",
	A: function () {
	  var deferred = this.scope.attr('deferreddata'),
		scope = this.scope;
	  if (can.isDeferred(deferred)) {
		this.scope.attr("waiting", true);
		this.element.find('tbody').css('opacity', 0.5);
		deferred.then(function (items) {
		  scope.attr('items').replace(items);
		});
	  } else {
		scope.attr('items').attr(deferred, true);
	  }
	},
	"{items} change": function () {
	  this.scope.attr("waiting", false);
	  this.element.find('tbody').css('opacity', 1);
	}
  }
});

首先看下template属性,里面有个比较特殊的标签是<content></content>,使用这个标签可以实现中间嵌套,嵌套的效果就是:

<grid deferredData='websitesDeferred'>
     <table><tbody>
  {{#each items}}
	<tr>
	  <td width='40%'>{{name}}</td>
	  <td width='70%'>{{url}}</td>
	</tr>
  {{/each}}
     </tbody></table>
</grid>

另外可以看到events中有个内置的init方法可以实现初始化。"{scope} deferreddata": "A"则是通过查找deferreddata后实现绑定A方法。在执行scope.attr('items').replace(items);后会触发{items} change绑定的事件。

5、再谈可观察对象Map

现在再来回头说下Map。当前当我们点击下一页分页的按钮时候会触发如下函数:can-click="paginate.next",它会执行paginate的next方法,从而对Offset重新设定取值:this.attr('offset', this.offset + this.limit);。前面已经说过,使用attr会触发其他使用到这个属性的方法执行。

在文件解析阶段,canjs会把读取该属性取值的关联方法当成事件都绑定到这个属性的事件下面,如offset属性,关联它的事件有:canNext、canPrev和page三个,那在this.__bindEvents.offset下面将会有这三个事件;当offset的取值发生变化时候,可以使用compute.js中的如下方法cur = cur[reads[i]]匹配到关联的属性下面的事件,然后逐个执行,执行的结果会返回到试图进行更新。

if (typeof prev[reads[i]] === 'function' && prev.constructor.prototype[reads[i]] === prev[reads[i]]) {
					// call that method
					if (options.returnObserveMethods) {
						cur = cur[reads[i]];
					} else if (reads[i] === 'constructor' && prev instanceof can.Construct) {
						cur = prev[reads[i]];
					} else {
						cur = prev[reads[i]].apply(prev, options.args || []);
					}

6、测试验证

canjs的model中提供了5个方法来CRUD操作, 分别是findAll, findOne, create, update 和 destroy。这里我们使用findAll

var Website = can.Model.extend({
  findAll: "/websites"
}, {});

fixture可以拦截ajax请求,而且可以返回数据。我们要做的首先是构造数据:

var websites = [{id:1,name:"CanJS",url:"http://canjs.us"},{id: 2,name:"jQuery++",url:"http://jquerypp.com"},
			  {id:3,name:"JavaScriptMVC",url:"http://javascriptmvc.com"},{id: 4,name:"Bitovi",url:"http://bitovi.com"},
			  {id:5,name:"FuncUnit",url:"http://funcunit.com"},{id: 6,name:"StealJS",url:"http://stealjs.com"},
			  {id:7,name:"jQuery",url:"http://jquery.com"},{id: 8,name:"Mootools",url:"http://mootools.com"},
			  {id:9,name:"Dojo",url:"http://dojo.com"},{id: 10,name:"YUI",url:"http://yui.com"},
			  {id:11,name:"DoneJS",url:"http://donejs.com"},{id: 12,name:"Mindjet Connect",url:"http://connect.mindjet.com"},
			  {id:13,name:"JSFiddle",url:"http://jsfiddle.net"},{id: 14,name:"Zepto",url:"http://zepto.com"},
			  {id:15,name:"Spill",url:"http://spill.com"},{id: 16,name:"Github",url:"http://github.com"}];
			  
can.fixture("/websites", function (request) {
  var start = request.data.offset || 0,
	  end = start + (request.data.limit || websites.length);
  return {
	count: websites.length,
	data: websites.slice(start, end)
  };
});

例子的最终效果请参见:http://canjs.com/docs/can.Component.html#section_Examples

Clone this wiki locally