使用CanJS编写Web前端应用

前言

CanJS是Web前端应用框架。和它类似的有:

  • backbone
  • angular
  • ember

为什么使用CanJS,相对来说,学习起来比较容易。

本文的目的不是围绕CanJS的讲解。

本文希望通过一个WebApp的开发,说明一种开发正式前端项目的方案。

准备工作

CanJS有两种加载方式,一种是普通的javascript代码加载,另一种使用require.js做异步加载。

本文使用了后者。

另外,前端开发可能涉及很多库和框架,比如jQuery,Bootstrap和require等,怎么把它们部署到前端项目中去,这也是个问题。

比如,一般情况下,是到它们的官方网站下载库文件,然后复制到自己项目的相关目录中。

本文使用bower来管理这个过程,并全面接管前端库的部署。

使用bower

bower,直接参照官网文档安装。

然后,进入你的web项目根目录,对于我的expressjs/nodejs项目,是public目录。输入命令安装:

1
bower install YOUR_PACKAGE

那么会在当前目录下创建类似这样的目录结构:

1
bower_componets/YOUR_PACKAGE

require.js

通过bower安装:

1
bower install requirejs

其他组件的安装

安装CanJS:

1
bower install canjs

安装jQuery:

1
bower install jquery

安装bootstrap:

1
bower install bootstrap

加载这些库和组件

重新来一遍,这回是正式的了。假设你跑的是expressjs项目。用这个命令创建:

1
2
$ express Pm25WebApp
$ cd Pm25WebApp

正式的使用bower,需要两个文件,放在项目的根目录下。

bower.json,这个文件的大部分可通过命令生成:

1
bower init

然后,将需要依赖的库写下来:

1
2
3
4
5
6
7
8
9
...

"devDependencies": {
"requirejs":"",
"bootstrap":"",
"canjs":"",
"jquery": ""
}

}

另外,需要一个名为:.bowerrc 的文件,内容是指定bower文件部署的目录:

1
2
3
{
"directory": "public/bower_components"
}

再往后,我们要加载这些文件,通过require.js的功能。这里就不展开说明了,见github上共享代码m1版本

需要注意的是,git不上传bower部署的js组件,因为没有必要,别人可以通过下面命令自行部署:

1
$ bower install

实现显示北京地区PM2.5功能

要对照CanJS官网文档了解本示例。

使用一个web界面实现这个功能,包括功能点:

  • 获取地点列表
  • 获取具体地点的数据:aqi(pm2.5指数)和污染程度的描述
  • 选择地点,显示该地点信息

获取模型

模型,即Model,在CanJS里的也是Model

CanJS的Model主要解决的是对REST的支持。

这里考虑通过2个REST的请求分别获取。

地点列表,通过GET请求:http://myserver/places。

返回数据类似:

1
2
3
4
5
6
7
8
{
data: [
{place: "奥体中心"},
{place: "天坛"},
{place: "农展馆"},
{place: "南三环"}
]

};

获取具体地点数据,通过id(这里是数组下标),GET请求类似这样:http://myserver/place/2。

返回数据类似:

1
2
3
4
5
{
place: "农展馆",
aqi: '55',
desc: '良'
}

通过CanJS实现这个功能,首先要创建Model类:

1
2
3
4
var Place = can.Model({
findAll: 'GET /places',
findOne: 'GET /place/{id}'
}, {});

然后可以通过类似下面的代码得到模型对象:

1
2
3
Place.findAll({}, function (places) {
//获取地名列表后要执行的代码
});

或者:

1
2
3
Place.findOne({id: 2}, function (place) {
//获取地点信息后执行的代码
});

为了专注于前端开发,未编写服务器端的支持代码,而是使用CanJS的fixture,在前端模拟返回数据。

具体做法类似这样:

1
2
3
4
5
6
7
can.fixture({
'GET /places': function () {
return {
//返回的数据
};
}
});

如果将来对接服务器端只需注释掉fixture部分代码即可。

界面控件的绘制

如上图,设计了2个控件,地点下拉列表和当地的信息。

CanJS有Control,应该是实现MVC中控制的职责。

使用Control,可以用来实现类似窗口小部件(Widget)或者控件。

Control的基本功能是得到模型对象,然后使用模板渲染它们,并封装对它们的控制逻辑。

我这里使用的模板是EJS,比如下面是地点列表的模板:

1
2
3
4
5
<select>
<% can.each(this, function(place, index) { %>
<option value='<%= index%>'><%= place.place %></option>
<% }); %>
</select>

然后,创建控件类,比如这样:

1
2
3
4
5
6
7
8
9
10
var PlaceListWidget = can.Control({
init: function () {
this.render();
},
render: function () {
this.element.html(
can.view('../views/placeList.ejs', this.options.places)
);
}
});

控件需要实例化,才能在页面的某个节点下渲染模板内容,比如:

1
new PlaceListWidget('#placeList', {places: places});

控件间的协作

上文提到了2个控件,地点列表和地点的信息。当选择某个地点后,地点信息控件需要自动根据这个变化显示新地点的信息。

这里就需要使用CanJS的Observables了。它实现了观察者模式。

为了让下拉列表控件变化后地点信息控件能跟着变化,我们引入了一个Observable对象:

1
var context = new can.Map({selectIndex: 0});

基本思路是,下拉列表控件变化后更新seelctIndex,而Observable对象会将这个更新通知地点信息控件做相应的更新。

这部分代码就不单独贴出来了,阅读完整的源代码可帮助了解,见:m2

通过回退返回上次地点选择

比如,我们想通过浏览器的回退按钮,返回刚才选择的地点:

目前m2版本是做不到的。

这里需要使用CanJS另外一个特性,Routing

完整的示例见m3,基本上就是用route替代了m2版本的context。因为route对象其实是一种特殊的observable对象。

这里需要考虑的一个问题是,何时该选择用route,何时又可用一般的observable。

我目前的理解是:

  • 类似翻页这样的操作,想象一下分页显示商品结果,需要使用route,这样可以借助浏览器的回退和前进按钮翻开历史
  • 类似点击菜单,菜单项展开,这样的场景是细粒度的交互,不必在浏览器历史中保留,就使用一般的observable

何时重新加载页面

使用REST+动态生成dom节点,可以做到One Page One Application。但是,当多人合作,而且项目内容比较复杂的时候,尤其是各个模块相关度不高的时候,这种做法会造成混乱:

  • 对架构设计要求很高,否则容易造成各个功能实现和人员分工的冲突
  • 容易引起内存释放问题和复杂的状态维护问题

因此,需要采用这样的原则:

  • 模块间跳转采用刷新页面的方式加载,比如从用户管理模块(网页)点击链接进入产品管理模块(网页)
  • 模块内部功能关联或者界面相近的,采用动态改变网页的方式(不刷新网页)

本文未涉及到的CanJS特性

因为接触CanJS时间较短,希望能尽量简化的使用CanJS,能够满足日常使用即可。

也许以后随着理解的加深,或者实现上需要再使用。

未涉及的CanJS特性:

  • 模板:mustache:当前CanJS已经倾向于默认使用mustache,本文中示例使用EJS需要额外再加入can.ejs模块。主要考虑是EJS更直白一些,尤其使用过比如JSP的朋友
  • Components:需要使用mustache,至少默认是使用的,因为未使用mustache,因此未使用