【IT168技术】在本教程的上一篇(http://tech.it168.com/a2012/0111/1301/000001301499_all.shtml)中,为大家介绍了将要制作的记事本的总体架构,以及如何使用基于行为驱动模式开发的javascript框架jasmine进行简单的测试开发。在本文中,将指导大家使用jQuery Mobile框架进行实际的开发。
在本篇教程完结后,读者将会看到如下的一个制作好的记事本界面的样子,如下图:

下面我们开始对单元测试用例中编写修改的代码进行不断的重构,以达到我们的目的,这样的一个好处是能够很高效并且不编写多余的代码。
编写数据上下文模块
我们在第一篇教程中,使用jasmine编写了测试套件,第一个测试套件是如下所示:
it("Should have public interface to return notes list", function () {
expect(Notes.app.getNotesList).toBeDefined();
});
});
第二个测试套件则为:
it("Should return notes list", function () {
var notesList = Notes.app.getNotesList();
expect(notesList instanceof Array).toBeTruthy();
});
})
现在我们要对它们进行重构,尝试把它们整合为一个测试套件即可,如下代码所示:
it(“Exists in the app”, function () {
expect(Notes.dataContext).toBeDefined();
});
it(“Returns notes Array”, function () {
var notesList = Notes.dataContext.getNotesList();
expect(notesList instanceof Array).toBeTruthy();
});
});
在第一个测试用例it()方法中,我们将应用app的实例参数改为dataContext,也就是凡是跟数据访问有关系的方法,都会放在这个类的实例中。在教程的第一篇中,app变量是如下的代码:
var notesList = [];
function getNotesList() {
return notesList;
}
return {
getNotesList: getNotesList
}
})();
当我们更换名称后,代码如下:
var notesList = [];
function getNotesList() {
return notesList;
}
return {
getNotesList: getNotesList
}
})();
同时,我们将App.js更名为DataContext.js,在更改后,目录文件如下图所示:

当然,我们还要修改specrunner.html,将app.js修改为DataContext.js,如下:
<script type="text/javascript" src="app/DataContext.js"></script>
现在我们再次运行Jasmine,打开specrunner.html,然后运行,可以看到如下的结果:

这证明测试已经通过,简而言之,这里就是说我们将应用的模块名称改名了,叫做DataContext并且通过了单元测试。
创建空白的笔记
接下来,我们开始创建一个空白笔记的界面。界面的设计草图如下所示:

我们可以通过BDD测试描述这个行为,在测试用例中,添加如下代码:
var blankNote = Notes.dataContext.createBlankNote();
expect(blankNote.title.length === 0).toBeTruthy();
expect(blankNote.narrative.length === 0).toBeTruthy();
});
如果我们现在运行这个测试,测试结果肯定不能通过,因为没有将createBlankNote方法添加到DataContext的测试模块中,修改后的代码如下:
var notesList = [];
function createBlankNote() {
var dateCreated = new Date();
var id = new String(dateCreated.getTime()) + new String(getRandomInt(0, 100));
var noteModel = new Notes.NoteModel({
id: id,
dateCreated: dateCreated,
title: "",
narrative: ""
});
return noteModel;
}
function getNotesList() {
return notesList;
}
return {
createBlankNote: createBlankNote,
getNotesList: getNotesList
};
})();
在上面的代码中,会发现其中调用了getRandomInt这个助手类方法,它的作用是在创建新的记事时生成其id编号,由于我们还没编写getRandomint这个方法,所以运行测试时,可以看到是如下的出错界面:

接下来我们编写getRandomint的代码,如下:
return Math.floor(Math.random() * (max - min + 1)) + min;
}
再次运行测试后,依然发现有错误,这次的错误是说我们还没定义NoteModel类的实例,接下来我们在应用目录中创建NoteModel.js文件,代码如下:
this.id = id;
this.dateCreated = dateCreated;
this.title = title;
this.narrative = narrative;
}
所有的跟记事相关的操作都必须使用到这个类的实例,包括将记事缓存在移动设备中(用到的是将该类的实例的一个序列化)。
我们同样要在specrunner.html中,加上对NoteModel.js的引用,如下:
<script type="text/javascript" src="app/NoteModel.js"></script>
再次运行测试,可以看到这次测试结果运行正确了,如下图:

对Local Storage的操作
我们将使用HTML5新的特性localStorage API去保存记事内容。简单来说,HTML5的这一新特性localStorage是基于键值对操作的,其类型都为字符串。为了能将NoteModel类实例序列化,需要一个序列化机制将记事本数组转变为字符串,以方便保存在local storage中。同样,也要需要反序列化的机制将已序列化的记事重新转变为NoteModel实例,这是需要在用到NoteModel类时要用到。
为了方便操作,我们使用一个localStorage API的抽象层去实现,幸好有jStorage这个插件可以帮我们实现这个序列化和反序列化的操作。jStorage插件的地址如下可以下载:
http://www.jstorage.info/
jStorage插件允许我们去缓存数字、字符串和对象,在下载解包jStorage后,将相关的文件复制到如下图所示的目录中去,

同时,在dataContext模块中,我们将创建一个方法,从localStorage中获得保存过的记事对象,如下代码:
var storedNotes = $.jStorage.get(notesListStorageKey);
if (storedNotes !== null) {
notesList = storedNotes;
}
}
同时我们也定义了notesListStorageKey这个变量,如下:
var notesList = [];
var notesListStorageKey = "Notes.NotesList";
其中的loadNotesFromLocalStorage()方法,通过使用了jStorage插件从本地存储中获得了已缓存的记事列表,而插件本身完成了从字符转变为数组实例的工作,并且赋值给数组变量notesList。
当应用运行的时候,要把已经缓存的记事内容呈现给呈现给用户,这就要求应用在运行时必须调用loadNotesFromLocalStorage()这个方法。为了在一开始调用这些相关的方法,我们再编写一个init()助手方法,首先同样是在AppSpec.js中进行驱动测试的代码编写:
expect(Notes.dataContext.init).toBeDefined();
});
接下来在DataContext.js中,添加如下代码:
loadNotesFromLocalStorage();
}
.. // Other functions…
return {
init: init,
createBlankNote: createBlankNote,
getNotesList: getNotesList
};
运行上面的测试,可以看到如下图能通过所有的测试:

测试获得记事列表
接下来,我们看下测试如何将记事保存到localstorage中去,并且放到一个助手方法中去,命名为TestHelper.js文件,代码如下:
function createDummyNotes() {
var notesListStorageKey = "Notes.NotesList";
var notesCount = 10;
var notes = [];
for (var i = 0; i < notesCount; i++) {
var note = Notes.dataContext.createBlankNote();
note.title = "Title " + i;
note.narrative = "Narrative " + i;
notes.push(note);
}
$.jStorage.set(notesListStorageKey, notes);
};
return {
createDummyNotes: createDummyNotes
}
})();
在上面的代码中,将调用Notes.dataContext.createBlankNote()方法生成记事记录内容,并将其插入到notes数组中,最后通过jStorage插件,将notes数组保存到本地设备存储中去。
同时,在specrunner.html中,也要加入TestHelper.js的引用,如下:
<script type="text/javascript" src="app/DataContext.js"></script>
<script type="text/javascript" src="app/NoteModel.js"></script>
<!-- Test Helper -->
<script type="text/javascript" src="spec/TestHelper.js"></script>
<!-- Spec -->
<script type="text/javascript" src="spec/AppSpec.js"></script>
而在AppSpec.js中,我们可以增加一个测试断言如下:
Notes.testHelper.createDummyNotes();
Notes.dataContext.init();
var notesList = Notes.dataContext.getNotesList();
expect(notesList.length > 0).toBeTruthy();
});
在上面这个测试用例中,首先调用了testHelper助手类中的createDummyNotes创建了记事内容,并且调用了在init()方法,注意在init()方法中,也调用了loadNotesFromLocalStorage()方法,将记事从设备的localStorage中获取,再断言判断反序列化后的notesList是否为空。
创建页面
接下来,我们开始创建相关的页面,首先将设计首页index.html,其目录结构如下:

其中index.html代码如下:
<head>
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="../../lib/jqm/jquery.mobile-1.0.min.css" rel="stylesheet" type="text/css" />
<link href="css/app.css" rel="stylesheet" type="text/css" />
<script src="../../lib/jqm/jquery-1.6.4.min.js" type="text/javascript"></script>
<script src="../../lib/jstorage/jstorage.min.js" type="text/javascript"></script>
<script src="app/DataContext.js" type="text/javascript"></script>
<script src="app/Controller.js" type="text/javascript"></script>
<script src="app/NoteModel.js" type="text/javascript"></script>
<script src="spec/TestHelper.js" type="text/javascript"></script>
<!—--- 这里添加相关的启动事件----->
<script src="../../Lib/jqm/jquery.mobile-1.0.min.js" type="text/javascript"></script>
</head>
<body>
<div data-role="page" id="notes-list-page" data-title="My Notes">
<div data-role="header" data-position="fixed">
<h1>
My Notes</h1>
<a href="#note-editor-page" class="ui-btn-right" data-theme="b" data-icon="plus">New</a>
</div>
<div data-role="content" id="notes-list-content">
</div>
</div>
<</body>
</html>
在上面的代码中,首先引用了jQuery Mobile的类库和相关的之前我们编写的各种js,注意我们留空了启动事件的部分暂时没编写,这部分的代码会在稍后进行编写,凡是希望应用在启动的时候进行相关初始化操作的代码,都可以在这里编写。
接下来代码中,就是使用jQuery mobile的模版,设置了页面的布局,其中分别用到了jQuery Mobile内置的样式模版定义了header部分和主体部分,如下图:

接下来,我们开始补充页面初始化时加载的脚本,如下代码:
<link href="../../lib/jqm/jquery.mobile-1.0.min.css" rel="stylesheet" type="text/css" />
<link href="css/app.css" rel="stylesheet" type="text/css" />
<script src="../../lib/jqm/jquery-1.6.4.min.js" type="text/javascript"></script>
<scriptsrc="../../lib/jstorage/jstorage.min.js" type="text/javascript"></script>
<script src="app/DataContext.js" type="text/javascript"></script>
<script src="app/Controller.js" type="text/javascript"></script>
<script src="app/NoteModel.js" type="text/javascript"></script>
<script src="spec/TestHelper.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).bind("mobileinit",function(){
//Notes.testHelper.createDummyNotes();
Notes.controller.init();
});
</script>
<scriptsrc="../../Lib/jqm/jquery.mobile-1.0.min.js" type="text/javascript"></script>
要注意的是,我们在加载jQuery Mobile框架前,编写了mobileinit事件的代码,这个事件会在jQuery Mobile框架执行前进行相关的事件处理(具体可以参考:
http://jquerymobile.com/test/docs/api/globalconfig.html )。
在这个事件中会执行相关的函数调用,首先是调用TestHelper助手模块中的createDummyNotes()方法创建记事,接下来的的Notes.controller.init()是属于控制层的代码,我们稍后再编写。
通过以上过程的学习,你会发现我们的开发过程是遵守MVC架构的,其中的NoteModel类和DataContext数据模块可以被看作模型层,而jQuery Mobile则作为表现层的呈现,接下来开始编写控制层代码。
创建控制层
我们将控制层的文件命名为Control.js,先定义一个空的控制层文件如下:
Notes.controller = (function ($, dataContext) {
})(jQuery, Notes.dataContext);
现在,我们需要的是控制层将缓存在本地存储的记事列表罗列出来,在控制层中,将会调用init方法,这个init方法会在页面加载时,调用mobileinit方法会间接调用到。代码如下:
function init() {
dataContext.init();
var d = $(document);
d.bind("pagechange", onPageChange);
}
return {
init: init
}
})(jQuery, Notes.dataContext);
在控制层中的init()方法首先触发了dataContext中的init()方法,这很容易理解,因为我们必须获得数据访问的上下文,接下来可能大家会认为在控制层的init()方法中会列出在缓存中的记事列表,这个想法没错,但我们会通过在jQuery Mobile的pagechange事件中(相关详细介绍见http://jquerymobile.com/demos/1.0/docs/pages/page-scripting.html)进行绑定,这样有个好处,就是不单在应用启动的时候显示记事的列表,而且是当用户从编辑记事等其他页面返回主页时也能显示这些记事列表。下面我们往控制层中增加这个onPageChange()方法如下:
var notesListSelector = "#notes-list-content";
var noNotesCachedMsg = "</pre>
<div>No notes cached</div>
<pre>";
var notesListPageId = "notes-list-page";
var currentNote = null;
function init() {
dataContext.init();
var d = $(document);
d.bind("pagechange", onPageChange);
}
function onPageChange(event, data) {
var toPageId = data.toPage.attr("id");
switch (toPageId) {
case notesListPageId:
renderNotesList();
break;
}
}
return {
init: init
}
})(jQuery, Notes.dataContext);
处理的过程其实很简单,就是在onPageChange方法中,通过检查即将要跳转的页面的id属性(通过data.toPage方法),如果是等于“notes-list-page”,则调用renderNotesList()方法即可。
接下来,我们编写renderNotesList()方法,代码如下:
var notesList = dataContext.getNotesList();
var view = $(notesListSelector);
view.empty();
if (notesList.length === 0) {
$(noNotesCachedMsg).appendTo(view);
} else {
var notesCount = notesList.length;
var note;
var ul = $("<ul id=\"notes-list\" data-role=\"listview\"></ul>").appendTo(view);
for (var i = 0; i < notesCount; i++) {
note = notesList[i];
$("<li>"
+ "<a data-url=\"index.html#note-editor-page?noteId=" + note.id + "\" href=\"index.html#note-editor-page?noteId=" + note.id + "\">"
+ "<div>" + note.title + "</div>"
+ "<div class=\"list-item-narrative\">" + note.narrative + "</div>"
+ "</a>"
+ "</li>").appendTo(ul);
}
ul.listview();
}
};
在上面的代码中,首先通过data context的getNotesList()方法,获得了记事列表,并且将view区域(是个div)的内容清除,然后判断记事内容是否为空,如果为空的话则显示没有记事内容的提示信息,否则使用一个循环,将每个记事的内容增添到要显示的区域中,注意这里是通过字符串拼接的方式,构造出一个需要符合jQuery Mobile显示格式的字符串。最后还需要调用jQueryMobile的listview()方法才能生成一个完成的listview列表。
并且注意到,在列表中我们使用到了list-item-narrative的CSS样式文件,这需要我们定义如下:
{
color: #666666;
font-weight: normal;
}
并且将这个CSS文件保存在css目录下,命名为app.css,如下图:

最后,我们在手机浏览器上运行index.html,可以看到如下的界面:

小结
在这一讲中,我们继续使用单元测试驱动的方法,初步构建了模型层、控制层和表示层三层的需要的文件,读者可以从中体会到MVC的开发模式在移动开发中的应用,在下一讲中,我们将开始设计新增、编辑和删除记事文件。本讲的代码可以从
http://miamicoder.com/wp-content/uploads/2011/11/Building-a-jQuery-Mobile-App-Part-2-Src.zip 下载