代码改变世界

javascript 中的数据驱动页面模式

2014-10-13 16:33  sai.zhao  阅读(2689)  评论(4编辑  收藏  举报

前段时间一直在想前端MVC的意义。这个话题仁者见仁,但是MVC的使用方法给我提了一个管理数据的有意思的想法--数据管理和数据驱动页面。我们以前的思路一直是事件驱动页面,事件驱动页面合乎逻辑而且节约代码。但是往往代码组织结构非常松散,这个松散并不是大家所期望的松耦合,而是一种乱七八糟的感觉,后来在一次code中,我尝试了一下用数据来驱动页面,觉得效果也不错,逻辑也比较简单。下面简单分享一下我的思路。

  1. 我有一个电子商店,我需要一个购物车功能。
  2. 我希望购物车能在前端处理相关逻辑。而后台只是保存用户订单。

下面是订单保存的数据格式。

var orderList = {
    0:{
        'id':'12653',
        'productName':'Kindle fire',
        'price':790,
        'amount':2,
        'discount':0.75
    },
    1:{
        'id':'2653',
        'productName':'iPad',
        'price':2790,
        'amount':10,
        'discount':0.70
    },
    2:{
        'id':'653',
        'productName':'Mac',
        'price':7900,
        'amount':1,
        'discount':0.95
    },
    length:3,
    subscriberId:'254',
    totalPrice:0
}

首先我们使用一个数据管理器来维护用户的订单数据,我们把它设计为一个单体模式。

var shppingCar = function() {
    var orderList = {}
    this.add = function(obj){
        //添加一条购买数据
    }
    this.remove = function(obj){
        //删除一条购买数据
    }
    this.getTotilPrice = function(obj){
        //获取总价
    }
    this.update = function(obj){
        //更新购买数量
    }
    this.getOrder = function(){
        return orderList;
    }
}

这看起来数据结构清晰,代码组织似乎也不错。接下来涉及到我们DOM部分的操作了。

var order = new shppingCar();
orderList = order.getOrder();
var htmlManager = function(list){
    //用orderList数据渲染页面。
}
//第一次初始化数据
htmlManager();
//添加一条数据
orderList.add({});
orderList = order.getOrder();
htmlManager(orderList);
//删除一条数据
orderList.add(id);
orderList = order.getOrder();
htmlManager(orderList);
//更新一条数据
orderList.update(id);
orderList = order.getOrder();
htmlManager(orderList);

每做一次数据操作,我们都要更新一次数据。我们没有办法改变这个事实,因为事实就是数据改变,我们必然要修改页面。

或许你有更好的办法,那就是不用orderList渲染DOM,而是用一个回调函数来处理。那么代码变为

this.add = function(obj,fn){
    //添加一条购买数据
    if(fn){
        fn();
    }
}
你可以这样使用
orderList.add({},function(){
    //解析一次数据,生成一条DOM结构,插入
    //更改总价
});

这样也意味着你分别要为删除、添加、更新书写不同的回调函数,看起来也并不是一个非常好的办法。

回到前面的代码,我们只需要做一个小小的改变,就可以用数据的改变来驱动我们的页面更新,这也是一个伪观察者模式。其思想就是:数据更新了,我要重新渲染页面。

var shppingCar = function() {
    var orderList = {}

    //我们给shppingCar添加了一个私有方法,当数据改变时自动为我们来更新页面。
    var rander = function(){

    }
    this.add = function(obj){
        //添加一条购买数据
        rander();
    }
    this.remove = function(obj){
        //删除一条购买数据
        rander();
    }
    this.getTotilPrice = function(obj){
        //获取总价
        rander();
    }
    this.update = function(obj){
        //更新购买数量
        rander();
    }
    this.getOrder = function(){
        return orderList;
    }
}

这样我们使用的时候,就可以这样了

var orderList = new shppingCar();
//添加一条数据
orderList.add({});

我们只是把外部渲染函数改成了购物车对象的私有方法,然后在数据变动时调用这个私有方法,就可以省去了在外部每次更新数据都要再次调用一个更新页面的方法。虽然代码量减少的不是很多,但是将所有的内容封装起来外面调用看起来更是省心省力。

至于删除数据和更新数据,我们甚至不需要在外部定义,直接在渲染页面的时候把事件绑定到元素之后即可(下面的示例代码我实现了一个删除绑定,修改商品个数的功能大家有兴趣可以自己实现。)

我不知道这样做是否有意义,希望大家拍。

附测试代码如下。

var shppingCar = function() {
    //我们把数据设计为这样的格式
    var orderList = {
        length:0,
        subscriberId:'254',
        totalPrice:0
    }
    //一些工具方法
    //通过图书id获取当前是第几条数据
    var getItemById = function(id){
        for (var i = 0; i < orderList.length; i++) {
            if(orderList[i].id == id) {
                return i;
            }
        }
    }
    //重新整理数据成为标准格式
    var refreshData = function(){
        var  o = {},n = [];
        for (var key in orderList) {
            var k = Number(key);
            if(!isNaN(k)){
                n.push(orderList[key]);
            }else{
                o[key] = orderList[key];
            }
        }
        for (var i = 0; i < n.length; i++) {
            o[i] = n[i];
        }
        orderList = o;
    }
    //计算总价
    var updateTotilPrice = function() {
        var totalprice = 0;
        for (var i = 0; i < orderList.length; i++) {
            totalprice +=orderList[i].price*orderList[i].discount*orderList[i].amount;
        }
        return totalprice;
    };
    //渲染页面
    var htmlManager = function () {
        var items = "<ul>";
        for (var i=0;i<orderList.length;i++) {
            items += "<li><span>商品编号:"+orderList[i].id
                    +"</span> <span>商品名字:"+orderList[i].productName
                    +"</span> <span>商品价格:"+orderList[i].price
                    +"</span> <span>订购数量:"+orderList[i].amount
                    +"</span> <span>商品折扣:"+orderList[i].discount+"</span>"
                    +"<a data-id="+orderList[i].id+" href='###'>删除</a></li>"
        }
        items += "</ul>";
        items+="商品总价格为"+ orderList.totalPrice +"元";
        document.getElementsByTagName("body")[0].innerHTML = (items);

        //绑定删除事件
        var delBtns = document.getElementsByTagName("a");
        for (var j = 0; j < delBtns.length; j++) {
            (function(k){
                delBtns[k].onclick = function(){
                    remove(delBtns[k].getAttribute('data-id'));
                    return false;
                }
            })(j)
        }
        //绑定修改个数事件
    };
    //删除一条数据
    var remove = function(id){
        var item = getItemById(id);
        delete orderList[item];
        orderList.length-=1;
        refreshData();
        orderList.totalPrice = updateTotilPrice();
        htmlManager();
    }
    //更新商品个数
    var update = function(id,amount){
        //TODO:更新购买数量
        orderList.totalPrice = updateTotilPrice();
        htmlManager();
    }
    //对外俩个接口方法,一个可以添加一条购买数据,一个为获取当前购物车的所有数据
    this.add = function(obj){
        //TODO:验证传入的数据是否合法
        //TODO:此处判断是否已经存在该商品,如果存在,则调用updata方法。
        orderList[orderList.length] = obj;
        if(orderList[orderList.length]){
            orderList.length +=1;
        }
        orderList.totalPrice = updateTotilPrice();
        htmlManager();
    }

    this.getOrder = function() {
        return orderList;
    };
};
//使用方法:
var orderList = new shppingCar();
orderList.add({
    'id':'6530',
    'productName':'Mac mini-0',
    'price':4900,
    'amount':4,
    'discount':0.90
})
orderList.add({
    'id':'65301',
    'productName':'Mac mini-1',
    'price':5000,
    'amount':4,
    'discount':0.90
})

document.onclick = function() {
    console.log(orderList.getOrder());
};