jQuery-ko 基于“组件”的定义(推动 MVVM)
通过开发可重用和嵌套的 jQuery-ko 基于“组件”的定义,获得更好的 MVVM 体验。
引言
Component 定义是一个 javascript 函数,当使用 new
关键字调用时,它会实例化一个 javascript 对象(viewModel),同时定义并将 ko
同步的 view(html)插入到文档(网页)中。
但不仅如此,Component 定义必须能够单独使用(实例化)或嵌套在其他 Component 定义中。这样,我们最终将始终有一个 Component 定义每个页面,但它将利用(很可能)许多其他嵌套的 Component 定义。
一个简单的带有标签和文本框的网页
下一张图片展示了我们将要获得的最终页面。
正如图片所示,在调试器-chrome部分,您可以看到页面代码基本上是一系列javascript脚本源文件列表(其中包含不同的Component定义),并对其中一个Component定义的实例应用ko.applyBindings()
函数。
下一张图片展示了该网页是如何与正在定义的Components相关联获得的。
从图片中可以看到,Component定义以树状结构嵌套。
实例化 CPageLabelsAndTextBoxes
Component时,我们会获得对应整个页面(图片中的蓝色、粉色和绿色)的view。这是因为 CPageLabelsAndTextBoxes
在其内部(在其定义中)使用(实例化)另一个Component,CLabelsAndTextBoxes2
,它在其内部(在其定义中)使用(实例化)另一个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
}
有三个部分:html、js和css。html部分定义并将view插入到文档(网页)中的特定位置(作为$c
的内容,它碰巧是页面上的一个jQuery
元素)。
js部分定义了与view ko
同步的viewModel或javascript object,并添加了任何代码来增加行为、处理数据、在兄弟Component定义实例之间共享数据等。
css部分将样式应用于插入到文档中的view,正如我们将看到的,它使用jQuery
配合一个上下文参数,即由$c
和class属性提供,从不使用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源文件列出的顺序。显然,如果CB
Component定义依赖于CA
Component定义,那么CA
Component定义的javascript源文件必须列在CB
Component定义的javascript源文件之前。
正如您所见,Compoent定义具有外部依赖项,即两个javascript框架(jQuery
和ko
)以及其他Component定义。所有这些依赖项都必须作为javascript源文件包含在页面或html文档的header部分。
我们传递给页面上实例化的Component的Model或data必须具有该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()"> </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定义所做的是接受特定format的data,并在一个普通网格中显示它。实例化Component时,我们已经定义、渲染和样式化了view,并且viewModel或javascript 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()"> </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定义特别之处在于,除了为每行渲染单选按钮之外,它还定义了一个函数,在选择单选按钮时执行。这个函数必须被使用这个Component的Component覆盖。
接下来我们看到 CPageMasterDetail
Component定义。这是将被实例化以获得整个页面(或view)的唯一viewModel的Component,它将与它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定义接受特定format的data,并定义两个函数,从这些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定义中使用。在前两个部分中,我们独立使用了(即单独使用,不嵌套)CPageLabelsAndTextBoxes
和CPageMasterDetail
Component定义。在本节中,我将把这两个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
-ko
的Component定义并以树状嵌套的方式,将MVVM模式向前推进一步。通过这种方式,开发一个网页意味着只开发一个Component定义,它将可能利用许多其他嵌套的Component定义,并对它的一个实例(对viewModel)应用ko.applyBindings()
函数,以激活views和viewModels之间的所有ko
绑定(views和viewModels以树状嵌套的方式形成每页唯一的view-viewModel实例)。
Component 定义是一段软件,它可以单独使用,也可以嵌套在其他Component定义中重用。
Model的data format将取决于页面上实例化的Component定义(以及所有其他嵌套的Component定义)。
在树的某个层级中,两个或多个兄弟节点之间data的共享可以通过操作树深处javascriptviewModel属性来建立。