MVC Pattern

2018-03-01

在MVC模式下,一个应用被分解为三个部分:Model(数据), View(表现层), Controller(交互层)…..

MVC是一种应用的设计模式。 在这种模式下,一个应用被分解为三个部分:Model(数据), View(表现层), Controller(交互层)。

The Model

  • Model 应该从View和Controller分离出来, 数据和与数据相关的的逻辑和操作都应该放在Model中。并且应该正确地命名空间。
  • 把Model的属性放在命名空间下可以防止与全局变量产生冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var User = {
records: [/* */],
fetchReomote: function(){ /**/ }

}
```
- 我们可以把实例使用的方法放在类的原型上,而与实例无关的属性则直接添加作为类的属性。
```javascript
var User= function(atts){
this.attributes = atts || {};
};
User.prototype.destory = function(){
/* */
}

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

关系对象映射器(Object-relational mapper or ORM)

  • ORM 是数据的包装层。通过ORM连接Model和服务器, 任何对Model的改变都会通过Ajax请求反馈给服务器。 我们也可以连接Model和HTML元素, 这样Model的任何改变在View(表现层) 上也会有直观体现。

原型继承

  • 通过Object.create()原型继承来构建ORM.
  • Object.create() 方法使用指定的原型对象及其属性去创建一个新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var Model = {
inherited: function(){},
created: function(){},

prototype: {
init: function(){}
},

create: function(){
var object = Object.create(this);
object.parent = this;
object.prototype = object.fn = Object.create(this.prototype);

object.created();
this.inherited(object);
return object;
},

init: function(){
var instance = Object.create(this.prototype);
instance.parent = this;
instance.init.apply(instance, arguments);
return instance;
}
}
  • create()返回一个从Model继承的新对象,用来创建 新的Model类型init()返回一个从Model.prototype继承的新对象——一个Model实例。

添加 ORM 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Add Object properties
jQuery.extend(Model, {
find: function(){}
});
// Add instance properties
jQuery.extend(Model.prototype, {
init: function(atts){
if(atts) this.load(atts);
},
load: function(attributes){
for( var name in attributes ){
this[name] = attributes[name];
}
}
});
  • 目前的部分实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

var Model = {
inherited: function(){},
created: function(){},

prototype: {
init: function(){}
},
create: function(){
var object = Object.create(this);
object.parent = this;
object.prototype = object.fn = object.create(this.prototype);

object.created();
this.inherited(object);
return object;
},
init: function(){
var instance = Object.create(this.prototype);
object.parent = this;
instance.init.apply(instance, arguments);
return instance;
},
extend: function(o){
var extended = o.extended;
for(var a in o){
this[a] = o[a];
}
if(extended) extended(this);
},
include: function(o){
var included = o.included;
for(var a in o){
this.prototype[a] = o[a];
}
if(included) included(this);
}
}

持久记录

  • 我们需要一种保存记录的方法,保存创建实例的引用, 以便稍候访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// An object of saved assets
Model.records = {};
// 记录删减
Model.include({
newRecord: true,
create: function(){
this.newRecord = false;
this.parent.records[this.id] = this;
},
destory: function(){
delete this.parent.records[this.id];
}
});
// 记录更新
Model.include({
update: function(){
this.parent.records[this.id] = this;
}
});
// 记录保存
Model.include({
save: function(){
this.newRecord ? this.create() : this.update();
}
});
//记录查找
Model.extend({
find: function(id){
return this.records[id] || throw ("Unknown record");
}
});

增加 ID 支持

  • 我们需要一种自动生成ID的方法,常见的方法是GUID(Globally Unique Identifier)全局唯一标识符生成器,但是此方法不适用于JavaScript。
  • Robert Kieffer 通过 Math.random() 设计了一个简易的GUID生成器
1
2
3
4
5
6
Math.guid = function(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c){
var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
}).toUpperCase();
}
  • &|执行按位运算。r初始值包含一个从0x0到0xf的随机数,用二进制表示为r = abcd。r & 0x3 执行二进制数abcd与0011的&按位运算,结果为00cd,然后 |0x8执行的结果为10cd,所以r最终可能的值为1000,1001,1010,1011(二进制),最终通过.toString(16)从16进制转化为字母。
1
2
3
4
5
6
7
Model.extend({
create: function(){
if(!this.id) this.id = Math.guid();
this.newRecord = false;
this.parent.records[this.id] = this;
}
});

处理引用问题

  • 如果仔细观察我们的代码你会发现,当Model的一个实例属性改变后,Records里这个实例的对应属性也会自动改变(本应该执行update()之后改变),这是由于 records 的实例和实例本身同时指向相同的对象。
  • 我们可以复制实例本身,然后将复制的加入到records中来解决这个问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Model.extend({
find: function(id){
var record = this.records[id];
if ( !record ) throw('Unknown record');
return record.dup();
}
})
Model.include({
create: function(){
this.newRecord = false;
this.id = Math.guid();
this.parent.records[this.id] = this.dup();
},
update: function(){
this.parent.records[this.id] = this.dup();
},
dup: function(){
return jQuery.extend(true,{},this); // Deep copy 有待实现
}
})
  • 另一个问题是Model.records在任何一个特定的Model子类上都可以访问。
  • 可以去掉Model上的records, 通过Model.created()(Model.create()的callback)在每一个创建的Model子类上分别创建records对象。
    1
    2
    3
    4
    5
    6
    7
    Model.extend(
    {
    created:function(){
    this.records = {};
    }
    }
    )

数据加载

  • 加载数据的三种方法
    • 初始页面行内加载——增大HTTP页面大小
    • 通过AJAX
    • 通过 JSONP
Ajax
  • jQuery Ajax’s API

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    jQuery.ajax({
    url: '/ajax/endpoint',
    type: 'GET',
    success: function(data){
    alert(data);
    }
    });


    jQuery.get('/ajax/endpoint', function(data){
    $('.ajaxResult').text(data);
    });
    jQuery.get('/ajax/endpoint', {foo: 'bar'}, function(data){
    /*...*/
    });
    jQuery.post('/users', {first_name: 'Alex'}, function(result){
    /* Ajax POST was a success */
    })
    jQuery.getJSON('/json/endpoint', function(json){
    /*...*/
    })
  • 同源策略

  • Ajax的限制来自于同源策略,同源策略限制了请求必须发送到页面发送方的域名、子域名、端口。
    • 这是因为在发送Ajax请求时,本域名下的Cookie信息会和Ajax请求一同发送给远端服务器。如果没有同源策略的限制, 不同域名的服务器就可以发访问本域名下的Cookie,获得Cookie中的重要信息,比如邮件信息,登录信息…..
  • CORS 跨站资源共享
    • 通过CORS, 如果你想授权他人访问你的服务器, 只需在返回回复中添加如下头部, 该头部授权来自example.com的跨源GET和POST请求
1
2
Access-Control-Allow-Origin: example.com
Access-Control-Request-Method: GET, POST
  • 可以使用Access-Control-Request-Headers标头授权自定义请求标头
    1
    Access-Control-Request-Headers: Authorization
JSONP
  • Script标签不受同源策略限制。
  • 实现方式: 设置一个<script></script>src属性指向一个JSON端点(发送JSON数据的url), 返回的数据被封装在一个函数调用中。这种方式适用于任何浏览器。
1
2
3
4
5
<script src="http://example.com/data.json">
jQuery.getJson('http://example.com/data.json?callback=?', function(result){
/* Do stuff with the result*/
})
</script>
  • jQuery更换URL中最后一个?为一个临时函数。服务器需要读取callback的值然后将其作为返回的封装函数
跨域请求的安全性问题
  • 如果不确定哪些域可以通过CORS/JSONP访问你的API,要考虑如下几方面:
    • 不要暴露重要信息,与邮箱地址等。
    • 不允许任何操作,如 Follow,Unfollow, Delete….

填充ORM

1
2
3
4
5
6
7
8
9
10
11
12
Model.extend({
populate: function(values){
//Reset model & records
this.records = {};

for(var i = 0, il = values.length; i < il; i++){
var record = this.init(values[i]);
record.newRecord = false;
this.records[record.id] = record.dup();
}
}
})
1
2
3
4
var Asset = User.create()
jQuery.getJson('/assets', function(result){
Asset.populate(result);
})

本地储存数据

  • HTML5中包含了本地储存的实现,大部分浏览器都支持这个特性。大部分浏览器为每个域名提供了至少5MB的储存。
  • HTML5 储存的实现包含两种:
    • local storage: 在浏览器关闭后仍然存在。
    • session storage: 窗口关闭后则消失。
  • 储存的所有数据的作用域是按照域名区分的。并且只能由最初存储数据的域名中的脚本进行操作。
  • 通过 localStoragesessionStorage进行操作。

    1
    2
    3
    4
    5
    6
    7
    localStorage['somData'] = 'wem';

    var itemsStored = localStorage.length;
    localStorage.setItem('someData', 'wem');
    localStorage.getItem('someData'); // 'wem'
    localStorage.removeItem('someData');
    localStorage.clear();
  • 数据是作为字符串储存的,你需要自己完成转换。

    1
    2
    3
    var object = {some: 'object'};
    localStorage.setItem('seriData', JSON.stringify(object));
    var result = JSON.parse(localStorage.getItem('seriData'));
  • 存储数据大小超过上限后, 会产生一个QUOTA_EXCEEDED_ERR错误。

ORM增加本地储存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Model.extend({
created: function(){
this.records = {};
this.attributes = [];
}
});
Asset.attributes = ['name', 'ext'];

Model.include({
attributes: function(){
var result = {};
for(var i in this.parent.attributes){
var attr = this.parent.attributes[i];
result[attr] = this[attr];
}
result.id = this.id;
return result;
}
});
Asset.attributes = ['name', 'ext']; //Set an array of attributes that we save to LocalStorage /SessionStorage
var asset = Asset.init({name: 'document', ext: '.txt'});
asset.attributes(); // => {name: 'document', ext: '.txt'}
Model.include({
toJSON: function(){
return (this.attributes()):
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
var Model.localStorage = {
saveLocal: function(name){
var result = [];
for (var i in this.records){
result.push(this.records[i]);
}
localStorage[name] = JSON.stringify(result);
},
loadLocal: function(name){
var result = JSON.parse(localStorage[name]);
this.populate(result);
}
}
提交新的记录到服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
Model.include({
createRemote: function(url, callback){
$.post(url, this.attributes(), callback);
},
updateRemote: function(url, callback){
$.ajax({
url: url,
data: this.attributes(),
success: callback,
type: 'PUT'
})
}
})
Final Version
Math.guid = function(){
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c){
        var r = Math.random() * 16|0, v = c =='x' ? r: (r&0x3|0x8);
        return v.toString(16);
    }).toUpperCase();
};



var Model = {
    // Methods on the instance 
    prototype: {
        newRecord: true,
        init: function(atts){
            if(atts) this.load(atts);
            this.save();
        },
        load: function(attributes){
            for(var name in attributes){
                this[name] = attributes[name];
                console.log('a'); //_-------------------------patch
            }
        },
        create: function(){
            if(!this.id) this.id = Math.guid();
            this.newRecord = false;
            this.parent.records[this.id] = this.dup();
        },
        save: function(){
            this.newRecord ? this.create() : this.update();
        },
        destory: function(){
            delete this.parent.records[this.id];

        },
        update: function(){
            this.parent.records[this.id] = this.dup();
        },
        dup: function(){
            return jQuery.extend(true, {}, this);// Deep copy;
        },
        attributes: function(){ // 返回由某个类型Model规定需要保存的属性组成的对象
            var result = {};
            for(var i in this.parent.attributes){
                var attr = this.parent.attributes[i];
                result[attr] = this[attr];
            }
            result.id = this.id;
            return result;
        },
        toJSON: function(){
            return (this.attributes());
        },
        createRemote: function(url, callback){
            $.post(url, this.attributes(), callback);
        },
        updateRemote: function(url, callback){
            $.ajax({
                url: url,
                data: this.attributes(),
                success: callback,
                type:'PUT'
            })
        }

    },


    // Methods on the parent
     and grandparent of instance
    inherited: function(){},
    created: function(){
        this.records = {};
        this.attributes = []; // 规定了某个特定Model需要保存的属性
    },
    create: function(){
        var object = Object.create(this);
        object.parent = this;
        object.prototype = object.fn = Object.create(this.prototype);

        object.created();
        this.inherited(object);
        return object;
    },
    init: function(){
        var instance = Object.create(this.prototype);
        instance.parent = this;
        instance.init.apply(instance, arguments); //调用原型prototype中的init来initlize instance
        return instance;
    },

    find: function(id){
        return this.records[id];
    },
    extend: function(o){
        var extended = o.extended;
        for(var a in o){
            this[a] = o[a]
        }
        if(extended) extended(this);
    }, 
    include: function(o){
        var included = o.included;
        for(var a in o){
            this.prototype[a] = o[a];
        }
        if(included) included(this);
    },
    populate: function(values){
        this.records = {};
        for( var i = 0, il = values.length; i < il; i++){
            var record = this.init(values[i]);
            record.newRecord = false;
            this.records[record.id] = record.dup();
        }
    },
    saveLocal: function(name){
        var result = [];
        for( var i in this.records){
            result.push(this.records[i]);
        }
        console.log(result);
        localStorage[name] = JSON.stringify(result);
    },
    loadLocal: function(name){
        var result = JSON.parse(localStorage[name]);
        this.populate(result);
    }
};

The Controller

Controller连接View和Model, 处理来自view的事件和输入,与model沟通并更新view.