我们在前面3章节学习过的一些创建型,结构型和行为型设计模式可以组合在一起,成为架构型设计模式。
8.1 MVC模式
MVC(Model-View-Controller,模型-视图-控制器)模式可以把JavaScript应用程序的代码划分为3个独立的部分:模型(Model),负责把代码中的与底层数据构成相关的代码组合在一起,包括对数组对存储和读取;视图(View),负责将那些用于把模型中所保存的数据显示在屏幕上的代码组合一起,本质上就是对各DOM元素进行处理;控制器(Controller),负责处理系统中的业务逻辑,并在需要时更新模型和视图,它使得模型和视图不需要在彼此之间直接沟通,实现了它们之间的松解耦连接。关注点分离(Spearation Of Concerns,SOC)会使得代码更易于理解和处理,更易于测试,并可使工作于相同项目的多位开发者根据应用程序的模型,视图,控制器3个层次进行任务划分。
MVC架构模式实际上是将我们在第6章和第7章所学过的3种特定设计模式结合到一起使用,即观察者模式,组合模式和策略模式。当模型中的数据发生改变时,就会运用观察者模式来触发事件,传出更新后的数据,以供系统中的其他部分使用。同样,视图也会使用相同的观察模式来监听模型中数据的变化,并使用更新后的数据来更新用户界面。视图只能从模型直接读取数据,并不能修改数据,那是控制器的职责。视图还能包含子视图,用作处理较大型UI的可重用的部分。可运用组合模式,这样确保控制器就不需要清楚其逻辑所需影响的视图数量了。最后,控制器将使用策略模式来应用一个特定的视图至它本身,使得较大型系统中的多个视图可以共享相同的控制器逻辑,前提是这些视图都暴露一个相似的方法。我选择把此方法命名为render()。该方法接收来自于模型的数据,并将视图放置在当前页面,把视图与系统的其余部分广播发布的事件进行绑定设置,以使视图准备就绪。值得注意的是,在JavaScript应用程序中,模型通常时通过Ajax连接至一个作为数据库(用于保存系统所表示的数据)的后台服务器的。
代码清单8-1展示了我们时如何创建一个“类”来处理一个采用MVC模式的简单系统中的模型数据的表示的,此系统用于屏幕上email地址列表的管理。
代码清单8-1 模型
1 //模型表示系统的数据。在此系统中,我们希望能管理屏幕上的一个email地址列表,可以从显示的列表中把 2 //email地址进行添加或删除。因此,这里的模型就表示所保存的各个email地址本身。当添加或移除email地址 3 //时,模型会使用代码清单7-6中的观察者模式来广播此变化情况 4 // 5 //定于该模型为一个“类”,这样,如果有需要就可以创建出多个对象实例 6 7 function EmailModel(data) { 8 9 //创建一个存储数组,用于保存email地址。如果在实例化时没有提供email地址,则默认其为空数据 10 this.emailAddress = data || []; 11 } 12 13 EmailModel.prototype = { 14 15 //定义一个方法,用于添加一个新email地址至保存的email地址列表 16 add: function(email) { 17 18 //把新email地址添加至数组的开始位置 19 this.emailAddress.unshift(email); 20 21 //广播一个事件至该系统,指出已经添加了一个新的email地址,并向那些正在监听此事件的其他代码 22 //模块传入该email地址 23 observer.publish("model.email-address.added", email); 24 }, 25 26 //定义一个方法,用于从所保存的email地址列表中移除一个email地址 27 remove: function(email) { 28 var index = 0, 29 length = this.emailAddress.length; 30 31 //循环遍历所保存的email地址列表,找出所提供的email地址 32 for(; index < length; index++) { 33 if(this.emailAddress[index] === email) { 34 35 //一旦找到该email地址列表,找出所提供的email地址 36 this.emailAddress.splice(index, 1); 37 38 //向系统广播一个事件,指出有一个email地址已经从所保存的地址列表中移除, 39 //传出所移除的email地址 40 observer.publish("model.email-address.removed", email); 41 42 break; 43 } 44 } 45 }, 46 47 //定义一个方法,用于返回所保存的email地址的完整列表 48 getAll: function() { 49 return this.emailAddress; 50 } 51 };
代码清单8-2展示了如何为用户界面定义出视图代码。此界面由一个包含着2个子视图的面板组成。一个子视图包含着一个简单的录入表单,用于添加新email地址;而另一个子视图则显示所保存的email地址列表,每个email地址旁边都有一个Remove按钮,以便用户把某单独的email地址从列表移除。
代码清单8-2 视图
1 //我们将要创建的页面由2部分组成:一个文本录入表单域及其相关的按钮,用于添加新email地址至保存地址的列表; 2 //还有一个列表,用于显示所保存的email地址,每个地址旁边都配有一个Remove按钮,以便我们把该地址从所保存的地址列表中移除。 3 //我们还将定义一个一般性视图,此视图用作多个子视图的容器。我们将使用这种方法,以一个一般性视图把两个子视图连在一起, 4 //把此一般性视图提供给代码清单8-3中的控制器使用。有了代码清单8-1中的模型,我们就可以利用代码清单7-6中的观察者模式方法了 5 // 6 //定义一个视图,表示一个简单的表单,用于添加新email地址至所显示的列表中。我们把这定义为一个“类”。这样,就可以在用户界面中如我们所需, 7 //创建并显示多个此表单的实例 8 function EmailFormView() { 9 10 //创建若干新DOM元素来表示我们所要创建的表单(你可能是想保存页面中你直接需要的HTML标签,而不是在这里创建它们) 11 this.form = document.createElement("form"); 12 this.input = document.createElement("input"); 13 this.button = document.createElement("button"); 14 15 //确保我们所创建的<input type="text">表单域有适合的placeholder文本 16 this.input.setAttribute("type", "text"); 17 this.input.setAttribute("placeholder", "New email address"); 18 19 //确保我们创建一个<button type="submit">Add</button>标签 20 this.button.setAttribute("type", "submit"); 21 this.button.innerHTML = "Add"; 22 } 23 24 EmailFormView.prototype = { 25 26 //所有的视图都应该有一个render()方法。此方法在控制器“类”实例化之后的某一时刻由控制器的实例 27 //进行调用。通常,来自模型的数据还会传入该方法,但在这里这个特别的例子中,我们并不需要此数据 28 render: function() { 29 30 //把<input>表单域和<button>标签嵌套在<form>标签中 31 this.form.appendChild(this.input); 32 this.form.appendChild(this.button); 33 34 //添加<form>至当前HTML页面的底部 35 document.body.appendChild(this.form); 36 37 //把此视图所表示的各DOM元素的各种事件进行绑定设置 38 this.bindEvents(); 39 }, 40 41 //定义一个方法,用于把此视图与全系统事件进行绑定 42 bindEvents: function() { 43 var that = this; 44 45 //当此视图所表示的表单被提交时,发布一个全系统的事件,指出,一个新的email地址已经通过用户 46 //界面添加,并传出此新email地址的值 47 this.form.addEventListener("submit", function(evt) { 48 49 //屏蔽表单的默认提交动作行为(以防止页面出现刷新) 50 evt.preventDefault(); 51 52 //广播发布一个全系统事件,指出,一个新email地址已经通过由此视图所表示的表单添加。 53 //控制器将会监听该事件,并会代表视图,与模型进行联系,来把数据添加至保存的地址列表 54 observer.publish("view.email-view.add", that.input.value) 55 }, false); 56 57 //订阅由模型发出的一个事件,此事件告诉我们,一个新email地址已经被添加至系统中。当此发生时, 58 //清空<input>表单域的文本 59 observer.subscribe("modal.email-address.added", function() { 60 that.clearInputField(); 61 }); 62 }, 63 64 //定义一个方法,用于清空<input>表单域中的文本值。当一个email地址被添加至模型时,调用此方法 65 clearInputField: function() { 66 this.input.value = ""; 67 } 68 }; 69 70 //定义第2个视图,用于表示系统中的email地址列表。列表中的每一项旁边都会显示一个Remove按钮, 71 //以便将其所关联的地址从所保存的地址列表中移除 72 function EmailListView() { 73 74 //为<ul>、<li>、<span>和<button>标签创建DOM元素 75 this.list = document.createElement("ul"); 76 this.listItem = document.createElement("li"); 77 this.listItemText = document.createElement("span"); 78 this.listItemRemoveButton = document.createElement("button"); 79 80 //设置<button>标签的显示文本为"Remove" 81 this.listItemRemoveButton.innerHTML = "Remove"; 82 } 83 84 EmailListView.prototype = { 85 86 //定义此视图的render()方法,它使用所提供的模型数据并渲染一个列表,列表中各项目对应着模型中 87 //所保存的每一个email地址 88 render: function(modelData) { 89 var index = 0, 90 length = modelData.length, 91 email; 92 93 //循环遍历包含着所保存的email地址列表的模型数据的数组,并为每一数组项创建其相应的列表项目, 94 //把此列表项目添加至该列表中 95 for(; index < length; index++) { 96 email = modelData[index]; 97 98 this.list.appendChild(this.createListItem(email)); 99 } 100 101 //把该列表添加至当前页面底部 102 document.body.appendChild(this.list); 103 104 //为此视图绑定系统事件 105 this.bindEvents(); 106 }, 107 108 //定义一个方法。此方法会被传入一个email地址,它会创建并返回一个表示此email地址的经填充内容的列表项目<li>标签 109 createListItem: function(email) { 110 111 //比起每一次都从原初始状态创建新的DOM元素,克隆已存在的、经配置的DOM元素会更为高效 112 var listItem = this.listItem.cloneNode(false), 113 listItemText = this.listItemText.cloneNode(false), 114 listItemRemoveButton = this.listItemRemoveButton.cloneNode(true); 115 116 //为<li>元素设置一个data-email标签特性,以此<li>元素所表示的email地址进行填充。此举目的 117 //在于简化在稍后的removeEmail()方法中尝试查找与特定email地址相关的列表项目的实现 118 listItem.setAttribute("data-email", email); 119 listItemRemoveButton.setAttribute("data-email", email); 120 121 //在<span>元素中显示该email地址,并把此<span>元素以及Remove按钮添加至此列表项目元素 122 listItemText.innerHTML = email; 123 listItem.appendChild(listItemText).appendChild(listItemRemoveButton); 124 125 //返回此新列表项目给发起调用的函数 126 return listItem; 127 }, 128 129 //定义一个方法,用于为此视图绑定全系统事件 130 bindEvents: function() { 131 var that = this; 132 133 //在列表本身创建一个事件委托,来处理列表中<button>的点击 134 this.list.addEventListener("click", function(evt) { 135 if(evt.target && evt.target.tagName === "BUTTON") { 136 137 //当<button>被点击时,广播发布一个全系统事件,此事件将会被控制器收到。 138 //传入与该<button>相关的email地址给该事件 139 observer.publish("view.email-view.remove", evt.target.getAttribute("data-email")); 140 } 141 }, false); 142 143 //监听由模型发出的表示一个新email地址已经被添加的事件,并执行daaEmail()方法 144 observer.subscribe("model.email-address.added", function(email) { 145 that.addEmail(email); 146 }); 147 148 //监听由模型发出的表示一个email地址已经被移除的事件,并执行removeEmail()方法 149 observer.subscribe("model.email-address.removed", function(email) { 150 that.removeEmail(email); 151 }); 152 }, 153 154 //定义一个方法,当一个email地址被添加至模型时进行调用。此方法将把一个新列表项目插入至由此视图所表示的列表的顶部 155 addEmail: function(email) { 156 this.list.insertBefore(this.createListItem(email), this.list.firstChild); 157 }, 158 159 //定义一个方法,当一个email地址从模型中被移除时进行调用。此方法将把相关的列表项目从此视图所表示的列表中移除 160 removeEmail: function(email) { 161 var listItems = this.list.getElementsByTagName("li"), 162 index = 0, 163 length = listItems.length; 164 165 //循环遍历所有的列表项目,查找与所提供的email地址相关的列表项目。一旦查得到则将其移除 166 for(; index < length; index++) { 167 if(listItems[index].getAttribute("data-email") === email) { 168 this.list.removeChild(listItems[index]); 169 170 //一旦移除了该email地址,则停止该循环的执行 171 break; 172 } 173 } 174 } 175 }; 176 177 //定义一个一般性视图,它可保护若干子视图。当它的render()方法被调用时,它会依次调用它的子视图的render()方法, 178 //执行render()时传入在(模型)实例化时提供的任何模块数据 179 function EmailView(views) { 180 this.views = views || []; 181 } 182 183 EmailView.prototype = { 184 185 //所有的视图都需要一个render()方法。对于此一般性视图,它直接执行它的每一个子视图的render()方法 186 render: function(modelData) { 187 var index = 0, 188 length = this.views.length; 189 190 //循环遍历所有的子视图,执行它们的方法,传入在(模型)实例化时所提供的模型数据 191 for(; index < length; index++) { 192 this.views[index].render(modelData); 193 } 194 } 195 };
模型中所发生的变化可以通过使用观察者模式来立即反映在视图中。然而,视图中所发生的变化不会立即传至模型,变化将由控制器处理,如代码清单8-3所示.
代码清单8-3 控制器
1 //控制器把模型连接至视图,定义该系统的逻辑。只要模型提供了add(),remove()和getAll()方法(用于访问它的数据), 2 //视图提供了render()方法,我们就可以提供可选择性(可替换)的模型和视图,同时还能实现相似的系统行为。 3 //这其实就是应用了策略模式。我们还将使用来自于代码清单7-6中的观察者模式的方法 4 // 5 //定义一个“类”来表示控制器,用于连接我们的email地址管理系统的模型与视图。控制器在模型和视图之后进行实例化, 6 //模型和视图的对象会作为参数传入 7 function EmailController(model, view) { 8 9 //保存所提供的模型和视图对象 10 this.model = model; 11 this.view = view; 12 } 13 14 EmailController.prototype = { 15 16 //定义一个方法,用于初始化系统,它使用getAll()方法从模型获取数据,并通过执行视图的render()方法 17 //把数据传给相关联的视图 18 initialize: function() { 19 20 //从想管理的模型中获取email地址列表 21 var modelData = this.model.getAll(); 22 23 //把该数据传给相关联视图的render()方法 24 this.view.render(modelData); 25 26 //把控制器逻辑与相关的全系统事件进行绑定设置 27 this.bindEvents(); 28 }, 29 30 //定义一个方法,用于把控制器逻辑与相关的全系统事件进行绑定设置 31 bindEvents: function() { 32 var that = this; 33 34 //当视图指出,已经通过用户界面添加了一个新email地址,则调用addEmail()方法 35 observer.subscribe("view.email-view.add", function(email) { 36 that.addEmail(email); 37 }); 38 39 //当视图指出,已经通过用户界面移除了某个email地址,则调用removeEmail()方法 40 observer.subscribe("view.email-view.remove", function(email) { 41 that.removeEmail(email); 42 }); 43 }, 44 45 //定义一个方法,用于添加一个email地址至模型。当一个email地址已经通过视图的用户界面被添加时,调用此方法 46 addEmail: function(email) { 47 48 //直接调用模型上的add()方法,传入通过视图所添加的email地址。然后,该模型将广播发布一个事件, 49 //指出一个新email地址已经添加,而视图将会直接响应该事件,更新用户界面 50 this.model.add(email); 51 }, 52 53 removeEmail: function(email) { 54 55 //直接调用模型上的email()方法,传入一个经由视图提供的email地址。然后,该模型将广播发布一个事件, 56 //指出某个email地址已经被移除,而视图将会直接响应该事件,更新用户界面 57 this.model.remove(email); 58 } 59 } 60 //代码清单8-4展示了如果使用代码清单8-1~代码清单8-3所创建的“类”,根据MVC架构模式来构建一个简单页面,如图8-1所示。 61 62 //代码清单8-4 使用MVC模式 63 64 //创建emailModel“类”的一个实例,使用若干email地址填充它,以作为开始 65 var emailModel = new EmailModel( 66 [ 67 "wingzw@qq.com", 68 "wingxw@gmail.com" 69 ]), 70 71 //创建表单视图和email列表视图“类”的实例 72 emailFormView = new EmailFormView(), 73 emailListView = new EmailListView(), 74 75 //把表单视图和email列表视图作为一个单独的视图对象的2个子视图组合在一起 76 emailView = new EmailView([emailFormView, emailListView]), 77 78 //创建我们的email管理系统控制器的一个实例,把模型实例和视图实例传入,以供使用。请留意,控制器并 79 //不需要清楚该视图是包含一个单独的视图还是多个组合视图,就如此处所示。这是使用组合模式的一个例子 80 emailController = new EmailController(emailModel, emailView); 81 82 //最后,初始化控制器。这回从模型中获取数据,并把数据传给视图的render()方法,然后,该方法会把用户 83 //界面与各种全系统事件进行关联(在EmailFormView的render()中调用bindEvents()),使这个系统组合起来 84 emailController.initialize();
通过依次组合代码清单7-6(观察者模式)与代码清单8-1~代码清单8-4的代码,此MVC应用程序可以运行在任何简单HTML的上下文中,例如:
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="UTF-8"> 5 <title>MVC Example</title> 6 </head> 7 <body> 8 <script src="Listing7-6.js" ></script> 9 <script src="Listing8-1.js" ></script> 10 <script src="Listing8-2.js" ></script> 11 <script src="Listing8-3.js" ></script> 12 <script src="Listing8-4.js" ></script> 13 </body> 14 </html>
当用户使用<input>表单域录入一个新email地址并提交表单时,该新email地址出现在表单下发的列表顶部(消息在系统中的传输是从视图至控制器、至模型,然后,模型会广播发布一个事件,更新视图)。当用户点击任一email地址旁边的Remove按钮时,该email将从所显示的列表中移除。
对于较大型的应用程序,当其中包含着需要在用户界面进行显示、交互、更新的一组数据,但又不希望代码库变得过于复杂时,使用MVC模式是很有用的。此时代码将被划分为3个部分,分别负责数据的保存和操作、数据的显示,以及业务逻辑的处理和数据与显示之间的连接。
8.3 MVVM模式
MVVM(Model-View-View-Model,模型-视图-视图-模型)模式是最近才从MVC模式衍生出来的。就像MVP模式一样,它的目的也是要使模型与视图完全分离,彼此之间不再直接进行通信。然而,这里所使用的不是表示器,而是使用视图模型将两者进行分离。视图模型充当了相似的角色,但它包含了一些本来是在视图中的代码。这样,视图就可以使用一些更为简单的内容进行替换,并通过HTML5 data-标签特性来连接(或绑定)至视图模型。事实上,视图模型与视图之间的分离是非常清晰的,甚至可以使用静态HTML文件作为视图。通过直接使用包含在这些data-标签特性中的对视图模型的绑定,我们可以把该静态HTML文件作为模板来构建用户界面。
让我们回到图8-1所示的相同email列表管理应用程序,来应用MVVM模式进行实现。我们可以重用代码清单8-1中相同的模型代码,但我们将要建立一个新的视图,并使用视图模型代替之前的控制器或表示器。代码清单8-8展示了一个HTML页面。当中,在相关标签上使用了特定的HTML5 data-标签特性,用于向视图模型指明基于其内部业务逻辑所应对视图进行的管理。
1 <!DOCTYPE html> 2 <html> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>MVVM Example</title> 7 8 </head> 9 10 <body> 11 12 <!-- 13 现在,视图是一个简单的HTML文档。它可以在JavaScript中通过DOM元素进行创建,但这里不需要这么麻烦地来实现。 14 该视图通过在若干特定的HTML标签上的HTML5 data特性来连接至视图模型。这样,这些特定的HTML标签就会根据需要绑定至视图模型中的指定行为 15 --> 16 <!-- 17 <form>标签拥有一个特定的HTML5 data标签特性,指出,在提交时,它将会绑定至视图模型中的addEmail()方法 18 --> 19 <form data-submit="addEmail"> 20 <input type="text" placeholder="New email address" /> 21 <button type="submit">Add</button> 22 </form> 23 <!-- 24 <ul>列表拥有一个特定的HTML5 data标签特性,用于指出包含的标签将会循环遍历所保存的模型数据的 25 每一项。因此,如果模型包含3个email地址,则会产生并渲染3个<li>标签,将当前存在的那个模板性的 26 <li>标签替换掉 27 </ul> 28 --> 29 <ul data-loop> 30 <li> 31 <!-- 32 我们使用data-text标签特性来指出,当循环遍历所保存的模型数据时,视图模型将使用 33 当前循环轮次所表示的单独email地址来替换标签内容 34 --> 35 <span data-text></span> 36 37 <!-- 38 就如之前<form>标签上使用data-submit,这里使用data-click标签特性,也就是说, 39 当该按钮被点击时会执行视图模型上暴露的特定方法 40 --> 41 <button data-click="removeEmail">Remove</button> 42 </li> 43 </ul> 44 <!-- 45 稍后,我们将在此页面的底部位置添加<script>标签来加载观察者模式,模型,视图模型以及用于初始化的代码 46 --> 47 48 </body> 49 50 </html>
我选择在<ul>标签上使用data-loop标签特性,用以表明,<ul>标签里面所显示的<li>标签将会重复地渲染出来以表示模型中的每一个email地址。此循环中的特定标签上的data-text标签特性表示,此标签的内容将会被email地址本身所替换。对于data-submit标签特性,它以一个特定的方法名称作为它的值,表示一个submit时间会被关联至该元素,当该时间发生时,会根据所给定的方法名称执行视图模型中的相应方法。相似地,data-click标签特性的值表示视图模型中的一个方法名称。当元素被用户点击时,执行此方法。这些标签特性的名称是随意选择的。它们在HTML语法或JavaScript中并没有特别的含义,只不过是我个人在这里定义它们而已。留意在该文件的最后的注释内容,当中指出,我们将会在此代码清单的底部添加若干<script>标签来加载和初始化代码。这是我们在学习完视图模型和复制初始化的代码之后所要添加的一些内容。
代码清单8-9中的代码展示了一个特定的视图模型,用于绑定相关的数据核行为至代码清单8-8中的视图,以此来表示该应用程序。
代码清单8-9 视图模型
1 //为该email系统定义视图模型,它连接静态的视图至模型中所保存的数据。它为视图解析特性的HTML5 data标签特性, 2 //并将其作为指令来影响该系统的行为。假如我们在视图模型中预先写好了相关代码来处理视图中包含的特定data标签特性, 3 //则系统将如我们所预期的那样运作。视图模型的一般性通用程度更高,因而使得视图的变化程度更大,然而都不需要更新这里的代码。 4 //要使用到代码清单7-6中的观察者模式 5 6 function EmailViewModel(model, view) { 7 var that = this; 8 9 this.model = model; 10 this.view = view; 11 12 //定义2个方法。我们希望能通过HTML5 data标签特性来供视图选用这些方法 13 this.methods = { 14 15 //addEmail()方法将把一个所提供的email地址添加至模型。然后,该方法会广播发布一个事件, 16 //指出模型已经发出更新 17 addEmail: function(email) { 18 that.model.add(email); 19 }, 20 21 //removeEmail()方法将把一个所提供的email地址从模型移除。然后,这方法会广播发布一个事件 22 removeEmail: function(email) { 23 that.model.remove(email); 24 } 25 }; 26 } 27 28 //定义一个方法来初始化模型与视图之间的连接 29 EmailViewModel.prototype.initialize = function() { 30 31 //查找出<ul data-loop>元素。它将作为根元素用于循环变量模型中所保存的email地址, 32 //并使用DOM树中<ul data-loop>项下的<li>副本来显示每一项email地址 33 this.listElement = this.view.querySelectorAll("[data-loop]")[0]; 34 35 //保存<ul data-loop>元素项下的<li>标签 36 this.listItemElement = this.listElement.getElementsByTagName("li")[0]; 37 38 //把视图中的<form data-submit>连接至模型 39 this.bindForm(); 40 41 //把视图中的<ul data-loop>连接至模型 42 this.bindList(); 43 44 //把由模型广播发布的事件连接至视图 45 this.bindEvents(); 46 }; 47 48 //定义一个方法,来配置视图中的<form data-submit> 49 EmailViewModel.prototype.bindForm = function() { 50 var that = this, 51 52 //查找该<form data-submit>标签 53 form = this.view.querySelectorAll("[data-submit]")[0], 54 55 //获取保存在data-submit HTML5 标签特性的值中的方法名称 56 formSubmitMethod = form.getAttribute("data-submit"); 57 58 //创建一个事件监听器,用于在该<form>被提交时,根据给定的名称执行相应的方法 59 form.addEventListener("submit", function(evt) { 60 61 //确保<form>标签的默认行为不会运行的,从而使页面不会出现刷新 62 evt.preventDefault(); 63 64 //获取<form>中的<input>表单域所输入的email地址 65 var email = form.getElementsByTagName("input")[0].value; 66 67 //在视图模型的methods属性中找出所给定的方法并执行该方法,传入在<form>中输入的email地址 68 if(that.methods[formSubmitMethod] && typeof that.methods[formSubmitMethod] === "function") { 69 that.methods[formSubmitMethod](email); 70 } 71 }); 72 }; 73 74 //定义一个方法,用于由模型中所保存的数据构建出email地址列表。稍后,此方法会连接至由模型所 75 //发出的事件,这样,每当模型中的数据发生变化时,该列表就会重新创建 76 EmailViewModel.prototype.bindList = function() { 77 78 //从模型获取最新的数据 79 var data = this.model.getAll(), 80 index = 0, 81 length = data.length, 82 that = this; 83 84 //定义一个函数,用于基于一个给定的email地址来创建一个事件处理函数。当包含着data-click HTML5 data标签特性的<button> 85 //标签被点击时,执行data-click标签特性中所保存的方法名称,传入所提供的email地址 86 function makeClickFunction(email) { 87 return function(evt) { 88 89 //找出在HTML5 data-click标签特性中所保存的方法名称 90 var methodName = evt.target.getAttribute("data-click"); 91 92 //在视图模型的methods属性中找出该给定的方法,并执行该方法,传入所提供的email地址 93 if(that.methods[methodName] && typeof that.methods[methodName] === "function") { 94 that.methods[methodName](email); 95 } 96 }; 97 } 98 99 //清空<ul data-loop>元素的内容,移除它里面所有的以前创建的<li>元素 100 this.listElement.innerHTML = ""; 101 102 //循环遍历模型中所保存的所有email地址,基于我们之前保存的视图的原始状态的结构,为每一个email地址创建<li>标签 103 for(; index < length; index++) { 104 email = data[index]; 105 106 //以所保存的标签的一个克隆的方式创建一个新<li>标签 107 newListItem = this.listItemElement.cloneNode(true); 108 109 //找出<span data-text>元素,并使用email地址来填充它 110 newListItem.querySelectorAll("[data-text]")[0].innerHTML = email; 111 112 //找出<button data-click>元素,并执行makeClickFunction()函数来为循环的当前轮次的email地址 113 //提供事件处理函数(所生成按钮的html内容为<button data-click="removeEmail">Remove</button>) 114 newListItem.querySelectorAll("[data-click]")[0].addEventListener("click", makeClickFunction(email), false); 115 makeClickFunction(email, false); 116 117 //把填充好内容的<li>标签天机至视图中的<ul data-loop>元素 118 this.listElement.appendChild(newListItem); 119 } 120 }; 121 122 //定义一个方法,用于清空在<input>表单域中输入的email地址 123 EmailViewModel.prototype.clearInputField = function() { 124 var textField = this.view.querySelectorAll("input[type=text]")[0]; 125 126 textField.value = ""; 127 }; 128 129 //bindEvents()方法把由模型广播发布的事件连接至视图 130 EmailViewModel.prototype.bindEvents = function() { 131 var that = this; 132 133 //定义一个函数,当模型中的数据出现更新时执行此函数 134 function updateView() { 135 136 //从头开始,重新创建email地址列表 137 that.bindList(); 138 139 //清空<input>表单域中输入的任何文本 140 that.clearInputField(); 141 } 142 143 //把updateView()函数关联至2个由模型发出的事件 144 observer.subscribe("model.email-address.added", updateView); 145 observer.subscribe("model.email-address.removed", updateView); 146 };
最后,我们就可以结合代码清单8-1中的模型,代码清单8-8中的普通HTML视图,以及代码清单8-9中的视图模型,使用代码清单8-10中的代码来初始化该应用程序了。在代码清单8-8中视图的文件底部所指出的位置添加若干<script>标签引用,来加载代码清单7-6中的观察者模式及上述代码。例如:
1 <script src="Listing7-6.js" ></script> 2 <script src="Listing8-1.js" ></script> 3 <script src="Listing8-9.js" ></script> 4 <script src="Listing8-10.js" ></script>
因为我们把这些<script>引用添加进入了视图HTML页面,我们就可以直接使用document.body属性来获得对页面的DOM表示的引用,如代码清单8-10所示,它用于对应用程序进行初始化。
代码清单8-10 使用MVVM模式
1 var emailModel = new EmailModel( 2 [ 3 "wingzw@qq.com", 4 "wingxw@gmail.com" 5 ]), 6 emailView = document.body, 7 8 //就如在MVC模式中创建控制器一样,我们在这里创建表示器 9 emailViewModel = new EmailViewModel(emailModel, emailView); 10 11 emailViewModel.initialize();
可以很明确地看出,比起MVP模式和MVC模式,MVVM模式的优点是其视图能够以更简单的形式实现。应用程序中的视图越多就越能体现出其有用之处。视图与那些将其连接至模型的代码的更清晰的分离还意味着开发团队中不同的开发者可以相互独立地负责不同层的工作,然后在合适的阶段,在各人的代码出现互相冲突的风险较低情况下,将大家的工作整合在一起。MVVM是JavaScript开发者最为常用的架构型模式。
8.4 架构型模式框架
现在网上有许多预构架的MVC、MVP和MVVM JavaScript库,可以将其用在你自己的应用程序中,来实现我们在本章介绍过的各种架构型模式。这些库可以简化较大型代码库的开发,因为它们把数据管理代码从用户界面渲染的代码中分离了出来。
但是要当心,因为很多这样的框架都有着较大的体积,会导致拖慢应用程序加载速度的情况。当你的代码达到一定体积时应用某个框架至代码,你将会意识到,使用这些架构型模式中的某一种将会解决你正面临的开发问题。请记住,设计模式是开发工具箱的各种工具,必须谨慎使用,以符合代码中的特定需求。