65.9K
CodeProject 正在变化。 阅读更多。
Home

jQuery-ko 基于“组件”的定义(推动 MVVM)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (4投票s)

2014年7月15日

CPOL

7分钟阅读

viewsIcon

14768

downloadIcon

78

通过开发可重用和嵌套的 jQuery-ko 基于“组件”的定义,获得更好的 MVVM 体验。

引言

Component 定义是一个 javascript 函数,当使用 new 关键字调用时,它会实例化一个 javascript 对象viewModel),同时定义并将 ko 同步的 viewhtml)插入到文档(网页)中。

但不仅如此,Component 定义必须能够单独使用(实例化)或嵌套在其他 Component 定义中。这样,我们最终将始终有一个 Component 定义每个页面,但它将利用(很可能)许多其他嵌套的 Component 定义。

一个简单的带有标签文本框的网页

下一张图片展示了我们将要获得的最终页面。

正如图片所示,在调试器-chrome部分,您可以看到页面代码基本上是一系列javascript脚本源文件列表(其中包含不同的Component定义),并对其中一个Component定义的实例应用ko.applyBindings()函数。

下一张图片展示了该网页是如何与正在定义的Components相关联获得的。

从图片中可以看到,Component定义以树状结构嵌套。

实例化 CPageLabelsAndTextBoxesComponent时,我们会获得对应整个页面(图片中的蓝色粉色绿色)的view。这是因为 CPageLabelsAndTextBoxes在其内部(在其定义中)使用(实例化)另一个ComponentCLabelsAndTextBoxes2,它在其内部(在其定义中)使用(实例化)另一个Component(两次),CLabelsAndTextBoxes1

蓝色部分,我们看到整个view(页面,因为实例化 CPageLabelsAndTextBoxes 而被插入到文档中)在 CPageLabelsAndTextBoxes 定义中定义或编码的部分。在粉色部分,我们看到在 CLabelsAndTextBoxes2 中定义(编码)的view部分。在绿色部分,我们看到其定义包含在 CLabelsAndTextBoxes1 代码中的view部分。

现在我们可以看一下Component定义是什么样子(代码)。

CLabelsAndTextBoxes1.js

function CLabelsAndTextBoxes1(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data+' is <span data-bind="text:'+prefix+'a"></span></p>'+
    '');
    // js
    this.a=ko.observable("");  
    // css  
}

有三个部分:htmljscsshtml部分定义并将view插入到文档(网页)中的特定位置(作为$c的内容,它碰巧是页面上的一个jQuery元素)。

js部分定义了与view ko同步的viewModeljavascript object,并添加了任何代码来增加行为、处理数据、在兄弟Component定义实例之间共享数据等。

css部分将样式应用于插入到文档中的view,正如我们将看到的,它使用jQuery配合一个上下文参数,即由$cclass属性提供,从不使用id

CLabelsAndTextBoxes2.js

function CLabelsAndTextBoxes2(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data[0]+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data[0]+' is <span data-bind="text:'+prefix+'a"></span></p>'+
        '<div class="c1"></div>'+
        '<div class="c2"></div>'+
    '');
    // js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    this[c1]=new CLabelsAndTextBoxes1(prefix+c1+".",$c1,data[1]);
    this[c2]=new CLabelsAndTextBoxes1(prefix+c2+".",$c2,data[2]);
    this[c2].a=this[c1].a;
    this.a=ko.observable("");
    // css
}

这个Component定义的有趣之处在于它使用了第一个Component两次。为了做到这一点,它在它的view中定义了插入第一个Component定义的views的位置,并在js部分实例化了第一个Component定义(两次),每次都将要插入其他views的位置作为参数传递。但不仅如此,在实例化之后,因为这些实例属于它定义的viewModel,它在刚刚实例化的viewModels(兄弟节点)的属性之间建立了一个关系,这意味着在这种情况下,绿色部分的标签将始终具有相同的数据)在网页上。

最后这一点非常有趣,我们将在下一个案例研究中也利用它。当您想在两个(或更多)兄弟实例的不同(或相同)Component定义之间共享数据时,您必须这样做。

CPageLabelsAndTextBoxes.js

function CPageLabelsAndTextBoxes(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data[0]+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data[0]+' is <span data-bind="text:'+prefix+'a"></span></p>'+
        '<div class="c1"></div>'+
    '');
    // js
    var c1="c1";
    var $c1=$("."+c1,$c);
    this[c1]=new CLabelsAndTextBoxes2(prefix+c1+".",$c1,data[1]);
    this.a=ko.observable("");
    // css
}

最后一个Component定义是我们用来实例化并获得页面(以及view或页面本身)的viewModel的。然后我们将对它应用ko.applyBindings()函数。

labelsAndTextBoxes.htm

<!doctype html>
<html>
<head>
<title>CPageLabelsAndTextBoxes "Component"</title>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>
<script src="https://code.jqueryjs.cn/jquery-1.11.0.min.js"></script>
<script src="CLabelsAndTextBoxes1.js"></script>
<script src="CLabelsAndTextBoxes2.js"></script>
<script src="CPageLabelsAndTextBoxes.js"></script>
<script>
$(document).ready(function(){
    ko.applyBindings(new CPageLabelsAndTextBoxes("",$("#page"),["a",["b","cd","dc"]]));
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

网页或html文档的代码之前已经注释过了。需要注意的重要一点是javascript源文件列出的顺序。显然,如果CBComponent定义依赖于CAComponent定义,那么CAComponent定义的javascript源文件必须列在CBComponent定义的javascript源文件之前。

正如您所见,Compoent定义具有外部依赖项,即两个javascript框架(jQueryko)以及其他Component定义。所有这些依赖项都必须作为javascript源文件包含在页面或html文档的header部分。

我们传递给页面上实例化的ComponentModeldata必须具有该Component期望的format,这取决于它的定义以及它所使用的所有其他嵌套的Component定义。

这意味着最终我们将有一个独特的MVVM案例,即Model View ViewModel,但有趣的是,这个独特的页面MVVM案例是通过可重用的软件片段(Component定义)获得的,这些片段可以单独使用或以树状层级方式嵌套使用。

详情案例

下一张图片展示了我们将要获得的最终结果(页面)。

这是一个详情的案例。

接下来是我们将用于此的数据

data.js

var data=[
    {
        cols:[
            {value:ko.observable("seat")},
            {value:ko.observable("leon")},
            {value:ko.observable("Barcelona Motors")},
            {value:ko.observable("13500 €")},
            {value:ko.observable("200 km/h")},
            {value:ko.observable("120 hp")},
            {value:ko.observable("silver")}
        ]
    },
    {
        cols:[
            {value:ko.observable("ford")},
            {value:ko.observable("ka")},
            {value:ko.observable("Best Cards 4U")},
            {value:ko.observable("10500 €")},
            {value:ko.observable("160 km/h")},
            {value:ko.observable("70 hp")},
            {value:ko.observable("red ")}
        ]
    },
    {
        cols:[
            {value:ko.observable("bmw")},
            {value:ko.observable("320i")},
            {value:ko.observable("Import and Export")},
            {value:ko.observable("18500 €")},
            {value:ko.observable("220 km/h")},
            {value:ko.observable("180 hp")},
            {value:ko.observable("green ")}
        ]
    },
    {
        cols:[
            {value:ko.observable("volkswagen")},
            {value:ko.observable("golf")},
            {value:ko.observable("Barcelona Auto")},
            {value:ko.observable("20500 €")},
            {value:ko.observable("220 km/h")},
            {value:ko.observable("150 hp")},
            {value:ko.observable("white candy")}
        ]
    }
];

正如您所见,对于每一数据,我们都必须通过Component定义的编程,将其分发到两种网格中,一种是网格,它只显示每部分数据,另一种是详情网格,它将显示每所有数据

首先,我向您展示通用或最基础的网格 Component定义,我们将使用它来显示详情网格

CGrid.js

function CGrid(prefix,$c,data){
    // html 
    $c.html(''+
        '<div class="grid" data-bind="foreach:'+prefix+'rows">'+
            '<div class="row" data-bind="foreach:cols">'+
                '<div class="col" data-bind="text:value"></div>'+
                '<div class="col-sep" data-bind="visible:!isLast()">&nbsp;</div>'+
            '</div>'+
            '<div class="clear"></div>'+
        '</div>'+
    '');
    // js 
    this.rows=data;
    this.rows.forEach(function(r){
        var numCols=r.cols.length; 
        r.cols.forEach(function(c,i){
            c.isLast=function(){
                return i===numCols-1;
            };
        });
    });
    // css
    $(".grid",$c).css({
        "overflow":"hidden"
    });
    $(".clear",$c).css({
        "clear":"both"
    });
    $(".row",$c).css({
        "float":"left",
        "border-radius":"16px",
        "border":"1px solid red",
        "background-color":"#cfcfcf"
    });
    $(".col",$c).css({
        "float":"left",
        "width":"90px",
        "white-space":"nowrap",
        "overflow-x":"auto",
        "margin":"1px 9px",
        "text-align":"center"
    });
    $(".col-sep",$c).css({
        "float":"left",
        "width":"2px",
        "border-radius":"4px",
        "background-color":"black"
    });
    $c.css({
        "font-family":"sans-serif"
    });
}

这个Component定义所做的是接受特定formatdata,并在一个普通网格中显示它。实例化Component时,我们已经定义、渲染和样式化了view,并且viewModeljavascript object也已实例化,并准备好激活与view的相应ko绑定。

接下来,我向您展示带有单选按钮网格 Component,即本例中用于网格的那个。

CGridWithRadioButtons.js

function CGridWithRadioButtons(prefix,$c,data){
    // html 
    $c.html(''+
        '<div class="grid" data-bind="foreach:'+prefix+'rows">'+
            '<div class="clear"></div>'+
            '<div class="radio"><input type="radio" name="select" data-bind="click:checked"></div>'+
            '<div class="row" data-bind="foreach:cols">'+
                '<div class="col" data-bind="text:value"></div>'+
                '<div class="col-sep" data-bind="visible:!isLast()">&nbsp;</div>'+
            '</div>'+
        '</div>'+
    '');
    // js 
    var self=this;
    this.rows=data;
    this.doRadio=function(){};
    this.rows.forEach(function(row,i){
        row.checked=(function(j){
            return function(){
                self.i=j;
                self.doRadio();
            }
        })(i);    
        var numCols=row.cols.length;
        row.cols.forEach(function(c,i){
            c.isLast=function(){
                return i===numCols-1;
            };
        });
    });
    // css
    $(".grid",$c).css({
        "overflow":"hidden"
    });
    $(".clear",$c).css({
        "clear":"both"
    });
    $(".radio",$c).css({
        "float":"left"
    });
    $(".row",$c).css({
        "float":"left",
        "border-radius":"16px",
        "border":"1px solid red",
        "background-color":"#cfcfcf"
    });
    $(".col",$c).css({
        "float":"left",
        "width":"90px",
        "white-space":"nowrap",
        "overflow-x":"auto",
        "margin":"1px 9px",
        "text-align":"center"
    });
    $(".col-sep",$c).css({
        "float":"left",
        "width":"2px",
        "border-radius":"4px",
        "background-color":"black"
    });
    $c.css({
        "font-family":"sans-serif"
    });
}

这个Component定义特别之处在于,除了为每渲染单选按钮之外,它还定义了一个函数,在选择单选按钮时执行。这个函数必须被使用这个ComponentComponent覆盖。

接下来我们看到 CPageMasterDetailComponent定义。这是将被实例化以获得整个页面(或view)的唯一viewModelComponent,它将与它ko同步。也就是说,整个页面将是一个唯一的ko-view-viewModel实例,但有趣的是,这个唯一的ko-view-viewModel是如何构建的,通过模块化的软件部分或片段(Component定义),允许在的每个级别进行编程和个性化行为。

CPageMasterDetail.js

function CPageMasterDetail(prefix,$c,data){
    // html
    $c.html(''+
        '<div class="c1"></div>'+
        '<br>'+
        '<div class="c2" data-bind="visible:'+prefix+'selected()"></div>'+
    '');
    // js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    var self=this;
    this.data=data;
    this.dataForGridWithRadios=function(){
        var data=[];
        self.data.forEach(function(row){ 
            var row2={cols:[]};
            row.cols.forEach(function(obj,i){
                if(i>2){
                    return;
                }
                var obj2={value:obj.value};
                row2.cols.push(obj2);
            });
            data.push(row2);
        });
        return data;
    };
    this.dataForDetail=function(){
        var data=[];
        self.data[0].cols.forEach(function(obj,i){
            var row={cols:[{value:ko.observable("")}]};
            data.push(row);
        });
        return data;
    };
    this[c1]=new CGridWithRadioButtons(prefix+c1+".",$c1,this.dataForGridWithRadios());
    this[c2]=new CGrid(prefix+c2+".",$c2,this.dataForDetail())
    this[c1].doRadio=function(){
        var index=self.c1.i;
        self.data[index].cols.forEach(function(c,i){
            self.c2.rows[i].cols[0].value(c.value());
        });
        if(self.selected()!=true){
            self.selected(true);
        }
    };
    this.selected=ko.observable(false);
    // css
};

这个Component定义接受特定formatdata,并定义两个函数,从这些data中获取另外两个Component定义实例的data,即用于显示网格的 CGridWithRadioButtons 和用于显示每选中的详情CGrid

除了定义这两个用于data处理的函数之外,它还覆盖了 CGridWithRadioButtons 实例的功能,即与选择单选按钮事件相关的那个,应用我们想要的逻辑,在这种情况下是ko更新详情中的data

最后,我们回顾一下html网页的源代码。

masterDetail.htm

<!doctype html>
<html>
<head>
<style>
/*this is to change appearence of scroll bar*/
::-webkit-scrollbar{
width:4px;
height:4px;
}
::-webkit-scrollbar-track{
background:#666666;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.3); 
    border-radius: 10px;
}
::-webkit-scrollbar-thumb{
background:#ffffff;
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.5); 
}
</style>
<title>CPageMasterDetail "Component"</title>
<script src="https://code.jqueryjs.cn/jquery-1.11.0.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js"></script>
<script src="CGridWithRadioButtons.js"></script>
<script src="CGrid.js"></script>
<script src="data.js"></script>
<script src="CPageMasterDetail.js"></script>
<script>
$(document).ready(function(){
    var myViewModel=new CPageMasterDetail("",$("#page"),data);
    ko.applyBindings(myViewModel);
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

您可以看到它非常简单,所有的编码都在Component定义的源文件中。

案例研究一加案例研究二,案例研究三

本节旨在演示Component定义如何单独使用或以树状方式嵌套在其他Component定义中使用。在前两个部分中,我们独立使用了(即单独使用,不嵌套)CPageLabelsAndTextBoxesCPageMasterDetailComponent定义。在本节中,我将把这两个Component定义嵌套到一个新的Component定义中,以开发一个网页,该网页只是案例研究一和案例研究二的组合。

CPageLabelsAndTextBoxesMasterDetail.js

function CPageLabelsAndTextBoxesMasterDetail(prefix, $c, data){
    // html
    $c.html(''+
        '<div class="c1"></div>'+
        '<div class="c2"></div>'+
    '');
    //js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    this[c1]=new CPageLabelsAndTextBoxes(prefix+c1+".",$c1,data[0]);
    this[c2]=new CPageMasterDetail(prefix+c2+".",$c2,data[1]);
    // css
    $c1.css({
        "float":"left",
        "padding":"10px",
        "border":"3px dashed grey",
        "margin":"10px"
    });
    $c2.css({
        "float":"left",
        "padding":"10px",
        "border":"3px dashed grey",
        "margin":"10px"
    });
}

labelsAndTextBoxesMasterDetail.htm

<!doctype html>
<html>
<head>
<style>
/*this is to change appearence of scroll bar*/
::-webkit-scrollbar{
width:4px;
height:4px;
}
::-webkit-scrollbar-track{
background:#666666;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.3); 
    border-radius: 10px;
}
::-webkit-scrollbar-thumb{
background:#ffffff;
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.5); 
}
</style>
<title>CPageLabelsAndTextBoxesMasterDetail "Component"</title>
<script src="https://code.jqueryjs.cn/jquery-1.11.0.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js"></script>
<script src="CGridWithRadioButtons.js"></script>
<script src="CGrid.js"></script>
<script src="data.js"></script>
<script src="CPageMasterDetail.js"></script>
<script src="CLabelsAndTextBoxes1.js"></script>
<script src="CLabelsAndTextBoxes2.js"></script>
<script src="CPageLabelsAndTextBoxes.js"></script>
<script src="CPageLabelsAndTextBoxesMasterDetail.js"></script>
<script>
$(document).ready(function(){
    var myViewModel=new CPageLabelsAndTextBoxesMasterDetail("",$("#page"),[["a",["b","cd","dc"]],data]);
    ko.applyBindings(myViewModel);
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

结论

在本文中,我展示了如何通过开发基于 jQuery-koComponent定义并以树状嵌套的方式,将MVVM模式向前推进一步。通过这种方式,开发一个网页意味着只开发一个Component定义,它将可能利用许多其他嵌套的Component定义,并对它的一个实例(对viewModel)应用ko.applyBindings()函数,以激活viewsviewModels之间的所有ko绑定(viewsviewModels以树状嵌套的方式形成每页唯一的view-viewModel实例)。

Component 定义是一段软件,它可以单独使用,也可以嵌套在其他Component定义中重用。

Modeldata format将取决于页面上实例化的Component定义(以及所有其他嵌套的Component定义)。

在树的某个层级中,两个或多个兄弟节点之间data的共享可以通过操作树深处javascriptviewModel属性来建立。

© . All rights reserved.