转载

经常在各种框架之间切换使用是种什么体验?

前言:

在一个喜欢尝鲜的团队里面工作,总会碰到这种情况.前一个项目用的这个框架这种构建,下一个项目就变成新的框架那种构建,上来就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 / javascriptsrc =lib /underscore.min.js’></s cript >

<script type =text / javascriptsrc =lib /jquery-2.1.4.min.js’></s cript >

<script type =text / javascriptsrc =lib /backbone.min.js’></s cript >

<style type=’text/css’>

.list-wrapper {

margin : 20px auto ;

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 - wrapperid =todo _ panel’ >

< h2 > Todo < /h2>

<span id=’remaining’></s pan > remaining [ archive ]

< ul id =todo _ list’ >

< / ul >

< form onsubmit =return false>

< input placeHolder =foo foo  type =text> < button class =add_btn> Add < / button >

< / form >

< / div >

<script type =text / templateid =list _ template’ >

< % for ( var p in data ) { % >

< li < % if ( data [ p ] . done ) { % > class =done - true< % } % > >  < % if ( data [ p ] . done ) { % > checked < % } % > > < %= data [ p ] . todo % > < /li>

<%}%>

</s cript >

<script type =text / javascriptsrc =app /todo.model.js’></s cript >

<script type =text / javascriptsrc =app /todo.view.js’></s cript >

< / body >

< / html >

下面是Model,Model可以看成一个描述业务的数据模型,描述越详细,越原子越好,而在此项目中,最原子的数据单元,就是每条计划,所有的需求交互都是围绕每一条计划来行动。而计划是批量的,每个计划都具有相同的行为和操作,所以我声明了一个CollectionCollection是所有相同Model的一个集合,用一个Collection来描述业务数据。声明里给了两个默认计划。全局上挂载这个对象,方便外部文件存取.(项目简单,就直接写了,一般会建议包装一个方法来导出单个文件内部方法或者变量。便于全局管理)

/**

* [todo backbone model file]

*

*/

( function ( global , Backbone ) {

var todoCollection = new Backbone . 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 ) {

var todoCollection = global . todoCollection ;

/**

* BaseView

* 基础视图,指定了默认数据源,初始化行为

*/

var BaseView = 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 ) {

var tpl = _ . template (<%= rest %> of <%= total %>) ,

rest = 0   ,

p ,

data = collection . toJSON ( ) ;

for ( p in data ) {

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 ) {

var tpl = _ . template ( $ (# list _ template’ ) . html ( ) ) ;

this . $ el . html ( tpl ( { data : collection . toJSON ( ) } ) ) ;

} ,

checked : function ( e ) {

var target = 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 ) {

var target = 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 ( ) {

var list = new ListView ( {

el : $ (# todo _ list’ )

} ) ,

remaining = new RemainView ( {

el : $ (# remaining)

} ) ,

Panel = new PanelView ( {

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 / javascriptsrc =lib /angular.min.js’></s cript >

<script src =app /todo.js”></s cript >

<style type=”text/css”>

.done-true {

text-decoration : line-through ;

color : grey ;

}

.list-wrapper {

margin : 20px auto ;

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</s pan >

[ < a href = "””" > 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}}</s pan >

< / li >

< / ul >

< form ng - submit =todoList . addTodo ( )>

< input type =textng - model =todoList . todoText  size =30

placeholder =foo foo>

< input class =btn - primarytype =submitvalue =add>

< / form >

< / div >

< / div >

< / body >

这是个取自官网的示例图,可以看到代码非常精简,基本流程如下.声明了一个module容器,然后在controller中处理几个行为, 界面行为完全在html模版中来处理,controller里面不再耦合任何dom操作

//app.js

angular . module (todoApp, [ ] )

. controller (TodoListController, function ( ) {

var todoList = 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 ( ) {

var count = 0 ;

angular . forEach ( todoList . todos , function ( todo ) {

count += todo . done ? 0 : 1 ;

} ) ;

return count ;

} ;

todoList . archive = function ( ) {

var oldTodos = todoList . todos ;

todoList . todos = [ ] ;

angular . forEach ( oldTodos , function ( todo ) {

if ( ! todo . done ) todoList . todos . push ( todo ) ;

} ) ;

} ;

} ) ;

Reactjs

Reactjs 官网上说明了它的几个基本特点,

经常在各种框架之间切换使用是种什么体验?
  1. 专注于UI,都是前端库,强调专注于UI又如何理解呢,与backbone,angular相比又有哪点更UI呢?

  2. 等我放完代码后再简单分析。

  3. VIRTUAL DOM,为了方便打字,我简称它为vdom,React提供了一种新的html模版语法形式,和一整套新的接口来支持这个vdom的特性,通过新语法编译和接口调用,来覆盖视图渲染的过程。让界面渲染变得相对透明,这样才可以衍生出服务端渲染和ReactNative的玩法,不用做大改动的前提下,同一套代码能在各种载体中渲染。

  4. DATA FLOW,数据流的规范,推荐和弱强制使用的单向数据流方式来约束代码和界面模块组织,顺着这个思路来确实会有些不同的编码体验。特别是在我刚用前面两个框架写完demo后立即开始写这个的时候,感受比较明显。

下面进入正题看代码,文档并没有特别需要说明的地方,为了方便和我没有配置整套的React环境,只是用了demo环境,引入了一个翻译jsx的库:

< ! - html - >

< ! DOCTYPE html >

< html >

< head >

< title > Hello React < / title >

<script src =lib /react.js”></s cript >

<script src =lib /JSXTransformer.js”></s cript >

<style type=”text/css”>

.done-true {

text-decoration : line-through ;

color : grey ;

}

.list-wrapper {

margin : 20px auto ;

width : 300px ;

}

input[type=’checkbox’] {

vertical-align : middle ;

}

.archive-btn {

color : blue ;

cursor : pointer ;

}

</style>

< / head >

< body >

< div class =list - wrapperid =example> < / div >

<script type =text / jsxsrc =app /todo.js”></s cript >

< / body >

< / html >

业务代码,专注于UI的库,首先需要的是拆分视图,这点与其它库并无太大区别。但是如何拆,如何确定各个视图之间的关系则是有套路的,且大致过下下面的代码.

// todo.js文件

/**

* ToDoTips 剩余与总计计数展示视图

*/

var ToDoTips = React . createClass ( {

render : function ( ) {

var items = this . props . items ,

remains = 0 ;

items . map ( function ( item ) {

if ( ! item . done ) {

++ remains ;

}

} ) ;

return < span > { remains } /{items.length}</s pan > ;

}

} ) ;

/**

* TodoList

*      计划列表展示视图

*      包含了两个行为状态,切换计划状态,以及清理已完成计划数  

*/

var TodoList = React . createClass ( {

getInitialState : function ( ) {

return { items : this . props . items } ;

} ,

shouldComponentUpdate   : function ( nextProps ) {

this . state . items . push ( nextProps . nextItem ) ;

return true ;

} ,

archived : function ( ) {

var items = this . state . items . concat ( [ ] ) ,

remainItems = [ ] ;

items . forEach ( function ( item ) {

if ( ! item . done ) {

remainItems . push ( item ) ;

}

} ) ;

this . setState ( { items : remainItems } ) ;

} ,

toggle : function ( e ) {

var index = e . target . getAttribute (data - index) ,  

items = this . state . items . concat ( [ ] ) ;

items [ index ] . done = ! items [ index ] . done ;

this . setState ( { items : items } ) ;

} ,

render : function ( ) {

var that = this ;

var createItem = function ( item , index ) {

var itemClass = item . done ?done - true: ‘’ ;

return < li className = { itemClass } > < input type =checkboxdata - index = { index } onClick = { that . toggle } checked = { item . done } / > { item . todo } < / li > ;

} ;

return (

< div >

< span > remaining < ToDoTips items = { this . state . items } /> [archive]</s pan >

< ul > { this . state . items . map ( createItem ) } < / ul >

< / div >

) ;

}

} ) ;

/**

*  TodoApp 面板入口

*  新增计划行为以及状态。

*/

var TodoApp = React . createClass ( {

getInitialState : function ( ) {

return { items : this . props . sourceData } ;

} ,

handleSubmit : function ( e ) {

e . preventDefault ( ) ;

var texts = 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}

*/

var sourceData = [

{ 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比较新,工程化痕迹相比前面两个框架会更重。

总结:

几种代码都已经浏览到,个人觉得非常影响编码体验主要在以下几点:

  • 代码写法

  • 逻辑分层方式

  • 默认约定

  • 模版语法

上述框架并没有优劣之分.框架我认为都是经验的聚合,总是诞生于自身业务开发中,带着一些业务痕迹,去照顾到各种团队的开发习惯和效率。

最后这篇文章的目的,有需要的时候,为大家的技术选型提供一些其它的思路。欢迎大家探讨和拍砖 。

原文  http://blogread.cn/it/article/7995?f=hot1
正文到此结束
Loading...