【IT168技术】在上一讲中,我们学习了如何通过jasmine的BDD驱动开发框架,一边编写单元测试用例一边按照MVC的架构编写了数据模型层,控制层和表现层的基本代码。在本文中,将继续介绍如下的几个方面内容。
1、如何在记事列表页面和记事编辑页面之间通过URL和参数传递信息。
2、如何在记事的编辑页面中加载一个记事内容进行编辑。
3、如何保存或者编辑一个记事内容。
我们预期完成本讲后,能设计出如下功能的界面:
步骤一、重构
下面我们开始重构之旅。之前我们的数据访问上下文在调用jStorage插件时,是直接引用了jQuery的类库的。我们现在进行一点修改,当访问数据模块dataContext时,将jQuery作为参数进行传递,如下:
Notes.dataContext = (function ($) {
// ……….
} (jQuery));
除此以外,我们将之前在程序中对在localstorage中保存的键值的名称去掉,原来是这样的:
var notesListStorageKey = "Notes.NotesList";
我们则可以通过init方法将这个值以参数的形式传递到dataContext中,如下所示:
var init = function (storageKey) {
notesListStorageKey = storageKey;
loadNotesFromLocalStorage();
};
当然,这个测试用例也很简单,我们稍微修改一下就可以了,注意修改的是
AppSpec.js文件,如下代码所示:
var notesListStorageKey = "Notes.NotesListTest";
// 省略其他部分
it("Returns dummy notes saved in local storage", function () {
Notes.testHelper.createDummyNotes();
//从localstorage中加载记事
Notes.dataContext.init(notesListStorageKey);
var notesList = Notes.dataContext.getNotesList();
expect(notesList.length > 0).toBeTruthy();
});
});
按日期对记事进行分组
为了能让用户更方便地在记事列表中寻找记事,我们将记事按日期进行分组,效果如下图:

代码如下:
var notesList = dataContext.getNotesList();
var view = $(notesListSelector);
view.empty();
if (notesList.length === 0) {
$(noNotesCachedMsg).appendTo(view);
} else {
var liArray = [],
notesCount = notesList.length,
note,
dateGroup,
noteDate,
i;
var ul = $("<ul id=\"notes-list\" data-role=\"listview\"></ul>").appendTo(view);
for (i = 0; i < notesCount; i += 1) {
note = notesList[i];
noteDate = (new Date(note.dateCreated)).toDateString();
if (dateGroup !== noteDate) {
liArray.push("<li data-role=\"list-divider\">" + noteDate + "</li>");
dateGroup = noteDate;
}
liArray.push("<li>"
+ "<a data-url=\"index.html#note-editor-page?noteId=" + note.id + "\" href=\"index.html#note-editor-page?noteId=" + note.id + "\">"
+ "<div class=\"list-item-title\">" + note.title + "</div>"
+ "<div class=\"list-item-narrative\">" + note.narrative + "</div>"
+ "</a>"
+ "</li>");
}
var listItems = liArray.join("");
$(listItems).appendTo(ul);
ul.listview();
}
};
如果学习过第2讲的教程,应该对上面循环产生记事的过程不陌生。但注意到的是,其中使用了变量noteDate保存了每个记事的日期,当判断出每个记事的所属日期不同的时候,则重新生成一个分隔项(其data-role为list-divider),而其内容为另外一个新的日期(详见
此外,请注意学习如何为每个记事生成编辑页面的链接,即:
这个note-editor-page就是我们要新增或者编辑记事的页面,下面开始学习如何设计
新增加记事页面设计
在新增加的记事页面中,注意我们是在index.html中增加相关的代码,并且注意其页面的data-role属性为page,代码如下:
<div data-role="header" data-position="fixed">
<a href="#notes-list-page" data-icon="back" data-rel="back">Cancel</a>
<h1>
Edit Note</h1>
<a id="save-note-button" href="" data-theme="b" data-icon="check">Save</a>
</div>
<div data-role="content">
<form action="" method="post" id="note-editor-form">
<label for="note-title-editor">
Title:</label>
<input type="text" name="note-title-editor" id="note-title-editor" value="" />
<label for="note-narrative-editor">
Narrative:</label>
<textarea name="note-narrative-editor" id="note-narrative-editor"></textarea>
</form>
</div>
<div data-role="footer" data-position="fixed" class="ui-bar">
<a id="delete-note-button" data-icon="delete" data-transition="slideup" data-rel="dialog">Delete</a>
</div>
</div>
其页面效果如下图:

其中的cancel按键用于返回记事列表用。
装载记事内容到记事页面
下面,我们看下如何装载记事到记事编辑器中。当用户在记事列表中点某个记事时,我们分为下面几个步骤进行设计:
在notesList数组中,根据在链接中的li元素中的id去找出对应的记事。
在编辑界面的标题和内容文本框中,将读取到的记事的标题和内容放置到相应的文本框中。
最后让记事编辑器页面处于激活状态
而如果用户是点新建记事按钮,则执行相类似的步骤,只不过不用先根据notedid在记事列表中找出对应的记事而已,而是直接获得用户提交的记事标题和内容再予以保存。下面先看下如何实现装载已有的记事内容到记事编辑页中。
首先,我们要在控制器的声明部分中,定义如下的页面变量:
var noteEditorPageId = "note-editor-page";
接下来,我们在onPageChange()方法中编写相关的事件处理代码,专门用于处理在记事列表页面跳转到记事编辑页面:
var toPageId = data.toPage.attr("id");
var fromPageId = null;
if (data.options.fromPage) {
fromPageId = data.options.fromPage.attr("id");
}
switch (toPageId) {
case notesListPageId:
resetCurrentNote();
renderNotesList();
break;
case noteEditorPageId:
if (fromPageId === notesListPageId) {
renderSelectedNote(data);
}
break;
}
};
下面解析下上面的代码,其中我们定义了fromPageId这个参数,今后我们简称为源页面,而toPageId则称之为目标页面。其中fromPageId的值会判断是否在记事编辑界面中加载记事内容:
renderSelectedNote(data);
}
这段代码判断了如果源页面来自记事列表页面,则通过renderSelectedNote方法读取具体的某个记事内容(这个方法稍后编写)。要注意的是,在应用启动后,pagechange 事件就发生了,但这个时候源页面为空,所以必须通过下面的代码,首先获得页面的id,如下:
fromPageId = data.options.fromPage.attr("id");
}
接下来,我们编写核心的从记事列表中装载记事的代码,即renderSelectedNote方法:
var u = $.mobile.path.parseUrl(data.options.fromPage.context.URL);
var re = "^#" + noteEditorPageId;
if (u.hash.search(re) !== -1) {
var queryStringObj = queryStringToObject(data.options.queryString);
var titleEditor = $(noteTitleEditorSel);
var narrativeEditor = $(noteNarrativeEditorSel);
var noteId = queryStringObj["noteId"];
if (typeof noteId !== "undefined") {
var notesList = dataContext.getNotesList();
var notesCount = notesList.length;
var note;
for (var i = 0; i < notesCount; i++) {
note = notesList[i];
if (noteId === note.id) {
titleEditor.val(note.title);
narrativeEditor.val(note.narrative);
currentNote = note;
}
}
} else {
//新建一个记事,设置标题文本框和记事文本框初始内容为空
titleEditor.val("");
narrativeEditor.val("");
}
titleEditor.focus();
}
};
在上面的代码中,首先判断来源页面是否为记事列表页面,其中使用了如下的通过正则表达式的方法判断:
var re = "^#" + noteEditorPageId;
if (u.hash.search(re) !== -1) {
… }
这里通过了jQuery Mobile的parseUrl方法(见http://jquerymobile.com/test/docs/api/methods.html)首先获得了来源的URL,存放到变量u中,并且通过正则表达式的判断方法进行判断。接着我们创建一个对象,用来存放相对应的来源URL,这通过一个助手类queryStringToObjec
t来实现,代码如下:
var queryStringObj = {};
var e;
var a = /\+/g;
var r = /([^&;=]+)=?([^&;]*)/g;
var d = function (s) { return decodeURIComponent(s.replace(a, " ")); };
e = r.exec(queryString);
while (e) {
queryStringObj[d(e[1])] = d(e[2]);
e = r.exec(queryString);
}
return queryStringObj;
};
然后在使用时,通过var queryStringObj = queryStringToObject(data.options.queryString);进行调用,获得的是上一个页面传进来的字符串参数对象,再通过var noteId = queryStringObj["noteId"];获得了要查看编辑的记事的notedId。再来分析以下这段代码:
var notesList = dataContext.getNotesList();
var notesCount = notesList.length;
var note;
for (var i = 0; i < notesCount; i++) {
note = notesList[i];
if (noteId === note.id) {
currentNote = note;
titleEditor.val(currentNote.title);
narrativeEditor.val(currentNote.narrative);
}
}
} else {
titleEditor.val("");
narrativeEditor.val("");
}
这里首先判断notedId是否已定义,如果不为空且已定义,则证明为加载编辑记事,通过dataContext数据模块加载记事列表,然后通过循环找出notedId为要查看记事id的记事
,注意这里使用currentNode变量保存了要查看的notes对象,这样就为后面增加和删除提供了方便,不用再次在noteList数组中再寻找。
运行后,当点记事列表中的某个记事时,则会出现具体的记事内容,如下图:

新增加记事内容
接下来我们开始设计新增记事的界面和代码,首先定义一个保存按钮的标识,如下:
var saveNoteButtonSel = "#save-note-button";
并在控制层中的init()方法中如下编写代码:
dataContext.init("Notes.NotesList");
var d = $(document);
d.bind("pagebeforechange", onPageBeforeChange);
d.bind("pagechange", onPageChange);
d.delegate(saveNoteButtonSel, "tap", onSaveNoteButtonTapped);
};
这里通过委托定义了用户在点新增按钮时,将触发onSaveNoteButtonTapped事件,代码如下:
var titleEditor = $(noteTitleEditorSel);
var narrativeEditor = $(noteNarrativeEditorSel);
var tempNote = dataContext.createBlankNote();
tempNote.title = titleEditor.val();
tempNote.narrative = narrativeEditor.val();
if (tempNote.isValid()) {
if (null !== currentNote) {
currentNote.title = tempNote.title;
currentNote.narrative = tempNote.narrative;
} else {
currentNote = tempNote;
}
dataContext.saveNote(currentNote);
returnToNotesListPage();
} else {
//告知用户保存失败,此处代码省略,留待下一讲完成 }
};
在上面的代码中,首先调用了NoteModel的isValid方法,用来判断要新增的记事是否已经完整添写了标题和内容,代码如下:
"use strict";
if (this.title && this.title.length > 0) {
return true;
}
return false;
};
接下来,我们通过观察currentNote变量的值,去判断是否在编辑记事还是新增记事。首先通过dataContext.createBlankNote()创建一个临时的记事对象tempNote,然后在判断输入合法后,判断是否是更新还是新增记事。如果currentNote变量不为空(证明是更新一个已存在的记事),则将currentNote的标题和内容简介设置为tempNote的标题和内容简介,否则是新增一个空的记事让currentNote设置为等于tempNote。再调用dataContext的saveNote方法,
这个方法还没定义,但我们先在AppSec文件中编写相关的测试用例。
//确认在测试前localstorage被清空
$.jStorage.deleteKey(notesListStorageKey);
var notesList = $.jStorage.get(notesListStorageKey);
expect(notesList).toBeNull();
// 创建一个记事
var dateCreated = new Date();
var id = dateCreated.getTime().toString();
var noteModel = new Notes.NoteModel({
id: id,
dateCreated: dateCreated,
title: ""
});
Notes.dataContext.init(notesListStorageKey);
Notes.dataContext.saveNote(noteModel);
notesList = $.jStorage.get(notesListStorageKey);
var expectedNote = notesList[0];
expect(expectedNote instanceof Notes.NoteModel).toBeTruthy();
// 清除notesListStorageKey)
$.jStorage.deleteKey(notesListStorageKey);
});
测试后结果如图所示:

编写代码如下:
var found = false;
var i;
for (i = 0; i < notesList.length; i += 1) {
if (notesList[i].id === noteModel.id) {
notesList[i] = noteModel;
found = true;
i = notesList.length;
}
}
if (!found) {
notesList.splice(0, 0, noteModel);
}
saveNotesToLocalStorage();
};
由于我们使用的是数组,因此跟传统的数据库保存有点不同,这里可能显得有点复杂。我们遍历整个notesList数组,如果是找到跟要编辑的记事对象,则直接设置found(找到标志)为true,并且覆盖原对象数组的内容,如果found为false,则找不到对象为新增,将新增的对象放置到数组头部。
再定义saveNotesToLocalStorage方法如下:
var saveNotesToLocalStorage = function () {
$.jStorage.set(notesListStorageKey, notesList);
};
将记事列表保存到local storage中去。
为了能在编辑或者新增记事后,能返回到记事列表页,特增加了
returnToNotesListPage方法,如下:
var returnToNotesListPage = function () {
$.mobile.changePage("#" + notesListPageId,
{ transition: "slide", reverse: true });
};
最后,为了能在新增或者删除记事后,都能将currentNote变量重新设置,需要增加方法resetCurrentNote,代码为:
var resetCurrentNote = function () {
currentNote = null;
}
这样,我们就完成了新增一个记事及编辑一个记事内容的工作,注意到我们在保存记事时,如果遇到异常情况,我们本讲没处理,将留待下一讲进行讲授。本讲的代码在http://miamicoder.com/wp-content/uploads/2011/12/Building-a-jQuery-Mobile-App-Part-3-Src.zip中可以下载。