β

配合AMD/RequireJS使用Backbone.js

葱丝瓣酱 1150 阅读

为什么使用AMD/RequireJS?

还记得C语言是怎么解决引用冲突的么?

#ifndef _MY_MODULE_H_
#define _MY_MODULE_H_

/*my code here*/

#endif  /*_MY_MODULE_H_*/

Backbone解决的是将用户数据,页面显示,以及流程控制模块化,而AMD解决的是将不同功能的代码封装到小的代码单元,代码单元功能的注册,以及代码依赖。

What are JavaScript modules? What is their purpose?
- Definition: how to encapsulate a piece of code into a useful unit, and how to register its capability/export a value for the module.
- Dependency References: how to refer to other units of code.

如何使用AMD/RequireJS?

下面javascript代码定义了一个新的模块module1,其依赖jquery模块:

define('module1', ['jquery'] , function ($) {
    return function () {};
});

define函数接受三个参数:

官网可以查到详细的说明和用例。

如何配合使用BackboneRequireJS?

正式的Backbone和Underscore版本已经不再缺省支持AMD,不过我们有好几种方法可以在AMD中使用Backbone和Underscore。Tim Branyen在他的博文 AMD/RequireJS Shim Plugin for Loading Incompatible JavaScript 中详细阐述了解决办法。下面的例子程序也是采用了Branyen推荐的方法。

使用AMD/RequireJS重写Contacts例子程序

这里主要用到了AMD/RequireJS的以下模块插件:

下面是所有模块代码:

Model: Contact

define([
  'use!underscore',
  'use!backbone'
], function(_, Backbone) {
  var Contact = Backbone.Model.extend({
    defaults: {
      name: 'unamed',
      email: ''
    },
    filter: function(query) {//helper function for user search
      if (typeof(query) === 'undefined' || query === null || query === '') return true;
      query = query.toLowerCase();
      return this.get('name').toLowerCase().indexOf(query) != -1 || this.get('email').toLowerCase().indexOf(query) != -1;
    }
  });
  return Contact;
});

Collection: Contacts

define([
  'use!underscore',
  'use!backbone',
  'models/contact',
  'store'
], function(_, Backbone, Contact, Store) {
  var Contacts = Backbone.Collection.extend({
    model: Contact,
    localStorage: new Store('my-contacts')
  });

  return new Contacts();
});

View: ContactItem

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'namespace',
  'text!templates/item.html'
], function($, _, Backbone, global, tplItem) {
  // contact item view
  var ContactItemView = Backbone.View.extend({
    className: 'item',
    template: _.template(tplItem),
    events: {
      'click': 'select'
    },
    // initialize
    initialize: function() {
      _.bindAll(this, 'select');
      this.model.bind('reset', this.render, this);
      this.model.bind('change', this.render, this);
      this.model.bind('destroy', this.remove, this);
      if (this.model.view) this.model.view.remove();
      this.model.view = this;
    },
    // render the contact item
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      return this;
    },
    select: function() {
      if (global.app && global.app.router)
        global.app.router.navigate('contacts/' + this.model.cid, {
          trigger: true
        });
    },
    active: function() {
      this.$el.addClass('active');
    },
    deactive: function() {
      this.$el.removeClass('active');
    }
  });

  return ContactItemView;
});

item template

<%= (name ? name : "<i>No Name</i>") %>

View: Sidebar

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'namespace',
  'text!templates/item.html'
], function($, _, Backbone, global, tplItem) {
  // contact item view
  var ContactItemView = Backbone.View.extend({
    className: 'item',
    template: _.template(tplItem),
    events: {
      'click': 'select'
    },
    // initialize
    initialize: function() {
      _.bindAll(this, 'select');
      this.model.bind('reset', this.render, this);
      this.model.bind('change', this.render, this);
      this.model.bind('destroy', this.remove, this);
      if (this.model.view) this.model.view.remove();
      this.model.view = this;
    },
    // render the contact item
    render: function() {
      this.$el.html(this.template(this.model.toJSON()));
      return this;
    },
    select: function() {
      if (global.app && global.app.router)
        global.app.router.navigate('contacts/' + this.model.cid, {
          trigger: true
        });
    },
    active: function() {
      this.$el.addClass('active');
    },
    deactive: function() {
      this.$el.removeClass('active');
    }
  });

  return ContactItemView;
});

sidebar template

<header>
  <input type="search" placeholder="search" results="0" incremental="true" autofocus>
</header>
<div class="items"></div>
<footer>
  <button>New Contact</button>
</footer>

View: Edit, Show 和 Main

ShowView用于显示选中Contact的详细内容:

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'namespace',
  'text!templates/show.html'
], function($, _, Backbone, global, tplShow) {
  // show selected contact details
  var ShowView = Backbone.View.extend({
    className: 'show',
    template: _.template(tplShow),
    events: {
      'click .edit': 'edit'
    },
    initialize: function() {
      _.bindAll(this, 'edit');
    },
    render: function() {
      if (this.item)
        this.$el.html(this.template(this.item.toJSON()));
      else
        this.$el.html('');
      return this;
    },
    change: function(item) {
      this.item = item;
      this.render();
    },
    edit: function() {
      if (this.item && global.app && global.app.router)
        global.app.router.navigate('contacts/' + this.item.cid + '/edit', {
          trigger: true
        });
    }
  });

  return new ShowView();
});
show template
<header>
  <a class="edit">Edit</a>
</header>
<div class="content">
  <p><label>name: <%= name %></label></p>
  <p><label>email: <%= email %></label></p>
</div>

EditView用于编辑选中Contact的详细内容:

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'namespace',
  'text!templates/edit.html'
], function($, _, Backbone, global, tplEdit) {
  // edit selected contact
  var EditView = Backbone.View.extend({
    className: 'edit',
    template: _.template(tplEdit),
    events: {
      'submit form': 'submit',
      'click .save': 'submit',
      'click .delete': 'remove'
    },
    initialize: function() {
      _.bindAll(this, 'submit', 'remove');
    },
    render: function() {
      if (this.item) this.$el.html(this.template(this.item.toJSON()));
      return this;
    },
    change: function(item) {
      this.item = item;
      this.render();
    },
    submit: function() {
      this.item.set(this.form());
      this.item.save();
      if (global.app && global.app.router)
        global.app.router.navigate('contacts/' + this.item.cid, {
          trigger: true
        });
      return false;
    },
    form: function() {
      return {
        name: this.$('form [name="name"]').val(),
        email: this.$('form [name="email"]').val()
      };
    },
    remove: function() {
      this.item.destroy();
      this.item = null;
      if (global.app && global.app.router)
        global.app.router.navigate('', {
          trigger: true
        });
    }
  });

  return new EditView();
});
edit template
<header>
  <a class="save">Save</a>
  <a class="delete">Delete</a>
</header>
<div class="content">
  <form>
    <label>
      <span>Name</span>
      <input type="text" name="name" value="<%= name %>">
    </label>
    <label>
      <span>Email</span>
      <input type="email" name="email" value="<%= email %>">
    </label>
    <button>Save</button>
  </form>
</div>

MainViewShowViewEditView的容器:

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'views/show',
  'views/edit'
], function($, _, Backbone, showView, editView) {
  // main view for show and view
  var MainView = Backbone.View.extend({
    className: 'main stack',
    initialize: function() {
      this.editView = editView;
      this.showView = showView;
    },
    render: function() {
      this.$el.append(this.showView.render().el);
      this.$el.append(this.editView.render().el);
      return this;
    },
    edit: function(item) {
      this.showView.$el.removeClass('active');
      this.editView.$el.addClass('active');
      this.editView.change(item);
    },
    show: function(item) {
      this.editView.$el.removeClass('active');
      this.showView.$el.addClass('active');
      this.showView.change(item);
    }
  });

  return new MainView();
});

View: App

define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'views/sidebar',
  'views/main'
], function($, _, Backbone, sidebarView, mainView) {
  var AppView = Backbone.View.extend({
    className: 'contacts',
    initialize: function() {
      this.sidebar = sidebarView;
      this.main = mainView;
      this.vdiv = $('<div />').addClass('vdivide');
      this.render();
    },
    render: function() {
      this.$el.append(this.sidebar.render().el);
      this.$el.append(this.vdiv);
      this.$el.append(this.main.render().el);
      $('#article').append(this.el);
      return this;
    },
    show: function(item) {
      this.sidebar.active(item);
      this.main.show(item);
    },
    edit: function(item) {
      this.sidebar.active(item);
      this.main.edit(item);
    }
  });

  return new AppView();
});

namespace

全局实例对象,用于存放所有的全局定义和实例对象,同时扩展了Backbone.Events的功能,可以提供模块间的pub/sub服务。

define([
  'use!underscore',
  'use!backbone',
], function(_, Backbone){
  //Context to store global variable.
  // It also provides the pubsub service for other modules.
  var global = {};
  _.extend(global, Backbone.Events);

  return global;
});

store

// Filename: store.js
define([
  'use!underscore',
  'use!backbone'
], function(_, Backbone) {
  // A simple module to replace `Backbone.sync` with *localStorage*-based
  // persistence. Models are given GUIDS, and saved into a JSON object. Simple
  // as that.

  // Generate four random hex digits.
  function S4() {
     return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
  }

  // Generate a pseudo-GUID by concatenating random hexadecimal.
  function guid() {
     return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
  }

  // Our Store is represented by a single JS object in *localStorage*. Create it
  // with a meaningful name, like the name you'd give a table.
  var Store = function(name) {
    this.name = name;
    var store = localStorage.getItem(this.name);
    this.data = (store && JSON.parse(store)) || {};
  };

  _.extend(Store.prototype, {

    // Save the current state of the **Store** to *localStorage*.
    save: function() {
      localStorage.setItem(this.name, JSON.stringify(this.data));
    },

    // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already
    // have an id of it's own.
    create: function(model) {
      if (!model.id) model.id = model.attributes.id = guid();
      this.data[model.id] = model;
      this.save();
      return model;
    },

    // Update a model by replacing its copy in `this.data`.
    update: function(model) {
      this.data[model.id] = model;
      this.save();
      return model;
    },

    // Retrieve a model from `this.data` by id.
    find: function(model) {
      return this.data[model.id];
    },

    // Return the array of all models currently in storage.
    findAll: function() {
      return _.values(this.data);
    },

    // Delete a model from `this.data`, returning it.
    destroy: function(model) {
      delete this.data[model.id];
      this.save();
      return model;
    }

  });

  // Override `Backbone.sync` to use delegate to the model or collection's
  // *localStorage* property, which should be an instance of `Store`.
  Backbone.sync = function(method, model, options) {

    var resp;
    var store = model.localStorage || model.collection.localStorage;

    switch (method) {
      case "read":    resp = model.id ? store.find(model) : store.findAll(); break;
      case "create":  resp = store.create(model);                            break;
      case "update":  resp = store.update(model);                            break;
      case "delete":  resp = store.destroy(model);                           break;
    }

    if (resp) {
      options.success(resp);
    } else {
      options.error("Record not found");
    }
  };

  return Store;
});

router

// Filename: router.js
define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'collections/contacts',
  'views/app'
], function($, _, Backbone, contacts, appView) {
  //Router
  var AppRouter = Backbone.Router.extend({
    routes: {
      '': 'show',
      'contacts/:id': 'show',
      'contacts/:id/edit': 'edit'
    },

    show: function(id) {
      appView.show(this.getContact(id));
    },
    edit: function(id) {
      appView.edit(this.getContact(id));
    },
    getContact: function(id) {
      if (typeof(id) === 'undefined' || id === null)
        if (contacts.length > 0)
          return contacts.at(0);
      return contacts.getByCid(id);
    }
  });

  return new AppRouter();
});

app

// Filename: app.js
define([
  'jquery',
  'use!underscore',
  'use!backbone',
  'collections/contacts',
  'router',
  'views/app',
  'namespace'
], function($, _, Backbone, contacts, appRouter, appView, global){
  var initialize = function() {
    // assign global variable
    global.app = {
      contacts : contacts,
      router : appRouter,
      view : appView
    };
    // fetch data
    contacts.fetch();
    //start
    Backbone.history.start();
  };
  return {
    initialize: initialize
  };
});

main

// Filename: main.js
require([
  'app'
], function(app){
  // The "app" dependency is passed in as "App"
  // Again, the other dependencies passed in are not "AMD" therefore don't pass a parameter to this function
  app.initialize();
});

config

// Require.js allows us to configure shortcut alias
// Their usage will become more apparent futher along in the tutorial.
require.config({
  // Initialize the application with the main application file
  deps: ["main"],

  paths: {
    // JavaScript folders
    libs: "libs",
    // Libraries
    jquery: 'libs/jquery/1.7.2/jquery',
    underscore: 'libs/underscore/1.3.2/underscore',
    backbone: 'libs/backbone/0.9.2/backbone',
    // requirejs plugins
    text: 'libs/require/plugins/text',
    use: 'libs/require/plugins/use',
    order: 'libs/require/plugins/order',
    //template
    templates: '../templates'
  },

  use: {
    backbone: {
      deps: ["use!underscore", "jquery"],
      attach: "Backbone"
    },

    underscore: {
      attach: "_"
    }
  }
});

index.html

index.html中只需要一个javascript加载语句:

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>App</title>
  <link rel="stylesheet" href="http://xiaocong.github.io/blog/2012/05/16/use-amd-and-backbone-together/"css/application.css" type="text/css" charset="utf-8">
  <script data-main="js/config" src=""js/libs/require/require.js"></script>
</head>
<body>
  <header id="header"><h1>AMD-Backbone Contacts</h1></header>
  <article id="article"></article>
</body>
</html>

关于AMD/RequireJS的一些思考

AMD/RequireJS是按照模块之间的依赖关系进行异步加载。程序开发人员根据需求来定义依赖的模块,以及本模块的实现,而不用过多操心依赖的模块是否已经加载,加载执行顺序由AMD/RequireJS来保证。

在正常的依赖关系下,如A依赖于BB依赖于C,那么模块执行的先后顺序是C–>B–>A。但是记住一点,这里讲的是模块初始化过程中的加载执行顺序,并不涉及到用户操作过程中的模块依赖关系。上诉例子中,可能出现C的用户事件响应函数需要访问A的数据,但是只要这个用户事件响应不是在模块初始化过程中发生的,我们就不能认为C依赖于A。模块初始化过程中的加载执行顺序肯定是不能出现循环依赖,否则就是死循环。

为了解决上诉例子中C的用户事件响应函数需要访问A的数据的问题,我们可以引人一个全局对象模块namespace。当模块A完成初始化之后,将自身对象注册到这个namespace中;namespace是一个被动对象,其在初始化过程中不依赖任何其他对象,而所有其他模块都可以依赖于namespace;当初始化完成后,C可以通过namespace访问到A

在地址本例子中,namespace就是一个全局对象模块;因为模块依赖定义中,AppRouter依赖于AppView,而AppView的依赖模块EditViewShowViewSidebarView需要能访问AppRouter对象;通过namespace全局对象模块,任何模块都可以运行时访问AppRouter对象。当然,我们也可以通过namespace,使得初始化过程中AppRouterAppView没有依赖关系。

在浏览器中这个namespace可以是window对象或其子对象。

"
作者:葱丝瓣酱
原文地址:配合AMD/RequireJS使用Backbone.js, 感谢原作者分享。

发表评论