- 教程
- 文档
【免费下载链接】jstutorial
Javascript tutorial book
本文是 jstutorial 仓库「语法专题」系列中《MVC框架与Backbone.js》一文的完整展开版。全文以该文为骨架,从 JavaScript 代码组织困境出发,依次讲解 MVC 概念、ES6 时代的"零框架"模块化方案、Backbone 的加载方式与整体用法,并逐项拆解
Backbone.View、Backbone.Events、Backbone.Router、Backbone.history、Backbone.Model、Backbone.Collection六大核心对象的 API 与实战代码。读完本文,你将掌握用 Backbone 组织一个可维护的浏览器端应用的基本方法,并理解它与 RequireJS、Browserify、Underscore 等生态工具如何协同工作。
MVC 框架:复杂 JavaScript 应用的代码组织之道
随着 JavaScript 程序变得越来越复杂,往往需要一个团队协作开发,这时代码的模块化和组织规范就变得异常重要。MVC 模式就是代码组织的经典模式。
MVC 模式把应用程序拆分为三层,各司其职:
(1)Model
Model 表示数据层,也就是程序需要的数据源,通常使用 JSON 格式表示。它封装了业务数据以及对这些数据的读写逻辑,是应用中"事实"的唯一来源。
(2)View
View 表示表现层,也就是用户界面。对于网页来说,就是用户看到的网页 HTML 代码。View 负责把 Model 中的数据呈现出来,并收集用户的交互动作。
(3)Controller
Controller 表示控制层,用来对原始数据(Model)进行加工,传送到 View。它承担"翻译官"的角色:监听用户操作、调用 Model 更新数据、再驱动 View 重新渲染。
由于网页编程不同于客户端编程,在 MVC 的基础上,JavaScript 社区产生了各种变体框架 MVP(Model-View-Presenter)、MVVM(Model-View-ViewModel)等等,有人就把所有这一类框架的各种模式统称为 MV*。
框架的优点在于合理组织代码、便于团队合作和未来的维护;缺点在于有一定的学习成本,且限制你只能采取它的写法。
从历史背景看,Backbone 正是这一波浪潮的标志性产物:jstutorial 的 JavaScript 发展史 记载,2010 年 NPM、BackboneJS 和 RequireJS 三个重要项目诞生,"标志着 JavaScript 进入模块化开发的时代";2012 年单页面应用程序框架(single-page app framework)开始崛起,AngularJS 和 Ember 项目相继发布 1.0 版本。理解这段背景,有助于把握 Backbone "小而自由"的设计取向。
零框架解决方案:ES6 时代的模块化组合
MVC 框架(尤其是大型框架)有一个严重的缺点,就是会产生用户的重度依赖。一旦框架本身出现问题或者停止更新,用户的处境就会很困难,维护和更新成本极高。
ES6 的到来,使得 JavaScript 语言有了原生的模块解决方案(相关语法特性可参见仓库中的 ECMAScript 6 介绍)。于是,开发者有了另一种选择,就是不使用 MVC 框架,只使用各种单一用途的模块库,组合完成一个项目。下面是可供选择的各种用途的模块清单(以下库名与用途说明整理自原文档)。
辅助功能库(Helper Libraries)
- moment.js:日期和时间的标准化
- underscore.js / Lo-Dash:一系列函数式编程的功能函数(本仓库的 underscore 教程 有专门章节)
路由库(Routing)
- router.js:Ember.js 使用的路由库
- route-recognizer:功能全面的路由库
- page.js:类似 Express 路由的库
- director:同时支持服务器和浏览器的路由库
Promise 库(异步方案的系统讲解可参考仓库 Promise 对象 一章)
- RSVP.js:ES6 兼容的 Promise 库
- ES6-Promise:RSVP.js 的子集,但是全面兼容 ES6
- q:最常用的 Promise 库之一,AngularJS 用了它的精简版
- native-promise-only:严格符合 ES6 的 Promise 标准,同时兼容老式浏览器
客户端与服务器的通信库
- fetch:实现 window.fetch 功能
- qwest:支持 XHR2 和 Promise 的 Ajax 库
- jQuery:jQuery 2.0 支持按模块打包,因此可以创建一个纯 Ajax 功能库
动画库(Animation)
- cssanimevent:兼容老式浏览器的 CSS3 动画库
- Velocity.js:性能优秀的动画库
辅助开发库(Development Assistance)
- LogJS:轻量级的 logging 功能库
- UserTiming.js:支持老式浏览器的高精度时间戳库
流程控制和架构(Flow Control/Architecture)
- ondomready:类似 jQuery 的 ready() 方法,符合 AMD 规范
- script.js:异步的脚本加载和依赖关系管理库
- async:浏览器和 node.js 的异步管理工具库
- Virtual DOM:react.js 的一个替代方案,核心思路是借助 Virtual DOM 与 diffing 算法减少真实 DOM 操作
数据绑定(Data-binding)
- Object.observe():Chrome 已经支持该方法,可以轻易实现双向数据绑定
模板库(Templating)
- Mustache:大概是目前使用最广的不含逻辑的模板系统
微框架(Micro-Framework)
某些情况下,可以使用微型框架,作为项目开发的起点。
- bottlejs:提供惰性加载、中间件钩子、装饰器等功能
- Stapes.js:微型 MVC 框架
- soma.js:提供一个松耦合、易测试的架构
- knockout:最流行的微框架之一,主要关注 UI
Backbone 的加载:三种主流方式
Backbone 依赖 jQuery(或 Zepto)与 Underscore 两个库,加载时必须保证依赖顺序正确。
方式一:传统<script>标签顺序加载
这是原文档给出的经典加载方式,依赖先于 Backbone 声明,业务脚本(router、init)放在最后:
<script src="/javascripts/lib/jquery.js"></script> <script src="/javascripts/lib/underscore.js"></script> <script src="/javascripts/lib/backbone.js"></script> <script src="/javascripts/jst.js"></script> <script src="/javascripts/router.js"></script> <script src="/javascripts/init.js"></script>方式二:RequireJS 的 shim 声明
Backbone 与 Underscore 本身都不是 AMD 规范的库,使用 RequireJS 时需要借助shim声明依赖关系与导出符号。仓库 RequireJS 教程 中给出了标准配置:
require.config({ paths: { "backbone": "vendor/backbone", "underscore": "vendor/underscore" }, shim: { "backbone": { deps: [ "underscore" ], exports: "Backbone" }, "underscore": { exports: "_" } } });上面的配置中,shim指定了 backbone 依赖于 underscore,以及两个库的输出符号(backbone 为Backbone,underscore 为_)。模块化加载后,就可以用 AMD 的define形式引入 Backbone 并定义视图:
define([ 'backbone', 'text!templates.html' ], function( Backbone, template ){ // ... });其中text!前缀的插件会把模板文本作为字符串传入回调函数。
方式三:Browserify 的 CommonJS 打包
在 Node.js 生态中,可以用 npm 安装 Backbone 及其依赖,再通过 Browserify 打包成浏览器可直接加载的单个文件。仓库 Browserify 教程 给出的完整流程如下:
npm install backbone jquery新建main.js:
// main.js var Backbone = require('backbone'); var $ = Backbone.$ = require('jquery/dist/jquery')(window); var AppView = Backbone.View.extend({ render: function(){ $('main').append('<h1>Browserify is a great tool.</h1>'); } }); var appView = new AppView(); appView.render();然后打包:
browserify main.js -o app.jsapp.js可以直接插入 HTML 网页,且只需引入这一个文件,完全不需要再加载 backbone.js 和 jQuery 了。
Backbone 的整体用法:从应用骨架到启动
Backbone 是最早的 JavaScript MVC 框架,也是最简化的一个框架。它的设计思想是:只提供最基本的功能,给用户提供最大的自由。这意味着,好的一面是它没有一整套规则强制你接受;坏的一面是很多功能你必须自己实现。Backbone 的体积相当小,最小化后只有 30 多 KB。
首先,定义一个对象表示 Web 应用,它由四部分组成:Model、Collection、Controller 和 View。
var AppName = { Models :{}, Views :{}, Collections :{}, Controllers :{} };定义 Model,表示数据的一个基本单位:
AppName.Models.Person = Backbone.Model.extend({ urlRoot: "/persons" });定义 Collection,表示 Model 的集合。Collection 对象必须有model属性,指明由哪一个 model 构成:
AppName.Collections.Library = Backbone.Collection.extend({ model: AppName.Models.Book });定义 View。View 对象必须有el属性,指明当前 View 绑定的 DOM 节点;events属性指明事件和对应的方法:
AppName.Views.Modals.AcceptDecline = Backbone.View.Extend({ el: ".modal-accept", events: { "ajax:success .link-accept" :"acceptSuccess", "ajax:error .link-accept" :"acceptError" }, acceptSuccess :function(evt, response) { this.$el.modal("hide"); alert('Cool! Thanks'); }, acceptError :function(evt, response) { var $modalContent = this.$el.find('.panel-modal'); $modalContent.append("Something was wrong!"); } });定义 Controller,负责业务逻辑的编排,例如按 id 取回数据、更新数据、再渲染对应视图:
AppName.Controllers.Person = {}; AppName.Controllers.Person.show = function(id) { var aMa = new AppName.Models.Person({id: id}); aMa.updateAge(25); aMa.fetch().done(function(){ var view = new AppName.Views.Show({model: aMa}); }); };最后,定义路由,启动应用程序。routes对象的键是 URL 路径规则,值是对应的处理函数(这里直接指向 Controller 中的方法):
var Workspace = Backbone.Router.extend({ routes: { "*" :"wholeApp", "users/:id" :"usersShow", "users/:id/orders/" :"ordersIndex" }, wholeApp :AppName.Controller.Application.default, usersShow :AppName.Controller.Users.show, ordersIndex :AppName.Controller.Orders.index }); new Workspace(); Backbone.history.start({pushState: true});Backbone.View:视图层核心 API
基本用法
Backbone.View方法用于定义视图类。通过extend定义视图类,内部用render方法把视图放置到网页上:
var AppView = Backbone.View.extend({ render: function(){ $('main').append('<h1>一级标题</h1>'); } });使用的时候,需要先新建视图类的实例,再通过实例调用render方法:
var appView = new AppView(); appView.render();新建视图实例时,通常需要指定 Model,作为视图的数据来源:
var document = new Document({ model: doc });initialize 方法
视图还可以定义initialize方法,生成实例的时候会自动调用该方法对实例初始化,从而省去生成实例后手动调用render()的步骤:
var AppView = Backbone.View.extend({ initialize: function(){ this.render(); }, render: function(){ $('main').append('<h1>一级标题</h1>'); } }); var appView = new AppView();el 属性与 $el 属性
除了直接在render方法中指定"视图"所绑定的网页元素,还可以用视图的el属性指定网页元素:
var AppView = Backbone.View.extend({ el: $('main'), render: function(){ this.$el.append('<h1>一级标题</h1>'); } });上面的代码与在 render 方法中直接绑定网页元素,效果完全一样。注意这里除了el属性,还有$el属性:前者代表指定的 DOM 元素,后者表示该 DOM 元素对应的 jQuery 对象。
tagName 属性与 className 属性
如果不指定el属性,也可以通过tagName属性和className属性,让 Backbone 自动生成所需的 DOM 元素:
var Document = Backbone.View.extend({ tagName: "li", className: "document", render: function() { // ... } });template 方法(配合 Underscore 模板引擎)
视图的template属性用来指定网页模板。最常用的做法是借助 Underscore 的_.template函数编译模板字符串:
var AppView = Backbone.View.extend({ template: _.template("<h3>Hello <%= who %><h3>"), });_.template接受一个模板字符串作为参数,返回对应的模板函数。只要提供具体的值,就能生成网页代码。完整用法如下:
var AppView = Backbone.View.extend({ el: $('#container'), template: _.template("<h3>Hello <%= who %><h3>"), initialize: function(){ this.render(); }, render: function(){ this.$el.html(this.template({who: 'world!'})); } });这里render调用了template方法,把数据对象{who: 'world!'}代入模板,生成具体的网页代码后写入容器。
关于_.template的细节,仓库 underscore 教程 有更完整的说明,这里补充几个要点:
- 模板字符串就是普通 HTML,变量使用
<%= … %>的形式插入,data对象负责提供变量的值:_.template(txt, {word : "Hello World"}); - 如果变量的值包含五个特殊字符(
& < > " ' /),需要用<%- ... %>转义,例如_.template("<h2><%- word %></h2>", {word : "H & W"})会输出<h2>H & W</h2>; - JavaScript 命令可以采用
<% … %>的形式插入,支持判断语句和循环语句(配合_.each); - 如果
_.template只传第一个参数templateString,则会返回一个函数,之后可以向这个函数输入数据; - 模板内部变量默认是
obj对象的属性,可以通过第三个参数settings中的variable改名,例如_.template(..., null, { variable: "data" }),还能避免with语句带来的性能开销。
实际应用中,一般将模板放在<script>标签中,为了防止浏览器按照 JavaScript 代码解析,type属性设为text/template:
<script type="text/template">window.templates = {}; var $sources = $('script[type="text/template"]'); $sources.each(function(index, el) { var $el = $(el); templates[$el.data('name')] = _.template($el.html()); });events 属性
events属性用于指定视图的事件及其对应的处理函数。键的格式是"事件名 选择器",值是处理函数名:
var Document = Backbone.View.extend({ events: { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" } });上面代码指定了三个 CSS 选择器的单击事件,及其对应的三个处理函数。
listenTo 方法
listenTo方法用于为特定事件指定回调函数,典型场景是让视图监听自己的 Model 变化并自动重绘:
var Document = Backbone.View.extend({ initialize: function() { this.listenTo(this.model, "change", this.render); } });上面代码为 model 的change事件指定了回调函数render。与直接使用model.on(...)相比,listenTo由视图统一管理监听关系,视图销毁时监听也会被自动清理。
remove 方法
remove方法用于移除一个视图,典型用法是先移除旧视图再渲染新视图(更新视图场景):
updateView: function() { view.remove(); view.render(); };子视图(subview)
在父视图中可以调用子视图,实现视图的嵌套组合。下面是一种常见写法:父视图先渲染自己的模板,再创建子视图实例并填充到指定占位容器:
render : function (){ this.$el.html(this.template()); this.child = new Child(); this.child.appendTo($.('.container-placeholder').render(); }Backbone.Events:发布/订阅事件机制
Backbone.Events是一个事件对象。任何继承了这个对象的对象,都具备了Backbone.Events的事件接口,可以调用on和trigger方法,发布和订阅消息。
var EventChannel = _.extend({}, Backbone.Events);下面是几个例子。事件通道可以承载任意类型的数据作为消息载荷:
var channel = $.extend( {}, Backbone.Events ); channel.on('remove-node', function(msg) { // code to remove the node }); channel.trigger( 'remove-node', msg ); // 'msg' can be everything: String, number, object and so forth // also we can pass more than one message like the example below channel.on('add-node', function(node, callback) { // code to add a new node callback(); } ); channel.trigger('add-node', { label: 'I am a new node', color: 'black' }, function() { console.log( 'I am a callback' ); });可以看到,trigger可以携带多个参数(数据对象 + 回调函数),这些参数会原样传给on注册的处理函数。这也体现了 Backbone 事件机制的灵活性:事件名是字符串、载荷可以是任意 JavaScript 值。
Backbone.Router 与 routes 属性
Router 是 Backbone 提供的路由对象,用来将用户请求的网址与后端的处理函数一一对应。
首先,新定义一个 Router 类:
Router = Backbone.Router.extend({ routes: { } });routes 属性
Backbone.Router对象中,最重要的就是routes属性,它用来设置路径的处理方法。routes属性是一个对象,每个成员代表一个路径处理规则:键名为路径规则,键值为处理方法。
根路径:如果键名为空字符串,就代表根路径。
routes: { '': 'phonesIndex', }, phonesIndex: function () { new PhonesIndexView({ el: 'section#main' }); }任意路径捕获:星号代表任意路径,可以设置路径参数,捕获具体的路径值。
var AppRouter = Backbone.Router.extend({ routes: { "*actions": "defaultRoute" } }); var app_router = new AppRouter; app_router.on('route:defaultRoute', function(actions) { console.log(actions); })上面代码中,根路径后面的参数都会被捕获,传入回调函数。
参数占位符的写法::param匹配路径中的一个片段,*splat匹配任意多个路径片段,也支持用-连接多个参数:
var myrouter = Backbone.Router.extend({ routes: { "help": "help", "search/:query": "search" }, help: function() { ... }, search: function(query) { ... } }); routes: { "help/:page": "help", "download/*path": "download", "folder/:name": "openFolder", "folder/:name-:mode": "openFolder" } router.on("route:help", function(page) { ... });上面的:query、:page、:name、:mode都是具名参数,会按顺序作为参数传给对应的路由处理函数;*path是通配参数,捕获download/之后的全部路径。路由触发时,会发出形如route:help的事件,也可以在 Router 实例上监听这些事件。
Backbone.history:启动路由与应用
设置了 Router 以后,就可以启动应用程序。Backbone.history对象用来监控 URL 的变化,是路由真正开始工作前的最后一步。
App = new Router(); $(document).ready(function () { Backbone.history.start({ pushState: true }); });打开pushState选项后,路由将使用 HTML5 History API 管理 URL。如果应用程序不在根目录,就需要通过root参数指定根目录:
Backbone.history.start({pushState: true, root: "/public/search/"})Backbone.Model:数据实体核心 API
Model 代表单个的对象实体。使用extend方法生成一个 Model 类(相当于模板),再用new命令生成实例。defaults属性用来设置默认属性:
var User = Backbone.Model.extend({ defaults: { name: '', email: '' } }); var user = new User();上面代码中,user 对象默认有name和email两个属性,值都等于空字符串。生成实例时,也可以直接提供各个属性的具体值:
var user = new User ({ id: 1, name: 'name', email: 'name@email.com' });idAttribute 属性
Model 实例必须有一个属性,作为区分其他实例的主键。这个属性的名称由idAttribute属性设定,一般设为id:
var Music = Backbone.Model.extend({ idAttribute: 'id' });get 方法
get方法用于返回 Model 实例的某个属性的值:
var user = new User({ name: "name", age: 24}); var age = user.get("age"); // 24 var name = user.get("name"); // "name"set 方法
set方法用于设置 Model 实例的某个属性的值:
var User = Backbone.Model.extend({ buy: function(newCarsName){ this.set({car: newCarsName }); } }); var user = new User({name: 'BMW',model:'i8',type:'car'}); user.buy('Porsche'); var car = user.get("car"); // 'Porsche'on 方法
on方法用于监听对象的变化。事件名"change:name"表示 name 属性发生变化:
var user = new User({name: 'BMW',model:'i8'}); user.on("change:name", function(model){ var name = model.get("name"); // "Porsche" console.log("Changed my car's name to " + name); }); user.set({name: 'Porsche'}); // Changed my car's name to PorscheurlRoot 属性
urlRoot属性用于指定服务器端对 Model 进行操作的路径。一旦设定,该 Model 的所有 REST 请求都会以此为基准:
var User = Backbone.Model.extend({ urlRoot: '/user' });fetch 方法(GET)
fetch用于从服务器取出 Model。实例含有id属性(值为 1)时,fetch方法使用 HTTP 动词 GET,向网址/user/1发出请求,从服务器取出该实例:
var user = new User ({id: 1}); user.fetch({ success: function (user){ console.log(user.toJSON()); } })save 方法(POST / PUT)
save方法用于通知服务器新建或更新 Model。如果 Model 实例不含id属性,save将使用 POST 方法新建该实例。此时它有两个参数:第一个是实例对象的具体属性,第二个是回调函数对象,设定success(保存成功)事件的回调函数:
var User = Backbone.Model.extend({ urlRoot: '/user' }); var user = new User (); var userDetails = { name: 'name', email: 'name@email.com' }; user.save(userDetails, { success: function (user) { console.log(user.toJSON()); } })具体来说,save方法会向/user发出一个 POST 请求,并将{name: 'name', email: 'name@email.com'}作为数据提供。
如果 Model 实例含有id属性,save将使用 PUT 方法更新该实例:
var user = new User ({ id: 1, name: '张三', email: 'name@email.com' }); user.save({name: '李四'}, { success: function (model) { console.log(user.toJSON()); } });对象实例含有id属性(值为 1),save将使用 PUT 方法向网址/user/1发出请求,从而更新该实例。
destroy 方法(DELETE)
destroy方法用于在服务器上删除该实例。它将使用 HTTP 动词 DELETE,向网址/user/1发出请求,删除对应的 Model 实例:
var user = new User ({ id: 1, name: 'name', email: 'name@email.com' }); user.destroy({ success: function () { console.log('Destroyed'); } });综上,Backbone.Model 与服务器通信时遵循 REST 风格:fetch对应 GET,save根据有无id分别对应 POST 与 PUT,destroy对应 DELETE。
Backbone.Collection:Model 的集合
Collection 是同一类 Model 的集合。打个比方:Model 是动物,Collection 就是动物园;Model 是单个的人,Collection 就是一家公司。
定义 Collection 时,model属性指明该集合由哪一种 Model 构成:
var Song = Backbone.Model.extend({}); var Album = Backbone.Collection.extend({ model: Song });上面代码中,Song 是 Model,Album 是 Collection,Album 的model属性等于 Song,表明 Album 是 Song 的集合。
add 方法、remove 方法
Model 的实例可以直接放入 Collection 的实例,也可以用add方法添加:
var song1 = new Song({ id: 1 ,name: "歌名1", artist: "张三" }); var song2 = new Music ({id: 2,name: "歌名2", artist: "李四" }); var myAlbum = new Album([song1, song2]); var song3 = new Music({ id: 3, name: "歌名3",artist:"赵五" }); myAlbum.add(song3);remove方法用于从 Collection 实例中移除一个 Model 实例,参数是 model 实例的id属性:
myAlbum.remove(1);get 方法
get方法用于从 Collection 中获取指定 id 的 Model 实例:
myAlbum.get(2)fetch 方法
fetch方法用于从服务器取出整个 Collection 的数据。需要先给 Collection 指定url属性:
var songs = new Backbone.Collection; songs.url = '/songs'; songs.fetch();延伸阅读
- 继续深入 Backbone 的模板协作对象 Underscore,可阅读仓库的 underscore 教程,重点看
template、each、extend等函数的完整说明; - 用 AMD 规范组织 Backbone 模块,可参考仓库的 RequireJS 教程 中
shim配置与text!插件的用法; - 用 CommonJS 与 Browserify 打包 Backbone 应用,可参考仓库的 Browserify 教程;
- Backbone 诞生的历史背景与模块化开发时代的来龙去脉,可参考仓库的 JavaScript 发展史;
- 本文"零框架解决方案"中提到的 Promise 类库,其异步编程思想可结合仓库的 Promise 对象 一章理解。
- 教程
- 文档
【免费下载链接】jstutorial
Javascript tutorial book
相关推荐
ASP.NET Core MVC 架构模式解析:Model、View、Controller 与实战落地
ASP.NET Core MVC 架构模式解析:Model、View、Controller 与实战落地 导读 :MVC(Model View Controlle
文档教程知识库终极 DLSS Swapper 使用指南:不开游戏目录替换 DLSS 版本,帧率画质一步到位
终极 DLSS Swapper 使用指南:不开游戏目录替换 DLSS 版本,帧率画质一步到位 你的游戏自带的 DLSS 版本太旧,帧率上不去,又不想手动翻游戏目
桌面应用Android Debug Database MVC架构详解:Model-View-Controller在Web界面中的实现
Android Debug Database MVC架构详解:Model View Controller在Web界面中的实现 Android Debug Dat
开发工具数据库客户端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考