前言:
在一个喜欢尝鲜的团队里面工作,总会碰到这种情况.前一个项目用的这个框架这种构建,下一个项目就变成新的框架那种构建,上来就blablabla先学一通,刚觉得安心,接到个另外需求,到手一看.又变了一套 T,T , 又要重复上述步骤..如果能在各种框架和开发思路里面游刃有余,当然是最好的,可是多少总会对开发同学产生一些影响,那么各个框架之间的差异到底有多大呢?切换来去又会影响到哪些开发体验呢?且看我边做示例边分解…
正文:
我挑选了三个我认为比较有代表性的框架来实现同一个todo list的小项目,项目基本介绍如下:
示意图:
主要功能是协助记录一些计划,防止遗忘,完成后有直观反馈。
共有4需要个交互的地方:
- 可输入的计划内容的区域。
- 确认新增计划到,计划列表上的行为。
- 每个计划需要一个可改变状态的行为,让计划在’完成/未完成’的状态切换。
- 有可以清理已实现所有的计划的行为。
每个交互直接对应到了上图中的几个箭头。其中列表的状态展示会改变其样式。下面介绍用各种不同框架时的开发思路以及代码:
backbonejs
backbonejs的特点是其推荐使用MVC的方式来分层和组织代码.依赖jQuery,underscore
这里虽然是个单页应用,但并没有明显的操作路径,交互点和功能都是通过事件触发来推进,所以这里Controller层的概念会被淡化到事件中去.没有一个总控制器,基本数据流模型就如下图:
官网上还有其它数据流模型,有兴趣的同学可以去看看,下面是我的目录结构,lib里面是基础公共库,app里面是业务文件
在保证项目结构清晰的情况下.我会尽量拆分文件,这样便于管理和扩展,业务文件我按照model和view做了下拆分.程序入口则是todo.html,如下,文档结构非常简单
<—todo.html—>
<!DOCTYPE html>
<html>
<head>
<meta charset=’utf-8’>
<title>BtoDo</title>
<script type=’text/javascript’src=’lib/underscore.min.js’></script>
<script type=’text/javascript’src=’lib/jquery-2.1.4.min.js’></script>
<script type=’text/javascript’src=’lib/backbone.min.js’></script>
<style type=’text/css’>
.list-wrapper{
margin:20pxauto;
width:300px;
}
.done-true {
text-decoration:line-through;
color:grey;
}
input[type=’checkbox’]{
vertical-align:middle;
}
.archive-btn{
color:blue;
cursor:pointer;
}
</style>
</head>
<body>
<div class=’list-wrapper’id=’todo_panel’>
<h2>Todo</h2>
<span id=’remaining’></span>remaining[archive]
<ul id=’todo_list’>
</ul>
<form onsubmit=”returnfalse”>
<input placeHolder=’foo foo’ type=’text’><button class=’add_btn’>Add</button>
</form>
</div>
<script type=’text/template’id=’list_template’>
<%for(varpindata){%>
<li<%if(data[p].done){%>class=’done-true’<%}%>>’ <%if(data[p].done){%>checked<%}%>><%=data[p].todo%></li>
<%}%>
</script>
<script type=’text/javascript’src=’app/todo.model.js’></script>
<script type=’text/javascript’src=’app/todo.view.js’></script>
</body>
</html>下面是Model,Model可以看成一个描述业务的数据模型,描述越详细,越原子越好,而在此项目中,最原子的数据单元,就是每条计划,所有的需求交互都是围绕每一条计划来行动。而计划是批量的,每个计划都具有相同的行为和操作,所以我声明了一个CollectionCollection是所有相同Model的一个集合,用一个Collection来描述业务数据。声明里给了两个默认计划。全局上挂载这个对象,方便外部文件存取.(项目简单,就直接写了,一般会建议包装一个方法来导出单个文件内部方法或者变量。便于全局管理)
/**
* [todo backbone model file]
*
*/
(function(global,Backbone){
vartodoCollection=newBackbone.Collection([{todo:‘some demos’,done:false},{todo:’some other todo’,done:true }]);
global.todoCollection=todoCollection;
})(window,Backbone);再来看看View ,我定义了一个基础类型的View 叫 BaseView , BaseView的数据源是刚才我导出的todoCollection,然后实例化当前类时候会侦听该数据源的动作。然后定义了RemainView ,ListView,PanelView具体作用见下面我的注释。
值得注意的是 RemainView 会覆盖 initialize方法,这里稍微特别。
/**
* [todo backbone view files]
*
*/
(function(global,Backbone){
vartodoCollection=global.todoCollection;
/**
* BaseView
* 基础视图,指定了默认数据源,初始化行为
*/
varBaseView=Backbone.View.extend({
collection:todoCollection,
initialize:function(){
this.listenTo(this.collection,‘add’,this.render);
this.listenTo(this.collection,‘remove’,this.render);
}
}),
/**
* RemainView
* 当前剩余计划数与总计划数的视图
* 方便用户了解当前所有任务完成状态。
*/
RemainView=BaseView.extend({
/**
* RemainView表示列表计划的完成状况,需要在状态改变时也做出对应
* 行为,所以它有特别的初始化行为。
* changed事件本来也可以归到父类的初始化函数当中,但这个事件其
* 实只在此视图中有使用,这样可以减少渲染函数调用次数.
*/
initialize:function(){
BaseView.prototype.initialize.apply(this,[]);
this.listenTo(this.collection,‘changed’,this.render);
},
render:function(model,collection){
vartpl=_.template(‘<%=rest%>of<%=total%>‘),
rest=0 ,
p,
data=collection.toJSON();
for(pindata){
if(!data[p].done){
++rest;
}
}
this.$el.html(tpl({rest:rest,total:data.length}));
}
}),
/**
* ListView
* 展示具体计划的列表视图
*/
ListView=BaseView.extend({
events:{
‘click.check_box’:‘checked’
},
render:function(model,collection){
vartpl=_.template($(‘#list_template’).html());
this.$el.html(tpl({data:collection.toJSON()}));
},
checked:function(e){
vartarget=e.target,
key=target.getAttribute(‘data-index’),
model=this.collection.at(key);
model.set(‘done’,target.checked);
/**
* 手动触发
*/
this.collection.trigger(‘changed’,{},this.collection);
}
}),
/**
* PanelView
* 操作面板视图,这里决定新增以及完成动作的行为引起的视图变化
*/
PanelView=BaseView.extend({
events:{
‘click.add_btn’:‘add’,
‘click.arc_btn’:‘archived’
},
add:function(e){
vartarget=e.target,
$input=$(target.parentNode).find(‘input’);
this.collection.add({
todo:$input.val(),
done:false
});
$input.val(‘’);
},
archived:function(){
this.collection.remove(this.collection.where({done:true}));
}
});
/**
* 文档加载完全后开始实例化各个类
*/
global.onload=function(){
varlist=newListView({
el:$(‘#todo_list’)
}),
remaining=newRemainView({
el:$(‘#remaining’)
}),
Panel=newPanelView({
el:$(‘#todo_panel’)
});
/**
* 放入默认数据
*/
todoCollection.add([{todo:‘some demos’,done:false},{todo:’some other todo’,done:true }]);
};
})(window,Backbone);angularjs
angularjs 自身集成了一套数据视图双向绑定的模版语法,同时约定了一个大致的应用开发流程.
一些基本概念如下:
<-!html->
<!doctype html>
<html ng-app=”todoApp”>
<head>
<script type=”text/javascript”src=’lib/angular.min.js’></script>
<script src=”app/todo.js”></script>
<style type=”text/css”>
.done-true {
text-decoration:line-through;
color:grey;
}
.list-wrapper{
margin:20pxauto;
width:300px;
}
</style>
</head>
<body>
<div class=’list-wrapper’>
<h2>Todo</h2>
<div ng-controller=”TodoListController as todoList”>
<span>{{todoList.remaining()}} of {{todoList.todos.length}} remaining</span>
[<ahref="””">archive</a>]
<ul class=”unstyled”>
<li ng-repeat=”todo in todoList.todos”>
<input type=”checkbox” ng-model=”todo.done”>
<span class=”done-{{todo.done}}”>{{todo.text}}</span>
</li>
</ul>
<form ng-submit=”todoList.addTodo()”>
<input type=”text”ng-model=”todoList.todoText” size=”30”
placeholder=”foo foo”>
<input class=”btn-primary”type=”submit”value=”add”>
</form>
</div>
</div>
</body>这是个取自官网的示例图,可以看到代码非常精简,基本流程如下.声明了一个module容器,然后在controller中处理几个行为,界面行为完全在html模版中来处理,controller里面不再耦合任何dom操作。
//app.js
angular.module(‘todoApp’,[])
.controller(‘TodoListController’,function(){
vartodoList=this;
todoList.todos=[
{text:’learn angular’,done:true},
{text:’build an angular app’,done:false}];
todoList.addTodo=function(){
todoList.todos.push({text:todoList.todoText,done:false});
todoList.todoText=‘’;
};
todoList.remaining=function(){
varcount=0;
angular.forEach(todoList.todos,function(todo){
count+=todo.done?0:1;
});
returncount;
};
todoList.archive=function(){
varoldTodos=todoList.todos;
todoList.todos=[];
angular.forEach(oldTodos,function(todo){
if(!todo.done)todoList.todos.push(todo);
});
};
});Reactjs
Reactjs官网上说明了它的几个基本特点,
专注于UI,都是前端库,强调专注于UI又如何理解呢,与backbone,angular相比又有哪点更UI呢?
等我放完代码后再简单分析。
VIRTUAL DOM,为了方便打字,我简称它为vdom,React提供了一种新的html模版语法形式,和一整套新的接口来支持这个vdom的特性,通过新语法编译和接口调用,来覆盖视图渲染的过程。让界面渲染变得相对透明,这样才可以衍生出服务端渲染和ReactNative的玩法,不用做大改动的前提下,同一套代码能在各种载体中渲染。
DATA FLOW,数据流的规范,推荐和弱强制使用的单向数据流方式来约束代码和界面模块组织,顺着这个思路来确实会有些不同的编码体验。特别是在我刚用前面两个框架写完demo后立即开始写这个的时候,感受比较明显。
下面进入正题看代码,文档并没有特别需要说明的地方,为了方便和我没有配置整套的React环境,只是用了demo环境,引入了一个翻译jsx的库:
<!-html->
<!DOCTYPEhtml>
<html>
<head>
<title>HelloReact</title>
<script src=”lib/react.js”></script>
<script src=”lib/JSXTransformer.js”></script>
<style type=”text/css”>
.done-true {
text-decoration:line-through;
color:grey;
}
.list-wrapper{
margin:20pxauto;
width:300px;
}
input[type=’checkbox’]{
vertical-align:middle;
}
.archive-btn{
color:blue;
cursor:pointer;
}
</style>
</head>
<body>
<div class=’list-wrapper’id=”example”></div>
<script type=”text/jsx”src=”app/todo.js”></script>
</body>
</html>业务代码,专注于UI的库,首先需要的是拆分视图,这点与其它库并无太大区别。但是如何拆,如何确定各个视图之间的关系则是有套路的,且大致过下下面的代码.
// todo.js文件
/**
* ToDoTips 剩余与总计计数展示视图
*/
varToDoTips=React.createClass({
render:function(){
varitems=this.props.items,
remains=0;
items.map(function(item){
if(!item.done){
++remains;
}
});
return<span>{remains}/{items.length}</span>;
}
});
/**
* TodoList
* 计划列表展示视图
* 包含了两个行为状态,切换计划状态,以及清理已完成计划数
*/
varTodoList=React.createClass({
getInitialState:function(){
return{items:this.props.items};
},
shouldComponentUpdate :function(nextProps){
this.state.items.push(nextProps.nextItem);
returntrue;
},
archived:function(){
varitems=this.state.items.concat([]),
remainItems=[];
items.forEach(function(item){
if(!item.done){
remainItems.push(item);
}
});
this.setState({items:remainItems});
},
toggle:function(e){
varindex=e.target.getAttribute(‘data-index’),
items=this.state.items.concat([]);
items[index].done=!items[index].done;
this.setState({items:items});
},
render:function(){
varthat=this;
varcreateItem=function(item,index){
varitemClass=item.done?‘done-true’:‘’;
return<li className={itemClass}><input type=’checkbox’data-index={index}onClick={that.toggle}checked={item.done}/>{item.todo}</li>;
};
return(
<div>
<span>remaining<ToDoTips items={this.state.items}/> [archive]</span>
<ul>{this.state.items.map(createItem)}</ul>
</div>
);
}
});
/**
* TodoApp 面板入口
* 新增计划行为以及状态。
*/
varTodoApp=React.createClass({
getInitialState:function(){
return{items:this.props.sourceData};
},
handleSubmit:function(e){
e.preventDefault();
vartexts=e.target[‘text’].value ;
this.setState({
nextItem:{
todo:texts,
done:false
}
});
e.target[‘text’].value=‘’;
},
render:function(){
return(
<div>
<h3>TODO</h3>
<TodoList items={this.state.items}nextItem={this.state.nextItem} />
<form onSubmit={this.handleSubmit}>
<input
placeholder=’foo foo’
name=’text’ />
<button>{‘Add#’+(this.state.items.length+1)}</button>
</form>
</div>
);
}
});
/**
* [sourceData 初始数据]
* @type {Array}
*/
varsourceData=[
{todo:’learn React’,done:true},
{todo:’build an React app’,done:false}];
React.render(
<TodoApp sourceData={sourceData}/>,
document.getElementById(‘example’)
);以上是基本视图切分,没看到任何MVC的影子,可以看出基本代码组织方式就是这样,像是一个一个相对高内聚的小型界面组件。然后下面是数据流,顺序是从左到右。这个顺序是由什么来决定的呢?
其实在render方法里面就能发现蛛丝马迹,下面我用一条直观的图来描述这个顺序的诞生。
render方法的调用条件是当前组件数据改变而调用setState时。每个组件内部会保有自己的state,state决定组件的展示状态.可以看到,每个引起状态改变的行为都会相对上层,最原子的状态界面是在最下层,所以在编码初始就需要想清楚哪些行为驱动数据发生变化,才能系统有条理的拆分组件。
为何更强调UI,我个人理解,React基本不提供任何关于url路径的处理方式,
以前B/S架构下出来的模式理念也被淡化成了强调组件,我个人更倾向于把它看成更偏客户端的一种代码组合方式,同时React比较新,工程化痕迹相比前面两个框架会更重。
总结:
几种代码都已经浏览到,个人觉得非常影响编码体验主要在以下几点:
代码写法
逻辑分层方式
默认约定
模版语法
上述框架并没有优劣之分.框架我认为都是经验的聚合,总是诞生于自身业务开发中,带着一些业务痕迹,去照顾到各种团队的开发习惯和效率。
最后这篇文章的目的,有需要的时候,为大家的技术选型提供一些其它的思路。
欢迎大家探讨和拍砖 。


