65.9K
CodeProject 正在变化。 阅读更多。
Home

TaskTracker 移动端离线 Web 应用,使用 HTML5/jQuery/KnockoutJS

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (34投票s)

2012年2月26日

CPOL

10分钟阅读

viewsIcon

113554

downloadIcon

3537

Task Tracker 是一个使用 HTML5、CSS3 和 JavaScript 开发的离线 Web 应用程序。

TaskTracker

引言

您在火车上通勤时,正在 iPhone 上观看一个有趣的 YouTube 视频,这时网络中断了,您无法继续观看视频。这让您感到沮丧。这就是典型的基于 Web 的应用程序的局限性,因为它们需要互联网连接才能无缝地提供应用程序。但是,有了 HTML5,现在可以开发离线 Web 应用程序,在互联网连接丢失的情况下仍能无缝运行。这是因为 HTML5 具有应用程序缓存和 Web 存储功能。通过 Web 存储,我们现在可以在客户端存储多达 5 MB 的数据。这是一个相当不错的大小,可以在互联网丢失的情况下缓存客户端数据,用户将能够继续工作,并在互联网连接恢复后与服务器同步。本文的目的是演示这些功能。TaskTracker 是一个针对移动平台开发的离线 Web 应用程序,可以帮助用户跟踪他们待办的任务。此外,他们还可以维护和管理他们的联系人。我还将解释如何利用 HTML5 和 jQuery/jQuery-UI/jQuery-Validation 和 Knockoutjs 等开源框架的强大功能来构建此应用程序。

本应用程序使用了 HTML5 的应用程序缓存和 Web 存储功能。

通过 jQuery,我们可以轻松地操作 DOM 元素,例如在用户界面中动态地附加事件、添加/删除或切换 CSS 类,并且代码简洁易懂,最重要的是它兼容所有浏览器!因此,我们可以专注于应用程序的业务逻辑,而不必担心这些问题。

jQuery-UI 插件能够通过几行代码将标准的 HTML 标记元素转换为优雅的 GUI 小部件。此外,它还提供了各种 主题,因此我们不必过多担心构建自己的 CSS 样式。

jQuery-Validation 插件提供了所有功能来处理客户端验证。

最后,Knockoutjs 是一个出色的框架,它使我们能够使用 Model-View-ViewModel (MVVM) 设计模式来构建应用程序。这是一种非常强大的设计模式,在 WPF 和 Silverlight 开发的应用程序中非常流行。Knockoutjs 提供了一个强大的数据绑定框架,用于 JavaScript 编程。有一个优秀的 教程 可以帮助您熟悉这个框架。

背景

几个月前,我观看了一个关于 HTML5 功能的有趣 视频,对其强大的功能印象深刻。HTML5 的功能,结合 CSS3 和 JavaScript 的强大功能,可以用于构建移动平台的 Web 应用程序,然后可以使用 PhoneGap 等开源框架将其转换为不同移动平台的原生移动应用程序。这样我们就可以编写一次,部署到多个平台。我决定开发一个 TaskTracker 应用程序来探索这些功能。我将在下面的章节中解释此应用程序中使用的不同框架的各种功能。您可以在此处查看正在运行的 演示

设计概述

该应用程序采用流行的 MVVM 设计模式构建。MVVM 是一种与旧的 MVC 设计模式非常相似的设计模式,并且解决了关注点分离的问题。像 MVC 一样,它有一个模型(Model)、视图(View),但控制器(Controller)的角色由 ViewModel 承担。ViewModel 负责处理用户交互,并与模型交互以更新视图。它除了模型之外还可以拥有 View 特有的附加属性。此外,在由多个视图组成的复杂 Web 应用程序中,每个视图都可以有一个相应的 ViewModel。在我们的应用程序中,只有一个视图和一个 ViewModel。应用程序的关键文件是 *main.html*、*taskcontroller.js* 和 *storageController.js*。应用程序的类图如图 1 所示。

Class diagram of Task Tracker

从图 1 可以看出,`TaskControllerUI` 类是一个概念类,代表应用程序的用户界面,并以纯 HTML 实现。`TaskViewModel` 是一个关键类,负责处理所有用户交互并更新视图,以及与模型进行交互。`Customer` 和 `Task` 是两个实体类,它们充当此应用程序的模型。`StorageController` 是一个包装类,负责从 WebStorage 保存和检索数据。

由于 JavaScript 不是像 C++、C# 或 Java 这样的传统面向对象编程语言,它没有 `Class` 构造。所以所有类都以不同的方式实现。我将首先解释业务层实现,然后是数据层,最后是 UI。

业务层

业务层包括两个关键实体:`Task` 和 `Customer`。另一个重要的类是 `TaskViewModel`。下面的部分显示了 `Task` 和 `Customer` 的实现细节。

// Declare Task class
 function Task() {
    var self = this;
    self.name= ko.observable("");
    self.id = 1,
        self.description = ko.observable(""),
        self.startDate = ko.observable($.datepicker.formatDate('yy-mm-dd',new Date())),
        self.visitDate = ko.observable($.datepicker.formatDate('yy-mm-dd',new Date())),
        self.visitTime = ko.observable("9:00 am"),
        self.price = ko.observable(0),
        self.status = ko.observable("Pending"),
        self.custid = ko.observable();
    self.notes = ko.observable("");

}
// Declare Customer Class
function Customer() {
    var self = this;
    self.custid = ko.observable(101);
    self.firstname = ko.observable("");
    self.lastname = ko.observable("");
    self.address1 = ko.observable("");
    self.address2 = ko.observable("");
    self.city = ko.observable("");
    self.country = ko.observable("");
    self.zip = ko.observable("");
    self.phone = ko.observable("");
    self.mobile = ko.observable("");
    self.email = ko.observable("");


}

// example of creating new instance of these classes
var task = new Task();
var cust = new Customer();
// Important point to remember knockout observables are methods.
var fname= cust.firstname(); // see we access the property as function.
// set the property.
cust.firstname('Joe'); // setting the observable property.

如果您查看上面的代码,您可能会注意到这两个类都只包含属性,因为它们是实体类,并且不暴露任何方法。但您也可能注意到这些属性是用 Knockout 构造初始化的。这些属性被声明为 `observables`。这将使这些属性能够绑定到 UI 并提供双向绑定。因此,在 UI 中对这些属性所做的任何更改都将自动更新这些属性。或者,如果您以编程方式更改它们的值,它将反映这些更改到 UI 中。需要记住的一点是,Knockout observables 是函数,您不能将它们作为普通属性访问或初始化。请参见上面显示设置和访问 Knockout observable 属性的示例代码。

现在让我们看看 `TaskViewModel` 的实现细节。

function TaskViewModel() {
    var self =this;
    self.lastID = 1000;
    self.custID = 100;
    self.taskCustomer = ko.observable();
    self.currentCustomer = ko.observable(new Customer());
    self.customerSelected = -1;
    self.customers = ko.observable([]);
    self.normalCustomers = [];
    self.selected = ko.observable();
    self.taskMenu = ko.observable("Add Task");
    self.customerMenu = ko.observable("Add Customer");
    self.editFlag = false;
    self.editCustFlag = false;
    self.tasks = ko.observableArray([]);
    self.normalTasks = [];
    self.currentTask = ko.observable(new Task());
    self.taskOptions = ['Pending', 'In Progress', 'Complete'],
    self.filterCustomer = ko.observable("");
   
    self.filterDate = ko.observable("");
    //Filter tasks by visit date
    self.ftasks = ko.dependentObservable(function () {
        var filter = self.filterDate();
        if (!filter) {
            return self.tasks();

        }
        return ko.utils.arrayFilter(self.tasks(), function (item) {
            if ($.isFunction(item.visitDate)) {
                var val1 = item.visitDate();
                var val2 = self.filterDate();
                if (item.status() === 'Complete')
                    return false;
                return (val1 === val2);
            } else {
                return self.tasks();
            }
        });

    }, self);

    // Methods for managing the tasks.

    // Init tasks
    self.init = function () {

        var sLastID = tsk_ns.storage.getItem("lastTaskID");
        if (sLastID !== null) {
            self.lastID = parseInt(sLastID);
        }
        // Read tasks.
        var sTasks = tsk_ns.storage.getItem("tasks");
        //  alert(sTasks);
        if (sTasks !== null) {

            self.normalTasks = JSON.parse(sTasks);
            self.updateTaskArray();
            self.currentTask(new Task());
            // this.tasks(ko.observableArray(ntasks));
            //this.tasks(ntasks);

        } else {

            alert("No Tasks in storage");
        }

    },
    // Add task
    self.addTask = function () {
        $('#taskForm').validate().form();
        var isvalid = $('#taskForm').valid();
        if (isvalid) {
            if (!self.editFlag) {
                self.lastID += 1;
                self.currentTask().id = self.lastID;
                self.tasks.push(self.currentTask);
                tsk_ns.storage.saveItem("lastTaskID", self.lastID.toString());

            } else {
                self.currentTask().custid = self.taskCustomer().custid;

                //self.selected(self.currentTask);
            }
            // Save last task ID
            //self.tasks(self.normalTasks);

            // save tasks
            var mTasks = ko.toJSON(self.tasks);
            console.log(mTasks);
            tsk_ns.storage.saveItem("tasks", mTasks);
            self.normalTasks = JSON.parse(mTasks);
            self.updateTaskArray();

            // current taks
            self.taskMenu("Add Task");
            self.currentTask(new Task());
            self.editFlag = false;
            console.log("No of tasks are :" + self.tasks().length);
        }
    };

    // Remove tasks.
    self.removeTask = function (itask) {

        self.tasks.remove(itask);
        var mTasks = ko.toJSON(self.tasks);
        self.normalTasks = JSON.parse(mTasks);
        tsk_ns.storage.saveItem("tasks", mTasks);
        self.updateTaskArray();
    };

    // Edit task
    self.editTask = function (itask) {
        self.selected(itask);
        //    var index=ko.utils.arrayIndexOf(self.tasks, self.selected);
        // Get Current customer

        var curCust = self.getCurrentCustomer(itask.custid);
        if (curCust !== null) {
            self.taskCustomer(curCust);
        }

        self.currentTask(itask);
        self.taskMenu("Edit Task");
        $("#tabs").tabs("option", "selected", 2);
        self.editFlag = true;
        //$("#taskNew").button("option", "disabled", true);
        //$("#taskUpdate").button("option", "disabled", false);

    };

    self.addNotes = function (itask) {

        var curCust = self.getCurrentCustomer(itask.custid);
        if (curCust !== null) {
            self.taskCustomer(curCust);
        }

        self.currentTask(itask);
        $("#dlgNotes").dialog("open");
        self.editFlag = true;

    };

    // Update task
    self.updateTask = function () {
        console.log("Select task index is " + this.selected);
        console.log(self.taskCustomer().custid);
        // self.currentTask().custid = self.taskCustomer.custid;
        var normalTsk = ko.toJSON(self.currentTask());
        // self.currentTask().visitDate(normalTsk.visitDate);
        var tskObject = JSON.parse(normalTsk);
       // var stdate = $("#taskStartDate").datepicker(
        tskObject.custid = self.taskCustomer().custid;
        console.log(tskObject.custid);
        if (this.selected > -1)
            this.normalTasks[this.selected] = tskObject;
    };

    self.getCurrentCustomer = function (cid) {
        for (var i = 0; i < self.normalCustomers.length; i++) {
            var c1 = self.normalCustomers[i];
            if (c1.custid === cid)
                return c1;
        }
        return null;

    }

    // copy tasks.
    self.updateTaskArray = function () {

        self.tasks.removeAll();
        // self.tasks(self.normalTasks);
        for (var i = 0; i < self.normalTasks.length; i++) {
            var ctask = self.normalTasks[i];
            // var ctask = JSON.parse(self.normalTasks[i]);

            var t = new Task();
            t.id = ctask.id;
            t.name(ctask.name);
            t.description(ctask.description);
            t.startDate(ctask.startDate);
            t.visitDate(ctask.visitDate);
            t.visitTime(ctask.visitTime);
            t.price(ctask.price);
            t.status(ctask.status);
            t.custid = ctask.custid;
            t.notes(ctask.notes);
            self.tasks.push(t);
            // console.log("Task name is " + ctask.name);
        };
        console.log("No of tasks are :" + self.tasks().length);
    };

    self.toggleScroll= function() {
        
        $("#taskcontent").toggleClass("scrollingon");
    };
}

`TaskViewModel` 是此应用程序的核心类,负责处理用户交互,并与模型交互以更新视图。上面的代码不是完整的源代码,只是突出显示了关键方法。ViewModel 拥有两个用于 Tasks 和 Customers 的*observable arrays*。另外请注意使用*dependent observable*按访问日期过滤待办任务。有关完整的实现细节,请参阅源代码。

数据层

数据层由 `StorageController` 类实现。其实现细节如下:

// Store Manager Singleton class
// Implemented using revealing module design pattern
var tsk_ns = tsk_ns || {};

tsk_ns.storage = function () {
    // create instance of local storage
    var local = window.localStorage,
    // Save to local storage
        saveItem = function (key, item) {
            local.setItem(key, item);
        },
        getItem = function (key) {
            return local.getItem(key);
        },
        hasLocalStorage = function () {

            return ('localStorage' in window && window['localStorage'] != null);
        };

    // public members
    return {
        saveItem: saveItem,
        getItem: getItem,
        hasLocalStorage: hasLocalStorage

    };
} ();

上述类只是 `window.localStorage` 的一个包装器,并提供了一个包装方法来从本地存储中保存和获取数据。该类有一个 `hasLocalStorage` 方法,用于检查浏览器是否支持 localStorage。

UI 层

UI 层以纯 HTML 文件实现。

所有文档加载完成后,它将执行以下初始化代码。该代码定义了验证规则、jQueryUI 初始化代码以及 ViewModel 与 UI 的绑定。

// Document ready
$(function () {
    // Define Validation rules for customer form
    $('#customerForm').validate({
        rules: {
            firstname: "required",
            lastname: "required",
            //compound rule
            email: {
                email: true

            }
        }
    });

    // Define validation options for taskform
    $('#taskForm').validate({
        rules: {
            taskname: "required",
            startdate: {
                dateITA: true

            },
            visitdate: {

                dateITA: true
            },
            visittime: {
                time12h: true
            },

            price: {
                number: true,
                min: 0
            }

        },
        messages: {
            startdate: {
                dateITA: "Invalid Date! Enter date in (dd/mm/yyyyy) format."
            },
            visitdate:{
                dateITA: "Invalid Date! Enter date in (dd/mm/yyyyy) format."
            },
        }
    });
    // Initialise GUI widgets.
    $("#tabs").tabs();
    $("#taskNew,#taskSave,#taskUpdate").button();
    $("#taskNew").button("option", "disabled", false);
    $("#taskUpdate").button("option", "disabled", true);
    //Create instance of view model and bind to UI.
    var viewModel = new TaskViewModel();
    viewModel.init();
    viewModel.initCustomers();
    ko.applyBindings(viewModel);

    // Initialise Dialog.
    $("#dlgNotes").dialog({
        autoOpen: false,
        modal: true,
        buttons: {
            Ok: function () {

                $(this).dialog("close");
                viewModel.addTask();
            }
        }
    });
});

我使用了 KnockoutJS 的不同类型的绑定,例如 `for-each`、`with`、`form` 和 `custom` 绑定功能。有关完整详细信息,请参阅源代码。下面是 `Customer` 自定义绑定的代码。

ko.bindingHandlers.customerFromID = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var options = allBindingsAccessor().viewmodelOptions || {};
        var custid = valueAccessor();
        var vm = options.ViewModel;
        var sfield = options.showField;
        var curCust = vm.getCurrentCustomer(custid);
        if (curCust != null) {
           if(sfield===1)
             $(element).text(curCust.firstname + '_' + curCust.lastname);
           else if(sfield===2)
               $(element).text(curCust.phone);
           else if(sfield==3)
               $(element).text(curCust.mobile);
           
        }
        else
            $(element).text('');
    }
}

在待办任务列表表中,我希望显示 `Customer` 的详细信息,例如客户姓名、电话和手机。由于每一行都绑定到一个 `Task` 对象,该对象有一个 `customerID` 作为其属性之一,因此我必须使用 KnockoutJS 的自定义绑定功能来从 `customerID` 中提取客户详细信息。有关详细信息,请参阅上面的代码。

配置离线工作

使用 HTML5,我们现在可以配置应用程序应该离线提供的页面、资产和资源。这可以通过定义应用程序清单文件来实现,该文件是一个纯文本文件。此外,在 `html` 标签中,您需要定义 `manifest` 属性来指定清单文件的名称。`<html manifest="manfiest.appcache">` 我添加了扩展名为 `.appcache` 的清单文件。但您可以使用任何扩展名。我们需要配置 Web 服务器以包含此新扩展名,并将其 MIME 类型指定为 `text/cache-manifest`。请参阅下面的代码查看清单文件的内容。

请记住,清单文件中的第一行应该是 `CACHE MANIFEST`。不要在此行之前添加任何注释。清单文件的结构很简单。它有三个部分:`CACHE`、`NETWORK` 和 `FALLBACK`。所有离线使用的页面和资源都需要在 `CACHE` 部分指定。仅在线需要的页面/资源在 `NETWORK` 部分。在 `FALLBACK` 部分,您可以指定回退时的备用页面。就是这样!

支持应用程序缓存的浏览器将首先在客户端保存所有离线资源,然后无论您是在线还是离线,它将始终从应用程序缓存中提供这些资源。因此,对这些页面所做的任何更改仅在修改清单文件后才会生效。这一点很重要,需要记住。修改清单文件并更新版本以告知浏览器存在某些更改,以便它从服务器获取最新更改并再次将其保存在客户端。

//contentmanifest.appcache
CACHE MANIFEST
## version 1.6

CACHE:
themes/le-frog/jquery-ui.css
themes/le-frog/jquery-ui-1.8.17.custom.css
css/tasktracker.css
scripts/jquery-1.7.1.min.js
scripts/json2.js
scripts/jquery-ui-1.8.17.custom.min.js
scripts/jquery-ui-timepicker-addon.js
scripts/knockout-latest.js
scripts/ko-protected-observable.js
scripts/StorageManager.js
scripts/TaskController.js
scripts/jquery.validate.min.js
scripts/additional-methods.js
images/app-note-icon.png
images/Actions-document-edit-icon.png
images/edit-trash-icon.png

NETWORK:
# Other pages and resources only to be served online will come here
FALLBACK:
# Any alternate pages in case of fallback will come here.

移动端样式

我无需进行大量更改即可为此应用程序配置移动端。您需要做的就是在我 HTML 文件的 `head` 部分指定 `viewport` 设置。我不得不为 `textarea` 标签定义一个额外的样式,因为它的宽度超出了其父容器。有关详细信息,请参阅下面的代码。

// <meta name="viewport" content="width=device-width, initial-scale=1">
// styling for textarea
.fixwidth
{
    width:80%;    
}

关注点

最初,我将 `Task` 类中的 `startdate` 和 `visitdate` 属性声明为 `Date` 类型。但在保存和检索数据时遇到了问题。在 DatePicker 控件中显示了不正确的日期。因此,我修改了代码,将其保存为字符串。

由于该应用程序没有服务器端处理,因此在保存 `Task` 或 `Customer` 详细信息之前,使用以下代码触发客户端验证。

// Validation for Task Entry form
$('#taskForm').validate().form();
var isvalid = $('#taskForm').valid();
// Validation for customer entry form.
$("#customerForm").validate().form();
var custValid=  $("#customerForm").valid();

此外,在 iPhone 或 Android 手机上,由于屏幕宽度限制,我无法在待办任务列表表中显示所有列,因为它超出了其容器。我尝试使用 CSS 样式来显示滚动条。在桌面浏览器中效果很好,但在智能手机上不行。因此,我在待办任务列表表的 `tr` 标签的 `click` 事件上使用了以下代码。用户可以通过点击标题来折叠或展开行。这是使用 jQuery 的 `toggleClass` 方法实现的。

// toggleScroll method of TaskController class
self.toggleScroll= function() {

    $("#taskcontent").toggleClass("scrollingon");
};

Android 手机/iPhone/iPad 用户

Android 用户可以在此处下载原生应用。iPhone 和 iPad 用户可以此处访问演示应用程序。他们可以将其添加书签。这样无论是在线还是离线,它都能正常工作。

历史

这是 TaskTracker 的第一个版本。在未来的版本中,我考虑将任务和客户详细信息保存在服务器端数据库中。这样,当连接到互联网时,应用程序就可以与数据库同步。这样,所有数据都可以从任何地方访问,无论是移动设备还是桌面浏览器。而此版本则不然,数据存储在客户端浏览器中。

结论

由于 HTML5 的新功能,现在可以轻松开发离线 Web 应用程序。该应用程序探讨了 HTML5、jQuery、jQuery-UI、jQuery-Validation 和 KnockoutJs 的关键功能。此外,我们还可以使用 PhoneGap 等开源框架将其转换为不同移动平台的原生应用程序。如果您对实现有任何疑问或需要澄清,请告诉我您的评论和建议。您可以通过 电子邮件 与我联系。

© . All rights reserved.