Backbone.js最佳实践 #

一、项目结构 #

1.1 推荐目录结构 #

text
my-app/
├── index.html
├── css/
│   └── main.css
├── js/
│   ├── app.js
│   ├── main.js
│   ├── router.js
│   ├── models/
│   │   ├── user.js
│   │   └── post.js
│   ├── collections/
│   │   ├── users.js
│   │   └── posts.js
│   ├── views/
│   │   ├── base/
│   │   │   └── base-view.js
│   │   ├── users/
│   │   │   ├── user-list-view.js
│   │   │   └── user-item-view.js
│   │   └── common/
│   │       ├── header-view.js
│   │       └── footer-view.js
│   ├── templates/
│   │   ├── users/
│   │   │   ├── user-list.html
│   │   │   └── user-item.html
│   │   └── common/
│   │       ├── header.html
│   │       └── footer.html
│   └── utils/
│       ├── template-loader.js
│       └── api-client.js
└── lib/
    ├── backbone.js
    ├── underscore.js
    └── jquery.js

1.2 模块化组织 #

javascript
define([
    'backbone',
    'models/user',
    'views/user-item-view'
], function(Backbone, User, UserItemView) {
    var UserListView = Backbone.View.extend({
        render: function() {
            this.collection.each(function(user) {
                var view = new UserItemView({ model: user });
                this.$el.append(view.render().el);
            }, this);
            return this;
        }
    });
    
    return UserListView;
});

二、命名约定 #

2.1 文件命名 #

text
模型:user.js(单数,小写)
集合:users.js(复数,小写)
视图:user-list-view.js(小写,连字符)
模板:user-list.html(小写,连字符)

2.2 类命名 #

javascript
var User = Backbone.Model.extend({});
var Users = Backbone.Collection.extend({});
var UserListView = Backbone.View.extend({});
var AppRouter = Backbone.Router.extend({});

2.3 事件命名 #

javascript
this.trigger('user:created', user);
this.trigger('user:updated', user);
this.trigger('user:deleted', userId);

this.trigger('collection:reset');
this.trigger('collection:sorted');

三、代码组织 #

3.1 基类设计 #

javascript
var BaseModel = Backbone.Model.extend({
    initialize: function(attrs, options) {
        this.options = options || {};
    },
    
    fetch: function(options) {
        options = options || {};
        this.trigger('fetch:start');
        
        var self = this;
        var success = options.success;
        
        options.success = function() {
            self.trigger('fetch:success');
            if (success) success.apply(this, arguments);
        };
        
        return Backbone.Model.prototype.fetch.call(this, options);
    }
});

var BaseView = Backbone.View.extend({
    initialize: function(options) {
        this.options = options || {};
        this.childViews = [];
    },
    
    addChild: function(view) {
        this.childViews.push(view);
        return view;
    },
    
    remove: function() {
        this.childViews.forEach(function(view) {
            view.remove();
        });
        this.childViews = [];
        this.stopListening();
        Backbone.View.prototype.remove.call(this);
    }
});

3.2 视图模式 #

javascript
var UserListView = BaseView.extend({
    template: _.template($('#user-list-template').html()),
    
    events: {
        'click .add-btn': 'addUser',
        'click .refresh-btn': 'refresh'
    },
    
    initialize: function(options) {
        BaseView.prototype.initialize.call(this, options);
        
        this.listenTo(this.collection, 'add', this.addOne);
        this.listenTo(this.collection, 'remove', this.removeOne);
        this.listenTo(this.collection, 'reset', this.render);
    },
    
    render: function() {
        this.$el.html(this.template({}));
        this.renderList();
        return this;
    },
    
    renderList: function() {
        this.collection.each(this.addOne, this);
    },
    
    addOne: function(user) {
        var view = new UserItemView({ model: user });
        this.addChild(view);
        this.$('.user-list').append(view.render().el);
    },
    
    removeOne: function(user) {
        this.$('[data-id="' + user.id + '"]').remove();
    },
    
    addUser: function() {
        this.trigger('user:add');
    },
    
    refresh: function() {
        this.collection.fetch({ reset: true });
    }
});

3.3 模型模式 #

javascript
var User = BaseModel.extend({
    urlRoot: '/api/users',
    
    defaults: {
        name: '',
        email: '',
        role: 'user',
        active: true
    },
    
    validate: function(attrs) {
        var errors = {};
        
        if (!attrs.name) {
            errors.name = 'Name is required';
        }
        
        if (!attrs.email) {
            errors.email = 'Email is required';
        } else if (!this.isValidEmail(attrs.email)) {
            errors.email = 'Invalid email format';
        }
        
        return Object.keys(errors).length > 0 ? errors : undefined;
    },
    
    isValidEmail: function(email) {
        return /^[\w-]+@[\w-]+\.[a-z]+$/i.test(email);
    },
    
    isAdmin: function() {
        return this.get('role') === 'admin';
    },
    
    activate: function() {
        this.save({ active: true });
    },
    
    deactivate: function() {
        this.save({ active: false });
    }
});

四、常见模式 #

4.1 服务模式 #

javascript
var UserService = {
    getCurrentUser: function() {
        var deferred = $.Deferred();
        
        if (this._currentUser) {
            deferred.resolve(this._currentUser);
        } else {
            var user = new User({ id: 'me' });
            user.fetch().done(function() {
                this._currentUser = user;
                deferred.resolve(user);
            }).fail(deferred.reject);
        }
        
        return deferred.promise();
    },
    
    login: function(credentials) {
        return $.ajax({
            url: '/api/auth/login',
            method: 'POST',
            data: JSON.stringify(credentials),
            contentType: 'application/json'
        }).done(function(response) {
            localStorage.setItem('token', response.token);
            this._currentUser = new User(response.user);
        });
    },
    
    logout: function() {
        localStorage.removeItem('token');
        this._currentUser = null;
    }
};

4.2 工厂模式 #

javascript
var ViewFactory = {
    views: {},
    
    create: function(name, options) {
        if (!this.views[name]) {
            throw new Error('View not found: ' + name);
        }
        
        return new this.views[name](options);
    },
    
    register: function(name, ViewClass) {
        this.views[name] = ViewClass;
    }
};

ViewFactory.register('UserList', UserListView);
ViewFactory.register('UserDetail', UserDetailView);

var view = ViewFactory.create('UserList', { collection: users });

4.3 状态机模式 #

javascript
var StateMachine = {
    state: 'idle',
    states: {},
    
    transition: function(newState) {
        var fromState = this.state;
        var toState = newState;
        
        if (!this.canTransition(fromState, toState)) {
            return false;
        }
        
        this.state = toState;
        this.trigger('state:change', toState, fromState);
        this.trigger('state:' + toState);
        
        return true;
    },
    
    canTransition: function(from, to) {
        var transitions = this.states[from];
        return transitions && transitions.indexOf(to) !== -1;
    },
    
    isState: function(state) {
        return this.state === state;
    }
};

var Task = Backbone.Model.extend({
    states: {
        'pending': ['in_progress', 'cancelled'],
        'in_progress': ['completed', 'cancelled'],
        'completed': [],
        'cancelled': []
    }
});

_.extend(Task.prototype, StateMachine, Backbone.Events);

五、错误处理 #

5.1 全局错误处理 #

javascript
var ErrorHandler = {
    setup: function() {
        $(document).ajaxError(function(event, xhr) {
            switch (xhr.status) {
                case 401:
                    this.handleUnauthorized();
                    break;
                case 403:
                    this.handleForbidden();
                    break;
                case 500:
                    this.handleServerError();
                    break;
            }
        }.bind(this));
    },
    
    handleUnauthorized: function() {
        alert('Session expired. Please login again.');
        Backbone.history.navigate('login', { trigger: true });
    },
    
    handleForbidden: function() {
        alert('You do not have permission to perform this action.');
    },
    
    handleServerError: function() {
        alert('Server error. Please try again later.');
    }
};

5.2 模型错误处理 #

javascript
var User = Backbone.Model.extend({
    save: function(attrs, options) {
        options = options || {};
        
        var error = options.error;
        options.error = function(model, xhr) {
            if (xhr.status === 422) {
                model.setErrors(xhr.responseJSON.errors);
            }
            if (error) error.apply(this, arguments);
        };
        
        return Backbone.Model.prototype.save.call(this, attrs, options);
    },
    
    setErrors: function(errors) {
        this._errors = errors;
        this.trigger('error', this, errors);
    },
    
    getErrors: function() {
        return this._errors || {};
    },
    
    hasErrors: function() {
        return Object.keys(this.getErrors()).length > 0;
    }
});

六、测试 #

6.1 模型测试 #

javascript
describe('User Model', function() {
    var user;
    
    beforeEach(function() {
        user = new User({ name: 'Test User', email: 'test@example.com' });
    });
    
    it('should have default values', function() {
        expect(user.get('role')).toBe('user');
        expect(user.get('active')).toBe(true);
    });
    
    it('should validate required fields', function() {
        user.set({ name: '' });
        var error = user.validate(user.attributes);
        expect(error.name).toBeDefined();
    });
    
    it('should validate email format', function() {
        user.set({ email: 'invalid' });
        var error = user.validate(user.attributes);
        expect(error.email).toBeDefined();
    });
});

6.2 视图测试 #

javascript
describe('UserListView', function() {
    var view, collection;
    
    beforeEach(function() {
        collection = new Users([
            { id: 1, name: 'User 1' },
            { id: 2, name: 'User 2' }
        ]);
        
        view = new UserListView({ collection: collection });
        view.render();
    });
    
    afterEach(function() {
        view.remove();
    });
    
    it('should render all users', function() {
        expect(view.$('.user-item').length).toBe(2);
    });
    
    it('should add user when collection adds', function() {
        collection.add({ id: 3, name: 'User 3' });
        expect(view.$('.user-item').length).toBe(3);
    });
});

七、总结 #

7.1 最佳实践清单 #

类别 实践
结构 清晰的目录结构,模块化组织
命名 一致的命名约定
代码 基类设计,代码复用
事件 及时解绑,合理使用委托
错误 统一错误处理
测试 单元测试覆盖

7.2 开发建议 #

  1. 保持代码简洁清晰
  2. 遵循单一职责原则
  3. 合理使用继承和混入
  4. 编写可测试的代码
  5. 持续重构优化
最后更新:2026-03-28