使用MVVM架构处理页面的生成与更新的好处是,我们从DOM的桎梏中解放出来,重点转移到数据的处理。数据的组织与维护向来是设计模式的阵地,这让我们亲近设计模式,写出高可维护性的软件。以近日我在公司遇到的弹出层为例吧。
后台管理界面的特点是多表格多弹出层,这样才方便展现更多数据更多功能。我们项目有一个叫云储存的模块,可以看作是115网盘的微缩版。这模块里有许多弹出层,用于处理分区,文件夹,文件的增删改查。为了防止用户在一个操作不结束时干另一个操作引起混乱,都做成模态对话框。换言之,它们都有浅黑色的半透明遮罩层。由于可能一个弹出层外弹出另一个弹出层,比如删除时的确认面板,因此我又不能让所有弹出层共享一个外框,唯一能共享的是遮罩层。
再看我的代码,每个弹出层在VM中对应两个方法与一个属性,那个属性用于负责弹出层的显示与隐藏,其他两个方法用于改变这个属性。这是一种非常经典的做法。但遮罩层怎么办呢?它也应该跟弹出层一同显示隐藏。但如何让多个弹出层一起操作它?这时我使用了一个技巧——使用"短路或"!
<div class="mask" ms-visible="flagCreatePartition || flagNewFolder || flagDeleteFiles || flagUploadFiles"> </div> <div class="dialog" ms-if="flagCreatePartition" ms-include-src="'modules/apps/storage/createPartitionDialog.html'"> </div> <div class="dialog" ms-if="flagDeleteFiles" ms-include-src="'modules/apps/storage/deleteFilesDialog.html'"> </div> <div class="dialog" ms-if="flagNewFolder" ms-include-src="'modules/apps/storage/newFolderDialog.html'"> </div> <div class="dialog" ms-if="flagUploadFiles" ms-include-src="'modules/apps/storage/uploadFilesDialog.html'"> </div>
var model = avalon.define("appstorage", function(vm) { vm.flagCreatePartition = false vm.openCreatePartition = function() { model.flagCreatePartition = true } vm.closeCreatePartition = function(create) { if (create === true) { //创建新分区 } model.flagCreatePartition = false } vm.flagNewFolder = false vm.openNewFolder = function() { model.flagNewFolder = true } vm.closeNewFolder = function(create) { if (create === true) { //创建文件夹 } model.flagNewFolder = false } vm.flagDeleteFiles = false vm.openDeleteFiles = function() { vm.flagDeleteFiles = true } vm.closeDeleteFiles = function() { vm.flagDeleteFiles = false } vm.flagUploadFiles = false vm.openUploadFiles = function(create) { if (create === true) { //上传文件 } vm.flagUploadFiles = true } vm.closeUploadFiles = function() { vm.flagUploadFiles = false } //略 })
这是一个非常工整划一的结构。当我们想打开某人弹出层时,只要在对应的按钮上绑定ms-click="openUploadFiles", 关闭弹出层,我们在弹出层的关闭按钮上绑定ms-click="closeUploadFiles"。弹出层里存在两种按钮,一种是确认,是存在实际操作,一种是取消,单纯的关闭,我们可以通过传参区分它们。下面是某一个弹出层的内容,它是通过ms-include实现动态加载。
<div class='wrapper'> <div class='title'>{{i18ndeletefile}}<span class='close' ms-click='closeDeleteFiles'></span></div> <p>Are you sure to delete seleted files?</p> <span class="button" ms-click="closeDeleteFiles(true)" >{{i18nok}}</span> </div>
弹出层出现的入口:
<header> <span class="newFolder iconbutton" ms-click="openNewFolder">{{ i18nnewFolder }}</span> <span class="uploadFile iconbutton" ms-click="openUploadFiles">{{ i18nuploadFile }}</span> <span class="moveBtn fastener">{{ i18nmove }}</span> <span class="removeBtn fastener" ms-click="openDeleteFiles">{{ i18nremove }}</span> <span class="renameBtn fastener">{{ i18nrename }}</span> </header>
一切是乎很完美,但细看,它是违反“开闭原则”,每添加一个新弹出层都要在VM加三个东西,而且视图上也要遮罩层的ms-visible的值进行修改。这时我们就需要查找重复代码了,把变化的东西封装起来。我们发现所有弹出层与遮罩层都是受某些变量而改变(flagXXXX),它们是一个简单的布尔,如果把它们合并起来做成一个变量更好用些。
vm.flagDialog = "" vm.openDialog = function(name) { vm.flagDialog = name } vm.openDialog = function(command, execute) { if (execute) { vm[command]() } vm.flagDialog = "" } vm.createPartition = function() { } vm.newFolder = function() { } vm.deleteFiles = function() { } vm.uploadFiles = function() { }
在JAVA中,它要求每个类都一个控制状态的内部类。在JS这样动态的语言中,我们可以把这个内部类改成对象,这里更进一步,改成一个字符串。然后我们只要控制这个字符串,就能批量控制N个弹出层了。
打开这些弹出层的HTML代码变成:
<header> <span class="newFolder iconbutton" ms-click="openDialog('newFolder')">{{ i18nnewFolder }}</span> <span class="uploadFile iconbutton" ms-click="openDialog('uploadFiles')">{{ i18nuploadFile }}</span> <span class="moveBtn fastener">{{ i18nmove }}</span> <span class="removeBtn fastener" ms-click="openDialog('deleteFiles')">{{ i18nremove }}</span> <span class="renameBtn fastener">{{ i18nrename }}</span> </header>
而弹出层的代码就变成这样了:
<div class="mask" ms-visible="flagDialog"> </div> <div class="dialog" ms-if="flagDialog === 'createPartition'" ms-include-src="'modules/apps/storage/createPartitionDialog.html'"> </div> <div class="dialog" ms-if="flagDialog === 'deleteFiles' " ms-include-src="'modules/apps/storage/deleteFilesDialog.html'"> </div> <div class="dialog" ms-if="flagDialog === 'newFolder'" ms-include-src="'modules/apps/storage/newFolderDialog.html'"> </div> <div class="dialog" ms-if="flagDialog === 'uploadFiles'" ms-include-src="'modules/apps/storage/uploadFilesDialog.html'"> </div>
弹出层的内容大概成为这样:
<div class='wrapper'> <div class='title'>{{i18nnewFolder}}<span class='close' ms-click='closeDialog'></span></div> <input class="partition" ms-duplex="folderName"> <span class="button partitionbtn" ms-click="closeDialog('newFolder',true)" ms-class-blue="folderName.length" >{{i18nok}}</span> </div>
我们发现每一个弹出层的外框都很像,因此可以循环生成
<div ms-each-name="dialogNames"> <div class="dialog" ms-if="flagDialog === name" ms-include-src="'modules/apps/storage/{{name}}Dialog.html'"> </div> </div>
VM上添加一个dialogNames的数组,里面包含所有弹出层的名字就行了。这样我的HTML与JS就干净多。
最后附上一张设计模式的图片: