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 开发建议 #
- 保持代码简洁清晰
- 遵循单一职责原则
- 合理使用继承和混入
- 编写可测试的代码
- 持续重构优化
最后更新:2026-03-28