Backbone.js混合模式 #

一、Mixin概述 #

1.1 什么是Mixin #

Mixin是一种代码复用模式,允许将功能模块混入到类中。

text
Mixin特点
├── 代码复用:提取通用功能
├── 组合优于继承:灵活组合功能
├── 单一职责:每个Mixin专注一个功能
└── 无侵入性:不修改原有类结构

1.2 Mixin vs 继承 #

特性 继承 Mixin
关系 is-a has-a
复用 单一继承链 多个Mixin组合
灵活性 较低 较高
复杂度 继承层次深时复杂 组合灵活

二、基本Mixin #

2.1 简单Mixin #

javascript
var TimestampMixin = {
    touch: function() {
        this.set('updatedAt', new Date().toISOString());
    },
    
    getAge: function() {
        var created = new Date(this.get('createdAt'));
        return Date.now() - created.getTime();
    }
};

var User = Backbone.Model.extend({
    initialize: function() {
        this.set('createdAt', new Date().toISOString());
    }
});

_.extend(User.prototype, TimestampMixin);

var user = new User();
user.touch();

2.2 带初始化的Mixin #

javascript
var EventLoggerMixin = {
    initializeEventLogger: function() {
        this.on('all', function(eventName) {
            console.log('[Event]', eventName);
        });
    }
};

var User = Backbone.Model.extend({
    initialize: function() {
        this.initializeEventLogger();
    }
});

_.extend(User.prototype, EventLoggerMixin);

2.3 带状态的Mixin #

javascript
var LoadingMixin = {
    isLoading: function() {
        return this._loading === true;
    },
    
    setLoading: function(loading) {
        this._loading = loading;
        this.trigger('loading:change', loading);
    }
};

var User = Backbone.Model.extend({
    fetch: function(options) {
        this.setLoading(true);
        
        var self = this;
        options = options || {};
        var success = options.success;
        
        options.success = function() {
            self.setLoading(false);
            if (success) success.apply(this, arguments);
        };
        
        return Backbone.Model.prototype.fetch.call(this, options);
    }
});

_.extend(User.prototype, LoadingMixin);

三、常用Mixin #

3.1 验证Mixin #

javascript
var ValidationMixin = {
    validateRequired: function(fields) {
        var errors = {};
        var hasError = false;
        
        fields.forEach(function(field) {
            if (!this.get(field)) {
                errors[field] = field + ' is required';
                hasError = true;
            }
        }, this);
        
        return hasError ? errors : undefined;
    },
    
    validateEmail: function(field) {
        var value = this.get(field);
        if (value && !value.match(/^[\w-]+@[\w-]+\.[a-z]+$/i)) {
            return 'Invalid email format';
        }
    },
    
    validateMinLength: function(field, minLength) {
        var value = this.get(field);
        if (value && value.length < minLength) {
            return field + ' must be at least ' + minLength + ' characters';
        }
    }
};

var User = Backbone.Model.extend({
    validate: function(attrs) {
        var errors = this.validateRequired(['name', 'email']);
        
        if (!errors) {
            errors = {};
        }
        
        var emailError = this.validateEmail('email');
        if (emailError) errors.email = emailError;
        
        return Object.keys(errors).length > 0 ? errors : undefined;
    }
});

_.extend(User.prototype, ValidationMixin);

3.2 选择Mixin #

javascript
var SelectableMixin = {
    select: function() {
        if (!this._selected) {
            this._selected = true;
            this.trigger('selected', this);
        }
    },
    
    deselect: function() {
        if (this._selected) {
            this._selected = false;
            this.trigger('deselected', this);
        }
    },
    
    toggleSelect: function() {
        if (this._selected) {
            this.deselect();
        } else {
            this.select();
        }
    },
    
    isSelected: function() {
        return this._selected === true;
    }
};

var Item = Backbone.Model.extend({});
_.extend(Item.prototype, SelectableMixin);

3.3 过滤Mixin #

javascript
var FilterMixin = {
    filterBy: function(criteria) {
        return this.filter(function(model) {
            for (var key in criteria) {
                if (model.get(key) !== criteria[key]) {
                    return false;
                }
            }
            return true;
        });
    },
    
    searchBy: function(fields, query) {
        query = query.toLowerCase();
        
        return this.filter(function(model) {
            return fields.some(function(field) {
                var value = model.get(field);
                return value && value.toLowerCase().indexOf(query) !== -1;
            });
        });
    }
};

var Users = Backbone.Collection.extend({});
_.extend(Users.prototype, FilterMixin);

3.4 分页Mixin #

javascript
var PaginationMixin = {
    page: 1,
    perPage: 10,
    
    setPage: function(page) {
        this.page = page;
        this.trigger('page:change', page);
    },
    
    getPage: function() {
        var start = (this.page - 1) * this.perPage;
        var end = start + this.perPage;
        return this.slice(start, end);
    },
    
    getTotalPages: function() {
        return Math.ceil(this.length / this.perPage);
    },
    
    hasNextPage: function() {
        return this.page < this.getTotalPages();
    },
    
    hasPrevPage: function() {
        return this.page > 1;
    },
    
    nextPage: function() {
        if (this.hasNextPage()) {
            this.setPage(this.page + 1);
        }
    },
    
    prevPage: function() {
        if (this.hasPrevPage()) {
            this.setPage(this.page - 1);
        }
    }
};

var Users = Backbone.Collection.extend({});
_.extend(Users.prototype, PaginationMixin);

四、高级Mixin #

4.1 带生命周期的Mixin #

javascript
var DisposableMixin = {
    initializeDisposable: function() {
        this._disposables = [];
    },
    
    registerDisposable: function(disposable) {
        this._disposables.push(disposable);
    },
    
    dispose: function() {
        this._disposables.forEach(function(disposable) {
            if (typeof disposable === 'function') {
                disposable();
            } else if (disposable.dispose) {
                disposable.dispose();
            }
        });
        this._disposables = [];
        this.trigger('disposed');
    }
};

var MyView = Backbone.View.extend({
    initialize: function() {
        this.initializeDisposable();
        
        this.registerDisposable(
            this.listenTo(this.model, 'change', this.render)
        );
    },
    
    remove: function() {
        this.dispose();
        Backbone.View.prototype.remove.call(this);
    }
});

_.extend(MyView.prototype, DisposableMixin);

4.2 带配置的Mixin #

javascript
function createValidationMixin(rules) {
    return {
        validate: function(attrs) {
            var errors = {};
            
            for (var field in rules) {
                var fieldRules = rules[field];
                
                fieldRules.forEach(function(rule) {
                    var value = attrs[field];
                    
                    if (rule.required && !value) {
                        errors[field] = rule.message || field + ' is required';
                    }
                    
                    if (rule.pattern && value && !rule.pattern.test(value)) {
                        errors[field] = rule.message || field + ' is invalid';
                    }
                    
                    if (rule.minLength && value && value.length < rule.minLength) {
                        errors[field] = rule.message || field + ' is too short';
                    }
                });
            }
            
            return Object.keys(errors).length > 0 ? errors : undefined;
        }
    };
}

var User = Backbone.Model.extend({});
_.extend(User.prototype, createValidationMixin({
    name: [
        { required: true, message: 'Name is required' },
        { minLength: 2, message: 'Name must be at least 2 characters' }
    ],
    email: [
        { required: true, message: 'Email is required' },
        { pattern: /^[\w-]+@[\w-]+\.[a-z]+$/i, message: 'Invalid email' }
    ]
}));

4.3 组合Mixin #

javascript
var mixin = function(target) {
    var sources = Array.prototype.slice.call(arguments, 1);
    
    sources.forEach(function(source) {
        var descriptors = {};
        
        Object.keys(source).forEach(function(key) {
            descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
        });
        
        Object.defineProperties(target, descriptors);
    });
    
    return target;
};

var User = Backbone.Model.extend({});
mixin(User.prototype, TimestampMixin, ValidationMixin, SelectableMixin);

五、实用示例 #

5.1 表单Mixin #

javascript
var FormMixin = {
    getFormData: function() {
        var data = {};
        
        this.$('input, select, textarea').each(function() {
            var $el = $(this);
            var name = $el.attr('name');
            
            if (name) {
                data[name] = $el.val();
            }
        });
        
        return data;
    },
    
    setFormData: function(data) {
        for (var name in data) {
            var $el = this.$('[name="' + name + '"]');
            if ($el.length) {
                $el.val(data[name]);
            }
        }
    },
    
    clearForm: function() {
        this.$('input, select, textarea').val('');
    },
    
    validateForm: function(rules) {
        var data = this.getFormData();
        var errors = {};
        
        for (var field in rules) {
            var value = data[field];
            var fieldRules = rules[field];
            
            if (fieldRules.required && !value) {
                errors[field] = fieldRules.requiredMessage || 'Required';
            }
        }
        
        return Object.keys(errors).length > 0 ? errors : null;
    }
};

5.2 拖拽Mixin #

javascript
var DraggableMixin = {
    enableDrag: function(options) {
        var self = this;
        options = options || {};
        
        this.$el.addClass('draggable');
        
        this.$el.on('mousedown.draggable', function(e) {
            e.preventDefault();
            
            var startX = e.pageX;
            var startY = e.pageY;
            var startPos = self.$el.position();
            
            $(document).on('mousemove.draggable', function(e) {
                var dx = e.pageX - startX;
                var dy = e.pageY - startY;
                
                self.$el.css({
                    left: startPos.left + dx,
                    top: startPos.top + dy
                });
                
                self.trigger('drag', { dx: dx, dy: dy });
            });
            
            $(document).on('mouseup.draggable', function() {
                $(document).off('.draggable');
                self.trigger('drag:end');
            });
            
            self.trigger('drag:start');
        });
    },
    
    disableDrag: function() {
        this.$el.removeClass('draggable');
        this.$el.off('.draggable');
    }
};

六、总结 #

6.1 Mixin最佳实践 #

实践 说明
单一职责 每个Mixin只做一件事
无状态优先 避免Mixin内部状态
命名约定 使用Mixin后缀
文档化 说明Mixin的用途和依赖

6.2 适用场景 #

  • 通用功能复用
  • 跨继承链的代码共享
  • 功能组合
  • 插件式扩展
最后更新:2026-03-28