IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

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

腾讯AlloyTeam 2016-03-24 15:34:33 累计浏览 3,059 次
本机暂存

前言:

在一个喜欢尝鲜的团队里面工作,总会碰到这种情况.前一个项目用的这个框架这种构建,下一个项目就变成新的框架那种构建,上来就blablabla先学一通,刚觉得安心,接到个另外需求,到手一看.又变了一套 T,T , 又要重复上述步骤..如果能在各种框架和开发思路里面游刃有余,当然是最好的,可是多少总会对开发同学产生一些影响,那么各个框架之间的差异到底有多大呢?切换来去又会影响到哪些开发体验呢?且看我边做示例边分解…

正文:


我挑选了三个我认为比较有代表性的框架来实现同一个todo list的小项目,项目基本介绍如下:

示意图:
todo_list_intro
主要功能是协助记录一些计划,防止遗忘,完成后有直观反馈。
共有4需要个交互的地方:

  • - 可输入的计划内容的区域。

  • - 确认新增计划到,计划列表上的行为。

  • - 每个计划需要一个可改变状态的行为,让计划在’完成/未完成’的状态切换。

  • - 有可以清理已实现所有的计划的行为。

每个交互直接对应到了上图中的几个箭头。其中列表的状态展示会改变其样式。下面介绍用各种不同框架时的开发思路以及代码:

backbonejs

backbonejs的特点是其推荐使用MVC的方式来分层和组织代码.依赖jQuery,underscore
这里虽然是个单页应用,但并没有明显的操作路径,交互点和功能都是通过事件触发来推进,所以这里Controller层的概念会被淡化到事件中去.没有一个总控制器,基本数据流模型就如下图:

b3

官网上还有其它数据流模型,有兴趣的同学可以去看看,下面是我的目录结构,lib里面是基础公共库,app里面是业务文件

menuTree

在保证项目结构清晰的情况下.我会尽量拆分文件,这样便于管理和扩展,业务文件我按照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 自身集成了一套数据视图双向绑定的模版语法,同时约定了一个大致的应用开发流程.

一些基本概念如下:

angluarjs

<-!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官网上说明了它的几个基本特点,
reactjs

  1. 专注于UI,都是前端库,强调专注于UI又如何理解呢,与backbone,angular相比又有哪点更UI呢?

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

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

  4. 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的影子,可以看出基本代码组织方式就是这样,像是一个一个相对高内聚的小型界面组件。然后下面是数据流,顺序是从左到右。这个顺序是由什么来决定的呢?
reactDataFlow
其实在render方法里面就能发现蛛丝马迹,下面我用一条直观的图来描述这个顺序的诞生。
reactdflow
render方法的调用条件是当前组件数据改变而调用setState时。每个组件内部会保有自己的state,state决定组件的展示状态.可以看到,每个引起状态改变的行为都会相对上层,最原子的状态界面是在最下层,所以在编码初始就需要想清楚哪些行为驱动数据发生变化,才能系统有条理的拆分组件。

为何更强调UI,我个人理解,React基本不提供任何关于url路径的处理方式,
以前B/S架构下出来的模式理念也被淡化成了强调组件,我个人更倾向于把它看成更偏客户端的一种代码组合方式,同时React比较新,工程化痕迹相比前面两个框架会更重。

总结:

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

  • 代码写法

  • 逻辑分层方式

  • 默认约定

  • 模版语法

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

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

同分类推荐文章

  1. translateZ() (2026-06-25 21:18:56)
  2. translateY() (2026-06-25 21:17:56)
  3. translateX() (2026-06-25 21:16:01)

查看更多 前端 文章 →

建议继续学习

  1. facebook 的工程师文化 (累计阅读 7,292)
  2. 如何成为一名优秀的web前端工程师(前端攻城师)? (累计阅读 7,213)
  3. Pinterest:充分挖掘视觉的潜力 (累计阅读 6,854)
  4. 如何做好一份前端工程师的简历? (累计阅读 5,935)
  5. 关于前端开发 (累计阅读 4,951)
  6. 微博,将让新浪血尽而死 (累计阅读 3,912)
  7. 建设一个网站的成本(之一) (累计阅读 3,696)
  8. React入门:关于虚拟DOM(Virtual DOM) (累计阅读 3,654)
  9. React初探 (累计阅读 3,649)
  10. 从零开始React服务器渲染 (累计阅读 3,626)