本文非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的概念的话应该很好理解
Backbone处理流程图
下面从流程的角度描绘一下Backbone的处理过程:
细心的你应该能够发现,没有了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 功能的效果图:
请注意一个细节:左侧导航栏有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
- layout
- views
- layout
- dashboard.js
- report.js
- app.js
- layout
- main.js
- index.html
说明:
名称 | 类型 | 用途 |
---|---|---|
lib | dir | 存放基本的js文件[本例不涉及] |
styles | dir | 存放css文件[本例不涉及] |
models | dir | 存放model文件[本例不涉及] |
templates | dir | 存放被view使用的模板文件 |
views | dir | 存放整个spa应用中的view,按功能分目录存放 |
main.js | js file | spa应用启动文件 |
index.html | html file | browser加载主文件 |
请注意一个细节:views和templates中的内容是完全一一对应的,views存放js,而templates中存放与view对应的html模板。
本文中不涉及的models其结构与views也有很大关系。
文件加载过程流程图
各文件主要内容
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,这也是掌握一个框架所必须经历的过程。