技术开发 频道

jQuery Mobile开发记事本应用第三季

  【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文件,如下代码所示:

describe("Data Context tests", function () {
    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 renderNotesList = function () {
    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),而其内容为另外一个新的日期(详见

liArray.push("<li data-role=\"list-divider\">" + noteDate + "</li>");)。

此外,请注意学习如何为每个记事生成编辑页面的链接,即:

   <a data-url=\"index.html#note-editor-page?noteId=" + note.id + "\" href=\"index.html#note-editor-page?noteId=" + note.id + "\">"

  这个note-editor-page就是我们要新增或者编辑记事的页面,下面开始学习如何设计

  新增加记事页面设计

  在新增加的记事页面中,注意我们是在index.html中增加相关的代码,并且注意其页面的data-role属性为page,代码如下:

  <div data-role="page" id="note-editor-page" data-title="Edit Note">
    
<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 onPageChange = function (event, data) {
    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的值会判断是否在记事编辑界面中加载记事内容:

  if (fromPageId === notesListPageId) {

  renderSelectedNote(data);

  }

  这段代码判断了如果源页面来自记事列表页面,则通过renderSelectedNote方法读取具体的某个记事内容(这个方法稍后编写)。要注意的是,在应用启动后,pagechange 事件就发生了,但这个时候源页面为空,所以必须通过下面的代码,首先获得页面的id,如下:

  if (data.options.fromPage) {

  fromPageId
= data.options.fromPage.attr("id");

  }

  接下来,我们编写核心的从记事列表中装载记事的代码,即renderSelectedNote方法:

var renderSelectedNote = function (data) {
    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 u = $.mobile.path.parseUrl(data.options.fromPage.context.URL);
    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 queryStringToObject = function (queryString) {
    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。再来分析以下这段代码:

  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) {
            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()方法中如下编写代码:

var init = function () {
    dataContext.init(
"Notes.NotesList");
    var d
= $(document);
    d.bind(
"pagebeforechange", onPageBeforeChange);
    d.bind(
"pagechange", onPageChange);
    d.delegate(saveNoteButtonSel,
"tap", onSaveNoteButtonTapped);
};

  这里通过委托定义了用户在点新增按钮时,将触发onSaveNoteButtonTapped事件,代码如下:

     var onSaveNoteButtonTapped = function () {
       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方法,用来判断要新增的记事是否已经完整添写了标题和内容,代码如下:

Notes.NoteModel.prototype.isValid = function () {
    
"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文件中编写相关的测试用例。

   it("Saves a note to local storage", function () {
    
//确认在测试前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 saveNote = function (noteModel) {
    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中可以下载。

0
相关文章