☰
Backbone.js 实战详解:MVC 架构、零框架替代方案与 Model/View/Router 核心用法(基于 jstutorial 仓库)
2026/10/13 8:34:29 网站建设 项目流程
  • 教程
  • 文档

【免费下载链接】jstutorial

Javascript tutorial book

项目地址:https://gitcode.com/gh_mirrors/js/jstutorial
点击查看免费下载

本文是 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.js

app.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 &amp; 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 Porsche

urlRoot 属性

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

项目地址:https://gitcode.com/gh_mirrors/js/jstutorial
点击查看免费下载
上一篇:英雄联盟玩家必看:如何用智能工具在选人阶段就赢在起跑线?
下一篇:百度网盘直链解析方案:突破限速瓶颈的技术实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询