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对象提供监听模式。

  1. 设置或者删除对象的属性的值
  2. 监听这些值的改变
  3. 监听嵌套的对象

新建一个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就是事件对象,我们可以从中取出点击的坐标轴之类。

这是一个坑,等待填满。

原文地址:https://www.cnblogs.com/biglaojiang/p/3069426.html