javascript MVC框架之 Backbone 实用指南

本文非Backbone入门级教程,很多细节可能会被无意识的忽略。Backbone框架比较简单,更多的是要理解其前端MVC思想及应用注意点,随着本文一个完整的Demo做完后,你就可以愉快的掌握使用了。本文更多的是给出一些注意点,强烈建议好好阅读官方API。Backbone基本依赖于Jquery,所以你最好对JQuery有基本的了解。本文中会利用Bootstrap演示部分重要功能,所以 你对css最好也有所了解。MVC开发离不开模板的概念,对常见的模板库比如underscore、handlebars最好都有所了解。

简要介绍

Backbone是一个非常流行而又简单、好用、基于MVC架构的js前端框架,非常轻量,适合做轻量级SPA应用。
同样适合做SPA的angularjs框架可以看我的另一篇文章:

参考资料

Backbone 官方 API
RequireJS
jQuery
Bootstrap3
HandlebarsJs

简要示意图

Blog中绘图始终是个麻烦事,这里就用序列图 和流程图 分别表示一下

SPA通用顺序图

下面的示意图不是很严谨,读者有mvc的概念的话应该很好理解

Created with Raphaël 2.1.0 browser browser router router controller controller view view model model 访问地址 用户触发 调用指定controller 获取视图 获取数据并渲染 返回视图 局部刷新页面

Backbone处理流程图

下面从流程的角度描绘一下Backbone的处理过程:

Created with Raphaël 2.1.0 开始 加载App 初始化router router监听url变化 url跳转站外 结束 触发view切换 yes no

细心的你应该能够发现,没有了controller。是的,使用backbone不需要单独建controller,这也是其比较简单的一面。

SPA中的view切换类型

router切换

  • router监听浏览器地址栏url的变化,并根据变化触发view切换;
  • 由于浏览器地址栏url发生了变化,可以使用浏览器收藏夹功能收藏该地址,方便下次快速进入;

非router切换

  • 使用针对Dom元素的监听事件(比如jquery.on),捕获需要局部刷新的操作,通过model、ajax、localStorage等方式获取数据信息并刷新指定区域;
  • 浏览器地址栏的url没有发生变化,无法使用收藏夹功能来提供快速进入的能力,可以认为这个变化是瞬态的;

router的设计非常重要,一般概要设计时就应该初具雏形。

Backbone MVC应用基本准则

这里的准则会写的比较抽象,但是写完一个backbone最基本的应用后,自然而然就会习惯,但最好先看下官方文档:Backbone 官方Api

  • 对router进行合理规划
  • 根据router将view的切换与router进行绑定
    • router切换一般用于最顶层App级别,比如左边sidebar、顶部navbar等等
    • 非router切换一般为局部Dom事件触发的切换,例如使用on事件处理按钮点击事件;
  • model的修改都要通过set方法,以便触发change事件
  • view必须做到数据无关,数据都存放在model中
  • 基于事件驱动的响应方式
    • Dom事件只改变相关model的属性 (改变model属性值)
    • Dom只在model发生改变而改变 (监听model的属性变化)
  • 尽量使用listenTo监听事件,因为其会自动解绑(on、bind都是需要手动解绑的)
  • 编码要保持链式调用
  • 多用事件、少用回调
  • 每个view都有scope,永远不要操作当前view之外的Dom
    其实这些准则在其他MVC框架中也基本同样适用,只是实现细节上需要调整。

本文的页面布局设计

在Bootstrap官网中有一个布局概念的范例,本文基于其结构进行基于Backbone的SPA化,如下图: ![bootstrap overview](https://img-blog.csdn.net/20151225092145718) 熟悉Bootstrap的话就知道左侧为功能导航,右侧为功能的具体内容 因此,本范例将整个App分为2种类型的View:
  • AppView:整个页面都属于AppView,一般情况下一个App只有一个AppView
  • MainView:App的主要功能显示区域,上图中的Dashboard部分,导航栏中的任意功能都有其对应的MainView,根据导航功能的选择进行切换

    AppView和MainView的划分比较适合本文范例的架构,具体应用具体分析,作为研发人员应该是不断创新,而不是被条条框框限制。

    切换到Report 功能的效果图:
    bootstrap report

请注意一个细节:左侧导航栏有Active的效果,Bootstrap 的NavItem只要设定 active css属性就能满足要求。在何处进行该属性的设定是我们要考虑的。个人建议:元素属于哪个View的范围,就应该由哪个View处理,也就是Scope的概念,上述的MVC基本原则中有提及,这里算是其一个应用实例。
在本文中左侧导航的切换只会触发右侧MainView的切换,上面两张示意图展现了两个不同的MainView,Overview和Report

编写标准Backbone架构的应用

步骤大致分为:

  • 建立目录结构
  • 建立main.js
  • 建立router.js
  • 建立view.js
  • 建立与view相关联的template
  • 建立与view关联的model

建立目录结构

建立目录结构是第一步,其中涉及到的基于npm和bower部分这里会忽略,具体详情可以参考我写的另一篇:javascript 前端 基于 npm和bower的SPA项目标准结构 。这里只给出基本的目录结构:

  • App
    • lib
    • styles
    • models
    • routers
      • app-router.js
    • templates
      • layout
        • dashboard.html
        • report.html
      • app.html
    • views
      • layout
        • dashboard.js
        • report.js
      • app.js
    • main.js
    • index.html

说明:

名称类型用途
libdir存放基本的js文件[本例不涉及]
stylesdir存放css文件[本例不涉及]
modelsdir存放model文件[本例不涉及]
templatesdir存放被view使用的模板文件
viewsdir存放整个spa应用中的view,按功能分目录存放
main.jsjs filespa应用启动文件
index.htmlhtml filebrowser加载主文件

请注意一个细节:views和templates中的内容是完全一一对应的,views存放js,而templates中存放与view对应的html模板。
本文中不涉及的models其结构与views也有很大关系。

文件加载过程流程图

Created with Raphaël 2.1.0 开始 browser加载index.html 加载main.js 加载app-router.js 初始化AppView router开始监听url变化 导航功能改变 切换MainView 结束 yes no

各文件主要内容

index.html

根据requirejs的规范,在index.html最重要的是添加js启动项,加载下一步要编写的main.js,代码非常简单,下面给出范例:

<!doctype html>
<html lang="en">
<head>
    <!-- 请根据需要填充head信息  -->
</head>

<body id="appView">  <!-- 这里标注body为#appView,确定整个App处理的区域,可以根据实际需要改变 -->
</body>
    <!-- 这里就是用requirejs加载同一目录下的main.js -->
    <script data-main="main" src="../bower_components/requirejs/require.js"></script>

</html>

main.js

main.js最重要的作用是初始化router,下面的代码非常简单:

'use strict';
//省略requirejs的配置部分
require([
    'backbone',
    'jquery',
    'routers/app-router',
    'domReady'
], function (backbone,$,AppRouter) {
    //参数 AppRouter指向了下一步要实现的app-router.js
    //AppRouter建立后就完成了对页面url的监视
    var router = new AppRouter('#appView');
    backbone.history.start();   
    //如果需要启用 HTML5 特性 pushState 的配置调用,修改上面的backbone.history.start();并需要后端支持rewrite,这里不做进一步说明

});

router

routers/app-router.js

router是在backbone中是非常重要的环节,其要完成的工作主要有:

  • AppView初始化
  • Route切换处理
define([
    'jquery',
    'backbone',
    'views/app'
],function($,Backbone,AppView){
    "use strict";
    var AppRouter=Backbone.Router.extend({
        initialize:function(el){
            this.el=el;//表明本应用对应的已有DOM元素,比如body、#appView等等
            this.$el=$(el);//转为jquery对象
            console.log("AppRouter initialized!");
            var  router=this;
            this.cleanAppView();
            var appView=new AppView();//主要工作1:初始化AppView
            this.setAppView(appView);
        },
        routes: { //主要工作2:Route切换处理
            '*filter': 'setFilter',//*filter会拦截所有的请求,需要进行过滤操作是可用
            "" : "getIndex",//默认页面,一般为MainView之一,本例中为显示Overview信息
            "overview":"getOverview",//在MainView中显示预览信息
            "report":"getReport",//在MainView中显示报表信息
            "group":"getGroup",//在MainView中显示分组信息
            "process":"getProcess",//在MainView中显示流程信息

            "*error" : "fourOfour"//出错处理
        },
        getIndex: function(){
            this.getOverview();
        },
        getOverview:function(){
            this.setMainview(new DashboardView());
        },
        getReport:function(){
            this.setMainview(new ReportView());
        },
        getGroup:function(){
            this.setMainview(new GroupsView());
        },
        getProcess:function(){
            this.setMainview(new ProcessesView());
        },
        setFilter: function (param) {
            // Set the current filter to be used
            //Common.TodoFilter = param || '';
            console.log("route.setFilter invoked,param="+param);
            // Trigger a collection filter event, causing hiding/unhiding
            // of the Todo view items
            //Todos.trigger('filter');
        },

        //--------------以下为内部函数--------------
        cleanAppView:function () {/*清除当前页面的appView*/
            if (this.appView) {
                this.appView.remove();
                this.appView = null;
            }
        },
        setAppView:function(newView){/*切换App视图函数*/
            this.cleanAppView();
            this.appView=newView.render().$el.appendTo($(this.el));
        },
        cleanMainview:function(){//清除当前的MainView
            if(this.mainView){
                this.mainView.remove();
                this.mainView=null;
            }
        },
        setMainview:function(newView){//设置当前的MainView
            this.cleanMainview();
            this.mainView=newView.render().$el.appendTo(this.$el.find("#main"));//重要点:在AppView的模板中给MainView预留的id
        }

    });
    return AppRouter;
});

AppView

views/app.js

app.js是整个SPA应用的View,即AppView,其负责整个App的整体显示与控制,本文中AppView主要完成的工作有:

  • 初始化与View关联的template(每个view都必须完成的工作)
  • 根据路由的切换改变NavBar中的active属性(监听route事件)
  • 顶部工具栏的查找、设置等等功能(暂时忽略,在events中添加处理事件即可)
define([
    'jquery',
    'underscore',
    'backbone',
    'text!templates/app.html',//重点:与本视图相关的template被注入
    'domReady!'
], function ($, _, Backbone,appTemplate) {
    'use strict';

    var AppView = Backbone.View.extend({

        el:'body',//重点:这里指定view对应Dom中的位置,AppView一般为body,也可以外部传入
        template: _.template(appTemplate),//主要工作1:初始化与view关联的template
        events: {
            //顶部工具栏的查找、设置等等功能
            //这里定义与本视图相关的事件处理,此处忽略  
        },

        initialize: function (options) {
            this.router=options.router;//将AppRouter传递进来,可以用于路由listenTo;
            this.routes=options.routes;//AppRouter传递来的路由列表
            this.viewState=new Backbone.Model();
            this.listenTo(this.router,"route",this.onRouteChange);
            //this.listenTo(this.viewState,"change:navitem",this.changeNavItem)
        },

        render: function () {
            this.$el.empty();
            this.$el.html(this.template());//

            return this;//重点:每个view的render方法都推荐使用return this以保持链式调用。
        },
        onRouteChange:function(routename){
            this.$el.find("#sidebar >ul.nav-sidebar li").removeClass("active");//主要工作2:清除选中的navitem状态
            //主要工作2:根据route信息查找当前选中的navitem
            var $nav_a=null///this.$el.find(String.format("li>a[href='#{0}']",routename));
            var selectorTemplate="#sidebar > ul.nav-sidebar li > a[href='#{0}']";
            //console.log(String.format("routename={0}",routename));
            var $el=this.$el;
            $.each(this.routes,function(k,v,obj){
                //console.log(String.format("key={0},value={1}",k,v));
                if (!$nav_a){
                    if(v===routename)
                        $nav_a=$el.find(String.format(selectorTemplate,k));
                }
            });


            if($nav_a){//主要工作2:激活当前选中的navitem
                $nav_a.parents("li").addClass("active");
                //console.log($nav_a.html());
            }
            else{
                console.error(String.format("app.onRouteChange invoked,but $nav_a not found,routename={0}",routename));
            }
        }

    });

    return AppView;
});
templates/app.html

app.html作为与AppView关联的template,其包含的主体内容为本SAP应用的布局信息,需要特别注意的一点是标注为#main的div

<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container-fluid">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="#">Process Explorer</a>
        </div>
        <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav navbar-right">
                <li><a href="#">Dashboard</a></li>
                <li><a href="#">Settings</a></li>
                <li><a href="#">Profile</a></li>
                <li><a href="#">Help</a></li>
            </ul>
            <form class="navbar-form navbar-right">
                <input type="text" class="form-control" placeholder="Search...">
            </form>
        </div>
    </div>
</nav>

<div class="container-fluid">
    <div class="row">
        <div class="col-sm-3 col-md-2 sidebar" id="sidebar">
            <ul class="nav nav-sidebar">
                <li class="active"><a href="#overview">Overview <span class="sr-only">(current)</span></a></li>
                <li><a href="#report">Reports</a></li>
                <li><a href="#group">Groups</a></li>
                <li><a href="#process">Processes</a></li>
            </ul>
            <ul class="nav nav-sidebar">
                <li><a href="#Nav_Item">Nav item</a></li>
                <li><a href="#Nav_Item">Nav item again</a></li>
                <li><a href="#OneMoreWay">One more nav</a></li>
                <li><a href="#AnotherNavItem">Another nav item</a></li>
                <li><a href="#MoreNavigation">More navigation</a></li>
            </ul>
            <ul class="nav nav-sidebar">
                <li><a href="">Nav item again</a></li>
                <li><a href="">One more nav</a></li>
                <li><a href="">Another nav item</a></li>
            </ul>
        </div>

        <div id="main" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
        <!-- 重点:#main是给MainView预留的区域 -->
        </div>

    </div>
</div>

dashboard mainview

views/layout/dashboard.js

dashboard.js对应的是overview视图,其完成的主要工作是:

  • 加载相关联的template(templates/layout/dashboard.html)
  • 渲染并显示template的内容
  • 处理view中的事件
define([
    'jquery',
    'underscore',
    'backbone',
    'handlebars',
    'text!templates/layout/dashboard.html'//加载相关联的模板
],function($,_,Backbone,Handlebars,ViewTemplate){
    var DashboardView=Backbone.View.extend({
        template:Handlebars.compile(ViewTemplate),
        events: {
            'click a':  'onClick'//处理所有a的单击事件
        },
        initialize:function(){

        },
        render:function(){
            this.$el.html(this.template(
                //给模板传递数据,模板+数据+模板处理=最后的html
                {id:1,data="test data"}
            ));//渲染并显示模板
            return this;
        },
        onClick:function(event){
            console.log($(event.currentTarget).text());//事件处理程序
        }
    });
    return DashboardView;
});
templates/layout/dashboard.html

dashboard.html和app.html,还有尚未提到的report.html都是一样的性质:模板文件。更确切的说,templates目录中的所有html都是作为模板文件存在的,每个模板文件都一定有视图与其关联。

当今有很多成熟的模板库可以被直接使用,主要区别在于提供的语法、处理能力强弱差异,选择一个符合要求的即可。对于backbone来说,其强依赖的underscore功能较弱,推荐使用handlebarsjs

<h1 class="page-header">Dashboard</h1>

<div class="row placeholders">
    <!-- 篇幅太长,忽略 -->
</div>

<h2 class="sub-header">Section title</h2>
    <!-- 篇幅太长,忽略 -->
</div>

report mainview

report相关的视图和模板由于与dashboard几乎完全相同,未少占篇幅,这里忽略,有兴趣可自行实践下。

总结

本文给出了一个基于Backbone框架的标准应用模式,可根据需求的不同进行局部调整,最终为的是达到规范开发、屏蔽细节、专注业务的目的。有些Backbone标准用法都没在文中体现,尤其是view嵌套、行列型model和view、model和collection之间的关系、模板库的使用等等,但作为前端MVC入门和框架使用的培训提纲感觉应该够了,细节部分建议好好阅读Backbone官方API,这也是掌握一个框架所必须经历的过程。


  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值