CanJS 简单入门
1. can.Construct
首先需要了解的是can.Construct。
一个完全的例子如下:
var Student = can.Construct({ role : "Student", getRole : function() { return this.role; } }, { _studentId : null, _name : null, _age : null, _gender : null, init : function(stuInfo) { this._studentId = stuInfo.id; this._name = stuInfo.name; this._age = stuInfo.age; this._gender = stuInfo.gender; }, getName : function() { return this._name; } });
此时我们就有了一个新的类Student,
我们可以看到我们传入了两个JSON格式的对象。这两个JSON对象中个有一些值对象和方法对象。这两个JSON对象的区别就是
第一个JSON中传入的全是类属性和类方法,第二个JSON中传入的是实例属性和实例方法。一下的例子可以看到他们的区别:
//类属性和类方法可以用类直接调用 console.log(Student.role); console.log(Student.getRole()); var stu = new Student({ id : 0, name : "Xinxing Jiang", age : "27", gender : "male" }); // 实例属性和实例方法需要首先创建一个实例出来才能调用 console.log(stu.getName());
这里需要注意的是类属性,方法和实例属性,方法不能相互调用,即实例对象只能调用实例属性方法,不能调用类方法属性。
需要注意的是第一个JSON可以省略不写,即当传入参数只有一个JSON对象时,默认为实例属性和方法。
实例方法init是这个类的构造函数,我们用以下代码新建一个实例时,我们可以传入参数[argsments],
var stu = new Student([argsments]);
参数可以在init方法中获取到。如 上面的例子中新建stu对象时,传入了参数:
{ id : 0, name : "Xinxing Jiang", age : "27", gender : "male" }
init方法中就用参数stuInfo获取了这个JSON对象。
我们还能很简单的实现继承:
var UniversityStudent = Student({ role : "University Student", level : "University", getLevel : function() { return this.level; } }, { getName : function() { return "Name: " + this._name; }, getAge : function() { return this._age; } });
子类能够重写父类的所有的属性,方法,还能扩展更多的属性和方法。
后面我们将提到的can.Control 和 can.Model就会用到can.Construct,
我们可以看作can.Control 和 can.Model是can.Construct的子类。
2. can.Observe
can.Observe 可以为JS对象提供监听模式。
- 设置或者删除对象的属性的值
- 监听这些值的改变
- 监听嵌套的对象
新建一个can.Observe实例如下:
var student = new can.Observe({ name : 'Xinxing Jiang', age : 27, gender : 'male' });
我们可以看下常见的方法:
//获取student的name属性 console.log(student.attr('name')); //---> Xinxing Jiang //设置student的age属性 student.attr('age',30); //student.attr()能得到student对象的所有属性,返回值是一个JSON对象。 console.log(JSON.stringify(student.attr())); //---> {name: 'Xinxing Jiang', age: 30, gender: 'male' } //同时设置student的多条属性 student.attr( { age: 35, gender: "male...." } ); console.log(JSON.stringify(student.attr())); //---> {name: 'Xinxing Jiang', age: 35, gender: 'male....' } //用removeAttr方法可以删除这条属性,并将当前值返回 console.log(student.removeAttr('age')); //---> 35
以上都是对对象的属性进行操作,我们还可以用以下方法来监听这些改变:
bind
我们来看个例子就能很简单的知道其用法
// 监听student对象的改变,监听所有属性的所有改变 // 其中attr是改变属性的属性名,如:age // how是改变的方式,有三个值,分别为 remove, add, set // newVal为改变后的值,如果为 remove事件, 这个值为undefined // oldVal为改变前的值,如果为 add事件, 这个值为undefined student.bind('change', function(ev, attr, how, newVal, oldVal) { console.log(ev, attr, how, newVal, oldVal); //#1 // 只监听age属性的改变 }).bind('age',function(ev, newVal, oldVal){ console.log(ev, newVal, oldVal);//#2 }); student.removeAttr('age'); //#1--->f.Event "age" "remove" undefined 35 //#2--->f.Event undefined 35 student.attr('age',18); //#1--->f.Event "age" "add" 18 undefined //#2--->f.Event 18 undefined student.attr('age',24); //#1--->f.Event "age" "set" 24 18 //#2--->f.Event 24 18 student.attr('age');//--->无改变,无输出
我们还可以对一个属性进行多次绑定
var ageListener1 = function(ev, newVal, oldVal) { console.log('listen age event 1');// #1 }; var ageListener2 = function(ev, newVal, oldVal) { console.log('listen age event 2');// #2 }; student.bind('age', ageListener1); student.bind('age', ageListener2);
此时如果我们再改变age的值:
student.attr('age',24);
输出会变为
//--->listen age event 1 //--->listen age event 2
由此可见我们对同一个属性绑定多个监听的时候会按绑定时的顺序,
依次执行我们绑定的方法。
unbind
与bind相对应,解除对属性改变的监听。
有两种写法:
一种是
student.unbind('age',ageListener1);
此时仅仅将我们绑定到age属性上的两个监听方法ageListener1,ageListener2中的ageListener1解除绑定,
ageListener2会继续监听。
如果我们想一次解绑所有的监听,我们可以用另一种写法:
student.unbind('age');
each
用each遍历所有的属性和对应的值
需要注意的是值在属性前
student.each(function(value, name) { console.log(name, value); }); // --->name Xinxing Jiang // --->gender male.... // --->age 24
can.Observe.List
can.Observe.List继承于can.Observe,所以我们仍能用bind,unbind,each方法
但是bind监听的事件只能是add,remove,change三种。
var students = new can.Observe.List([ 'Jiang', 'Wang', 'Li' ]); students.bind('add', function(ev, newVals, index) { console.log('added', newVals, 'at', index); }).bind('remove', function(ev, oldVals, index) { console.log('removed', oldVals, 'at', index); }).bind('change', function(ev, index, how, newVal, oldVal) { console.log('changed', ev, index, how, newVal, oldVal); }); students.unbind('change'); students.each(function(item) { console.log(item); });
并扩展了一些List的方法。
- indexOf list.indexOf( item ) - 返回item在list中的索引号
- pop list.pop() - 删除数组最后一个元素,并作为返回值返回
- push list.push( items... ) - 在数组最后增加一个元素,返回数组长度
- shift list.shift() - 删除数组第一个元素,并作为返回值返回
- splice list.splice( index, howMany, [ items... ] ) - 删除从序列号为index开始的howMany个元素,并将后面参数依次加到list的index处,返回值为删除掉的元素的数组
- unshift list.unshift( items... ) - 在数组开头增加一个元素,返回数组长度
console.log(students.indexOf('Jiang')); //---> 0 console.log(students.pop());//---> Li console.log(students.push('Zhu'));//---> 3 console.log(students.shift());//---> Jiang console.log(students.unshift('Jiang'));//---> 3 console.log(students.splice( 1, 2,'Qu','Gao','Su' ));//---> ['Wang', 'Zhu'] console.log(students);//---> ["Jiang", "Qu", "Gao", "Su", "Zhu"] //不要用如下方式进行改变值,因为这样监听事件监听不到改变 students[0] = 'Xinxing'; //要以如下方式 students.splice( 0, 1,'Xinxing');
3. can.Model
一个最简洁的Model如下:
var Student = can.Model({ findAll : 'GET /students', findOne : 'GET /students/{id}', create : 'POST /students', update : 'PUT /students/{id}', destroy : 'DELETE /students/{id}' }, {})
findAll, findOne, create, update, destroy这五个方法是由canjs特殊处理的,是Model的类方法,我们调用的时候可以使用
Student.findOne({id:1,name:"Xinxing Jiang"});
来调用,这是canjs会发送一个ajax请求到Server端并返回一个Student的实例。
其作用与以下代码相似:
var Student = can.Model({ findAll : 'GET /students', findOne : function(data, success) { $.ajax({ url : '/students/{id}', type : "GET", data : data, success : function(d) { success(new Student(d)); // 将返回的对象封装成Student Model的一个实例。 } }); }, create : 'POST /students', update : 'PUT /students/{id}', destroy : 'DELETE /students/{id}' }, {});
所以我就可以来增加更多的类方法:
var Student = can.Model({ findAll : 'GET /students', findOne : 'GET /students/{id}', findByName : function(data, success) { $.ajax({ url : '/students/name', type : "GET", data : data, success : function(d) { success(new Student(d)); } }); }, create : 'POST /student', update : 'PUT /students/{id}', destroy : 'DELETE /students/{id}' }, {});
调用时如下:
Student.findByName({ name : "Jiang" }, function(student) { console.log(student); }); Student.findOne({ id : 3 }, function(student) { console.log(student); });
以上只是说明Model的类方法基本都是一些增删改查,对应Java语言,其实Model的类方法就相当于Model的DAO类,用来对数据进行操作并返回一个Model的实例。
can.Model是一种can.Observe,可以看作是can.Observe的子类,扩展了一些符合REST的接口。Model的实例属性,实例方法才是Java中的Model.
var stu = new Student({ id : 3, name : "Xinxing Jiang", age : "27", gender : "male" });
以上就是一个Student的实例。就像can.Observe中一样,我们可以用 attr来对其中的值来进行读写:
stu.attr(); //---->{id:3,name:"Xinxing Jiang",age:"27",gender:"male"} stu.attr("name"); //---->Xinxing Jiang stu.attr("name","Susan Wang"); //---->stu object stu.attr({id:4,name:"Susan Wang",age:"27",gender:"female"}); //---->stu object
除此之外,还有一个实例方法save。使用方法如下:
stu.save(function(stu) { console.log("client1", stu); stu.attr("name", "Sheldon Jiang"); stu.save(function(stu) { console.log("client2", stu); }); });
save方法会自动调用类方法中的create,update方法。
当实例对象是新建出来 并且是第一次save的话 就会调用create方法,其他调用update方法。
同样的实例方法destroy会调用类方法destroy
与can.Observe一样我们可以给每个属性绑定监听,
stu.bind('name', function(ev, newVal, oldVal) { console.log('name changed to', newVal); });
或者给整个对象绑定监听:只有三个event: created,updated,destroyed
stu.bind('created', function(ev, stu) { console.log('created', stu); }); stu.bind('updated', function(ev, stu) { console.log('created', stu); }); stu.bind('destroyed', function(ev, stu) { console.log('created', stu); });
can.Model.List是can.Model的集合,用法与can.Observe.List一样。Student.findAll的返回值就是一个can.Model.List
4. can.View
can.View可以说是最简单的模块了,只是用来加载模板,它接收两个参数:can.View(idOrUrl, data);
第一个参数是模板对应的id或者模板的文件名(含路径), 第二个参数是传入模板的数据,这个数据通常是一个JSON对象或者JSON数组。
$("#content").html(can.View("view/test.ejs", {name: "Jiang", age: "27"}));
can.View也支持延迟加载:
can.view('view/test.ejs', { students : Student.findAll() }).then(function(testViewHtml) { $("#content").html(testViewHtml); })
Student.findAll()是一个从服务端下载数据的ajax请求,会花费一段时间,can.View能够等待这些数据返回再将这些数据运用到模板中,然后再加到页面上显示出来。其功用与以下写法一致。
Student.findAll({}, function(students) { $("#content").html(can.view('view/test.ejs', { students : students })); });
render功能其实就是将返回的Document对象变成String返回。 通常是在一个模板引用另一个模板时使用:
<% for( var i = 0; i < students.length; i++ ) { %> <li><%== can.view.render( 'view/student.ejs', { student: students[ i ] } ) %></li> <% } %>
5. can.EJS
上一节can.View中提到can.View是来加载模板的,can.EJS就是canjs默认使用的模板,并且也基本上是canjs使用者唯一使用的模板,因为虽然canjs号称支持多种模板,但是很多功能都支持的没有can.EJS好,比如Model的自动更新机制。
can.EJS有两种定义方式,一种是写成一个script标签中如下
<script type="text/ejs" id="testEJS"> <!--CODES--> </script>
还有一种创建的方式是创建一个test.ejs文件,并且将<!--CODES-->写在这个文件中。
这与html引用javascript是差不多的。
EJS中与html相似,但是又额外含有几种特殊标签:
- <% CODES %>: 这个标签用来运行javascript语句。
- <%= CODES %>: 这个标签用来运行javascript语句,并且将返回值输出到当前位置。
- <%== CODES %>: 这个标签用来运行javascript语句,并且将返回值输出到当前位置,并且会将返回值解析一次。
<%= CODES %>与<%== CODES %>有比较容易混淆的地方可以用一个例子来表示:
<%= ‘<b>黑体</b>’ %> 显示出来的是‘<b>黑体</b>’
而<%== ‘<b>黑体</b>’ %> 显示出来的是已经加粗的‘黑体’二字
当然在实际应用中我们很少会用到<%= ‘<b>黑体</b>’ %>这种写法,通常
<%= CODES %> 只是用来输出一个对象的值 比如 <%= student.name %>等,基本不会包含html的标签。
<%== CODES %>通常用来引用一个子模板,上面can.View里面我们提到过其应用。
can.EJS的重头戏,也是我认为canjs存在的最大意义的部门就是下面要说的动态绑定。
拿上文Model的例子来说。一个Student的Model如下:
var Student = can.Model({ findAll : 'GET /students', findOne : 'GET /students/{id}', findByName : function(data, success) { $.ajax({ url : '/students/name', type : "GET", data : data, success : function(d) { success(new Student(d)); } }); }, create : 'POST /student', update : 'PUT /students/{id}', destroy : 'DELETE /students/{id}' }, {});
那么我们可以得到一个Student的实例stu
var stu = new Student({ id : 0, name : "Xinxing Jiang", age : "27", gender : "male" });
然后我们有一个简单的模板叫student.ejs
<table> <tr><td>Name:</td><td><%=student.attr("name")%></td></tr> <tr><td>Age:</td><td><%=student.attr("age")%></td></tr> <tr><td>Gender:</td><td><%=student.attr("gender")%></td></tr> <table>
然后我们可以调用
$("#content").html(can.View("student.ejs",{student:stu}));
这时我们的页面会显示
Name:Xinxing Jiang
Age:27
Gender:male
如果我们使用
stu.attr("name","Sheldon Jiang");
我们不需要做任何其他的事情,页面会自动更新成
Name:Sheldon Jiang
Age:27
Gender:male
这就是我认为canjs中最值得使用的地方,他将一个页面上显示的element对象与后台的Model对应了起来,我们不需要因为值的改变来手动的更新html代码。
上面只是一个简单的例子,现在我们来写一个复杂一点的例子。
假设现在我们有许多学生:
var STUDENTS = [ { id : 0, name : "Xinxing Jiang", age : "27", gender : "male" }, { id : 1, name : "Zhongjiao Wang", age : "27", gender : "female" }, { id : 2, name : "Meimei Han", age : "26", gender : "female" }, { id : 3, name : "Lei Li", age : "28", gender : "male" } ];
现在我们需要用一个列表来显示所有的学生,首先我们需要一个模板用来显示:
<ul>
<% students.each( function( student ) { %>
<li><%==can.view.render( 'student.ejs', {student:student} )%></li>
<% }); %>
</ul>
我们还需要一个Model对象
var students = new Student.List(STUDENTS);
和一个调用的地方:
$("#content").html(can.View("students.ejs",{students:students}));
这里有几个需要注意的地方:
第一是创建Student.List对象的参数必须是Student对象的一个数组。
第二是 引用子模板:<%==can.view.render( 'student.ejs', {student:student} )%>
第三是对Student.List的遍历最好使用students.each的方式。
for (var i =0; i < students.length; i++) {...students[i]...} 的方式会导致Live Binding的功能失效.因为当这种方式循环结束是i的值永远都是students.length-1。canjs再也不能找到相对应的对象来进行监听。
还有用jquery的方式 $(students).each(function(index, student){ ...student...}),这种方式虽然能监听单个student的变化,但是当students增加或减少时将得不到更新。除非在这个代码块的上一行调用students.attr('length')。这显然不是一个值得提倡的代码方式,因为我们并不需要知道其lenght。
我们这种方式能够监听这个List的变化:
students.push(new Student({ id : 4, name : "Tom", age : "31", gender : "male" }));
或者
students[0].destroy();
can.EJS还有一个重要的功能就是 Element Callbacks。
写法如下:
<div <%= function( element ) { element.style.display = 'none' } %> > Test </div>
并且当方法内语句就是一个单行的时候可以简写成
<div <%= ( element ) -> element.style.display = 'none' %> >
Test
</div>
通常来说这个功能是让你能够将对象缓存在这个element上:
<div <%= ( el ) -> el.data('student', student) %> >
Test
</div>
然后如果用户点击这个div的时候我们就能很容易的得到其对应的Model实例
$('div').click(function() { var student = this.data('student'); // TODO:some other logic. })
6. can.Control
can.Control就是MVC中的控制器,控制器有以下特点:有组织的,内存回收,执行迅速,有状态的。
我们所有的逻辑代码,UI的控制代码都需要写在can.Control中。
var Todos = can.Control({ defaults : { age : '32', mail : "sheldon.jiang@airbiquity.com" } }, { 'init' : function(element, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view('todosEJS', todos)); }); } });
我们来分析这个Control,相当于在掉can.Control的时候传入了两个参数,这两个参数都是json对象,并且第一个参数可以省略。
第一个JSON参数中的值,我们可以看作Todos的类静态方法和静态属性,并且其中有一个Key叫做defaults,这个key对应的值会与实例化Todos的时候与传入的第二个参数进行合并。
第二个JSON参数中的值,我们看作是Todos的实例方法和实例属性,其中我们可以把init当作构造函数,这个构造函数能接收两个参数,一个是element,一个是options。
初始化一个Todos的实例如下:
var todos = new Todos('#test', {name:'sheldon',age:'27'});
element和options相对应的接收new Todos实例时传入的两个参数'#test'和{name:'sheldon',age:'27'}。
当然element就已经不是字符串'#test‘而是$('#test')对象。
options就是传入的json对象和上面提到的defaults里的值进行合并,可以用options.name来取得相对应的值。
并且options的整个值如下:
{ name:'sheldon', age:'27', mail: "sheldon.jiang@airbiquity.com" }
在除去init方法以外的方法中,我们也可以用this.element 和this.options来调用这两个参数。
既然我们Control不仅要组织逻辑还需要对页面,也就是can.View进行操作,就必不可少的需要监听页面上的用户交互事件。
var Todos = can.Control({ defaults : { age : '32', mail : "sheldon.jiang@airbiquity.com" } }, { 'init' : function(element, options) { var self = this; Todo.findAll({}, function(todos) { self.element.html(can.view('todosEJS', todos)) }) }, '.testButton click' : function(el, event) { // TODO: } });
我们可以看到
'.testButton click' : function(el, event) { //TODO: }
这就是监听class为testButton的页面元素的click事件,当用户点击testButton元素时,后面的时间就会被触发,并且接收两个参数,
el就是当前被点击的dom对象,即$('.testButton')。event就是事件对象,我们可以从中取出点击的坐标轴之类。
这是一个坑,等待填满。