HTML5 离线 MVC: 第一部分





5.00/5 (5投票s)
MVC 中的离线 Web 应用程序。
引言
我正在为我们的一个客户构建一个离线 Web 应用程序。目前(目标)是 iPad。由于我无法向您公开该特定项目的源代码,所以我决定创建一系列文章,重点介绍用于创建此类应用程序的一些 HTML5 功能。为此,我设计了一个简单的应用程序,您可以使用它来存储您认识的人的联系信息。
这里的目标是实际拥有离线视图、模型和 JavaScript。并以与 ASP.NET MVC 处理它们类似的方式处理它们。
我将在系列文章中向您展示整个应用程序的不同部分。我不确定整个事情将分为多少篇文章,以及它们的顺序。但它们应该共同涵盖以下内容:
- 创建 JavaScript MVC 结构
- 创建视图控制器结构 <- (就是这篇
- 创建控制器模型结构
- 创建视图控制器结构 <- (就是这篇
- 连接 WebSql 数据库并与之交互
- 通过 ApplicationCache 将整个应用离线化
必备组件
本演示和文章依赖于之前的两篇文章:《自己动手写一个 JavaScript 路由器》和《JavaScript 命名空间》。尽管后者对于此代码的实际工作并不重要。更重要的是,当您看到以下内容时,您会明白我的意思:
namespace("Demo.Controllers", function() {
然而,关于路由器代码,我需要假定您已经了解。
除此之外,它将使用 jQuery(当然)和 Mustache 模板引擎。我使用它已经有一段时间了,它确实能完成任务。我本可以尝试使用 Knockout 的双向绑定魔术,但我觉得那并不能使事情更清楚。我使用 Mustache 就像 ASP.NET MVC 使用 Razor 一样。
将有两个命名空间:`Core` 和 `Demo`。`Core` 将包含创建 MVC 魔法的所有关键。而 `Demo` 将是实现部分。
在文件夹中,您还会找到 `core.js`。这只是 Core 命名空间的开始,其中包含一些辅助函数。其中最重要的一个是 `Core.apply`。这个函数已经在我的工具箱中有一段时间了,并且被证明非常有用。我从 Ext 库中“偷”来了它,它的作用是将一个对象复制到另一个对象中。
控制器和视图
本文我将带您了解如何创建控制器和视图代码。我不介意复杂的代码,但让多个视图协同工作的控制器应该尽可能简单。ASP.NET MVC 中的控制器看起来非常简单。所有的复杂性都被隐藏在它们继承的 Controller 类中。我们也将采用完全相同的方法。
我们的 Home 控制器最终应该看起来像这样
namespace("Demo.Controllers", function(Namespace) {
var Home=function() {
//The construtor
Core.apply(this, Core.Controller.prototype);
return Core.Controller.apply(this, arguments);
};
Home.prototype.index=function(action, id) {
//home/index
//starts home/index.html
this.viewBag.datetime=new Date();
this.view();
}
Namespace["Home"]=Home;
})
您希望能看到与 ASP.NET MVC 的相似之处。`viewBag` 是我们的 Mustache 模板将消费的数据。而 `this.view` 语句将向我们展示结果。构造函数中的混乱我将在下面进一步解释。
我们的视图模板可能看起来像这样
<h2>Home</h2>
<div>It's now {{datetime}}<div>
目录结构
为了获取视图,目录有一个强制性的结构。至少对于视图应该放置的位置是这样。
- assets (存放核心代码和 CSS 等)
- 控制器
- contacts.js
- home.js
- 视图
- contacts
- contact.html
- index.html
- 主页
- index.html
- contacts
- index.html
所以路由 "#home/index" 将使用 "controllers/home.js" 来获取视图 "views/home/index.html"。
Core.Controller 类
在我们的 Home 控制器的构造函数中,我们看到了一些奇怪的语句。首先是 `Core.apply(this, Core.Controller.prototype);`,然后是 `return Core.Controller.apply(this, arguments);`。第一个是继承的替代方法。它不是像通常的 `Home.prototype=new Core.Controller();` 那样,而是将原型复制到自身中。
“为什么?”你问?因为 `Core.Controller` 的构造函数不返回自身。它返回一个函数。如果你还没有读过路由器文章,现在就应该读一下……读完了吗?路由器处理的是函数,而不是类。它根据提供的路由启动一个函数。我觉得它应该保持这种方式。如果我们将实际的对象实例提供给它,它就需要知道如何启动这些实例内部的正确函数。我不想那样做。但我确实希望它启动一个实例。让我通过展示主 `index.html` 中设置到我们的 home 控制器的路由的方式来阐明这一点。
$(document).ready(function() {
router.registerRoute("home/:action:/:id:", new Demo.Controllers.Home("home", "wrapper"));
});
`router.registerRoute` 将一个函数作为其参数。但这里它也在创建一个 Home 控制器的实例。秘密在于 `Core.Controller` 类。
namespace("Core", function() {
Core.Controller=function() {
//what's the head part of our route?
//this will be "home" coming from our constructor arguments!
this.viewBasePath=arguments[0];
//did we get a container id or should we just use body
//e.g. the div to render the view in
//this will be "wrapper" coming from our constructor arguments!
this.container=arguments[1]? "#"+arguments[1]: "body";
//the future data to be passed into the template
this.viewBag={};
//the view templates, we will cash them here to prevent http request
this.viewTemplates={};
//reference to ourself for the return function!
var _self=this;
return function() {
//we'll get the first argument out of our router function! Not the constructor!
var controllerFunction=arguments[0] ? arguments[0] : "index";
//So now we'll have the controller(_self.viewBasePath) and the function within that controller
_self.viewSubPath=controllerFunction;
//Do we actually have this function
if(typeof _self[controllerFunction] == "function") {
_self[controllerFunction].apply(_self, arguments);
}
else {
_self.unknownView();
}
}
}
........
正如你所看到的,它实际上返回一个函数。这是路由器将启动的函数。它通过以下语句传递给 Home 控制器
return Core.Controller.apply(this, arguments);
将此函数与类实例保持一致的诀窍是由 JavaScript 的函数作用域引起的。因为我们之前缓存了 `_self`。JavaScript 将向上遍历函数作用域以查找 `_self`。它最终将到达 `Core.Controller` 的构造函数。在那里,由于 `return Core.Controller.apply(this, arguments);`,作用域现在是我们的 Home 控制器,它拥有 `Core.Controller` 的整个原型。
也许您想在此时躺下。我不会怪你
所以让我们回顾一下:路由器启动了我们 `Demo.Controllers.Home` 类的一个实例,而它需要一个函数。`Demo.Controllers.Home` 实例将 `Core.Controller` 的原型复制到自身中,并返回对 `Core.Controller` 原型构造函数的调用。`Core.Controller` 实例设置了一些变量,存储了对自身的引用,然后返回一个函数,而不是它自己的实例给路由器。
这是一个很棒的技巧,允许你将某个实例传递给只接受函数的其他东西。路由器不应该被所有这些负担。它是一个只懂一招的马,而且应该保持这种方式。我在这里使用了我自己的路由器,所以我可以修改它,但是如果我宁愿使用像 `Crossroad.js` 这样的第三方路由器,我仍然可以用这种方式使用它。使用函数作为构造函数的返回值还为你提供了创建具有私有函数和变量的类的可能性。但那是另一篇文章的内容了。然而,有一个缺点。你不能使用经典的继承方式,比如 `Home.prototype=new Core.Controller();`。
为什么不将其设为单例模式?
只有一个 home 路由,那为什么不把 home 控制器做成单例呢?原因在于 home 将不是唯一的路由,我希望将复杂性隐藏在 `Core.Controller` 中。所以我会需要它的多个实例。
深入研究返回/路由函数
让我们看看 `Core.Controller` 实际返回的函数
var controllerFunction=arguments[0] ? arguments[0] : "index";
我们已经要求路由器查找
"home/:action:/:id:"所以 action 可以为空。在这种情况下,我们将其设为“index”。
_self.viewSubPath=controllerFunction;
存储以备将来参考。
//Do we actually have this function
if(typeof _self[controllerFunction] == "function") {
_self[controllerFunction].apply(_self, arguments);
}
else {
_self.unknownView();
}
如果有一个函数与路由器传递的 action 变量同名,就启动它。这将导致
Home.prototype.index=function(action, id) {
//home/index
//starts home/index.html
this.viewBag.datetime=new Date();
this.view();
}
视图功能
现在是所有关于的部分。我们想获取 `views/home/index.html` 并将其粘贴到页面中。所以 `Core.Controller` 有 `view` 函数。如果它还没有存储模板,它将通过 AJAX 获取模板。然后启动 `renderView` 函数,Mustache 将 `viewBag` 粘贴到模板中。变量 `this.container` 在我们最初设置路由时在构造函数中传递。它是这里的包装器,显然应该在主 `index.html` 中是一个 `div`,用于放置模板。
Core.Controller.prototype.view=function() {
if(this.viewTemplates.hasOwnProperty(subView)) {
//if we've already stored the template use that
this.renderView(subView);
}
else {
//we didn't have this template, so we'll have to get it with jQuery
var _self=this;
$.ajax({
url: "views/"+this.viewBasePath+"/"+subView+".html",
success: function(data) {
_self.viewTemplates[subView]=data;
_self.renderView(subView);
}
});
}
}
//Render the template
Core.Controller.prototype.renderView=function(subView) {
//The container is #wrapper, which came from the contructor function
//you could check here if this was alreay an jQuery object or just an id, but jQuery does that for you and I'm lazy
$(this.container).html(Mustache.render(this.viewTemplates[subView], this.viewBag));
}
下载包里有什么
至少还有一个控制器。联系人控制器。它有多个视图。并且还处理数据(某种程度上)。所以它应该能进一步阐明问题。将所有内容都放在文章中会太长。**下载它**。这是真正理解本文的唯一方法。如果你已经读到这里,你就应该这样做。
结论
我不得不承认,我不知道这个解释是否足够清楚。但请下载代码并运行它、阅读它、修改它。在接下来的几篇文章中,我们将扩展所有这些内容,使 `Core.Controller` 类的使用更加清晰。
下一个将是关于方程模型部分的制作。