JavaScriptMVC-学习指南-全-

JavaScriptMVC 学习指南(全)

原文:zh.annas-archive.org/md5/0cd0f52d5cb193918547ba5bb9825273

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

《学习 JavaScriptMVC》 将引导您了解所有框架方面,并展示如何构建小型到中型、结构良好且文档齐全的客户端应用程序,您将喜欢在它们上工作。

本书涵盖的内容

第一章,JavaScriptMVC 入门,提供了 JavaScriptMVC 框架的概述。安装它,了解其架构,并学习以最佳方式完成它——通过构建一个简单的应用程序。

第二章,DocumentJS,展示了尽管功能强大,但 DocumentJS 是一个简单的工具,旨在轻松创建任何 JavaScript 代码库的可搜索文档。

第三章,FuncUnit,解释了 FuncUnit 是一个具有类似 jQuery 语法的功能测试框架。使用 FuncUnit,我们可以在所有现代网络浏览器中运行测试。编写测试既简单又快捷。

第四章,jQueryMX,展示了 jQueryMX 是一个提供实现和组织大型 JavaScript 应用程序所需功能的 jQuery 库集合。它提供了经典继承模拟和模型-视图-控制器层,以提供逻辑上分离的代码库。

第五章,StealJS,展示了 StealJS 是一个独立的代码管理和构建工具。

第六章,构建应用程序,展示了如何从概念到设计、实现、文档和测试构建实际应用程序。

本书所需的条件

要运行本书中的示例,需要以下软件:

本书面向的对象

本书面向任何对使用基于最流行的 JavaScript 库——jQuery 的 JavaScriptMVC 框架开发小型到中型网络应用程序感兴趣的人。

读者应熟悉 JavaScript、浏览器 API、jQuery、HTML5 和 CSS。

习惯用法

在本书中,您将找到不同样式的文本,以区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词如下所示:"通过checkout标签轻松切换到另一个版本。"

代码块如下设置:

<!doctype html>

<html>
    <head>
        <title>Todo List</title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <ul id="todos">
            <li>all done!</li>
        </ul>

        <script src="img/steal.js?todo"></script>
    </body>
</html>

当我们希望您注意代码块中的特定部分时,相关的行或项目将以粗体显示:

steal(
    'jquery/class',
    'jquery/model',
 'jquery/dom/fixture',
 'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',

    function ($) {

    }
);

任何命令行输入或输出都如下所示:

$ git submodule add git://github.com/bitovi/steal.git
$ git submodule add git://github.com/bitovi/documentjs.git
$ git submodule add git://github.com/bitovi/funcunit.git
$ git submodule add git://github.com/jupiterjs/jquerymx jquery

新术语重要词汇以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中显示如下:“存档按钮在任务悬停时可见。”

注意

警告或重要提示以这样的框出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们始终欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中受益的标题非常重要。

要发送一般反馈,只需发送电子邮件到mailto:feedback@packtpub.com,并在邮件主题中提及书籍标题。

如果您在某个主题上具有专业知识,并且您对撰写或为书籍做出贡献感兴趣,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者了,我们有许多事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support,并注册以将文件直接通过电子邮件发送给您。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从www.packtpub.com/support选择您的标题来查看。

侵权

互联网上版权材料的侵权是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在网上遇到我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过链接mailto:copyright@packtpub.com与我们联系,并提供涉嫌侵权材料的链接。

我们感谢您在保护我们的作者和我们为您提供有价值内容的能力方面的帮助。

问题

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章. JavaScriptMVC 入门

在本章中,我们将概述 JavaScriptMVC 框架。我们将安装它,了解其架构,并以最佳方式学习它。最后,我们将构建一个简单的应用程序。没有什么比例子更有效了。有人说这是唯一有效的方法。

什么是 JavaScriptMVC?

JavaScriptMVCJMVC)是一个基于 jQuery 库构建的 JavaScript 开源 模型-视图-控制器MVC)框架。

它是一个后端无关的客户端框架,可以与任何后端解决方案一起使用,例如 Node.js、Ruby on Rails、Django 等。

JavaScriptMVC 的理念是提供一套工具,以尽可能短的时间内构建高质量且易于维护的应用程序。

JavaScriptMVC 包含以下独立组件:

  • StealJS:这是依赖管理器和生产构建

  • FuncUnit:这是单元和功能测试组件

  • jQueryMX:这包含一系列插件,提供将大型 JavaScript 代码库实现和组织成良好结构化和组织形式的功能,提供模型-视图-控制器抽象层

  • DocumentJS:这是文档

第一个版本于 2008 年 5 月发布。当前版本 3.2 于 2010 年 12 月发布。本书撰写时的最新版本是 3.2.2。

在即将发布的 JavaScriptMVC 3.3 版本中,jQueryMX 项目将被 CanJS 替换。使用当前 JMVC 版本的项目在经过少量重构后应能与 JMVC 3.3 一起工作,这得益于名称回退。

JavaScriptMVC 4.0 将更名为 DoneJS,并将对 StealJS 进行重大更改,使其完全兼容 AMD,并与 CommonJS 一起工作,在 Node.js 上运行。FuncUnit 将分为 3 部分:Syn - 合成事件库,ShouldJS - 使用 Jasmine 或 QUnit 的异步测试驱动,DidJS - 自动测试运行器绑定,用于 Jasmine 或 QUnit,Selenium,PhantomJS 等。

许可证

JavaScriptMVC 在以下例外情况下根据 MIT 许可证授权:

  • Rhino:这是 JavaScript 命令行(MPL 1.1)

  • Selenium 浏览器自动化(Apache 2)

链接

您可以参考以下 URL 了解更多关于 JavaScriptMVC 的信息:

为什么选择 JavaScriptMVC?

JavaScriptMVC 是一个稳固且文档齐全的框架。

它基于极其流行的 JavaScript 库 jQuery,许多 JavaScript 程序员都熟悉其工厂方法和链式函数风格。

JavaScriptMVC 是一个完整的包。它包含我们构建、管理、文档和测试 JavaScript 项目所需的一切。

由于它是一个模块化框架,我们不需要使用所有可用的组件。我们可以从只使用我们实际需要的框架组件开始,并在需要时添加额外的组件。

学习曲线相对较低,尤其是如果读者熟悉其他 JavaScript 框架,如轻量级的 Backbone 和 Sammy 或重量级的工具包,如 Dojo 工具包或 Google Closure。同时,它提供了比轻量级兄弟更多的功能,而没有沉重的感觉,例如 Google Closure,它生成的代码更干净,提供的文档比非常流行的 Dojo 工具包更好。

它的一个杀手级特性是防止内存泄漏。这是客户端应用程序的一个重要方面,这些应用程序在文档对象模型DOM)树上进行许多操作。

小贴士

JavaScriptVC 中的 MVC

JavaScriptMVC 利用经典的 MVC 模式,将业务逻辑和应用程序数据与用户界面分离。

系统架构方法

当构建 Web 应用程序时,我们可以区分两种方法——多页面应用和单页面应用。

多页面应用中,大部分的业务逻辑是在后端系统中实现的,同时在 JavaScript 中进行一些增强。例如,Ruby on Rails 应用程序,其中大部分主要逻辑是通过后端 MVC 架构实现的,当用户导航到另一个页面时,会发送一个普通的http请求。

单页面应用中,大部分的业务逻辑是在前端实现的。例如,JavaScriptMVC 应用程序,其中大部分主要逻辑是通过前端 MVC 架构实现的。当用户导航到另一个页面时,前端路由器将分发所有请求并调用后端 API;例如,在 Sinatra 中。

JavaScriptMVC 单页面应用

JavaScriptMVC 是为单页面应用场景设计的。了解与多页面应用相比,单页面应用方法的优缺点是很好的。

优点

  • 大多数状态都是在客户端维护的,因此我们不需要在服务器端保持会话状态。

  • 大多数请求都是通过 XHR 调用完成的,因此每次不需要加载新页面,这可能会导致高内存占用(尤其是在旧式的、非事件驱动的服务器,如 Apache 服务器中)。

  • 大部分业务逻辑在客户端,因此我们可以节省许多对服务器的调用。

缺点

  • 由于使用 RPC 在服务器和客户端之间来回移动数据,负载均衡和内容分发网络CDN)可能会变得复杂。

  • 搜索引擎优化SEO)由于动态构建的 JavaScript 页面可能会变得复杂。

真实世界的例子

读者可以在community.javascriptmvc.com/posts/in-bucket/apps找到使用 JavaScriptMVC 框架构建的 Web 应用程序。

安装 JavaScriptMVC

安装 JavaScriptMVC 就像泡茶一样简单,但速度更快。

选择你的方法

有三种方法。

最后两种方法是首选方式,原因如下:

第三种方法似乎是最好的,因为它包含了第二种方法的全部优点,并且创建了一个封装的环境,我们可以轻松快速地创建或删除,而不会影响我们当前的开发环境设置。

哪种方法适合我?

对于快速尝试库,选择第一种方法。对于实际开发,肯定选择第二种。

第一种方法——下载包

在这个方法中,我们将使用 JavaScriptMVC 网页上的 Web 界面来配置和下载包:

  1. javascriptmvc.com下载完整包并解压其内容。

  2. 在本地 web 服务器工作目录下创建一个名为Todo的文件夹。

  3. javascriptmvc-3.2.2文件夹中的所有文件复制到Todo文件夹,并启动 web 服务器。

    $ mkdir Todo && cp -r javascriptmvc-3.2.2/* Todo && cd Todo
    
    

就这些;我们已经设置好了,准备出发。

第二种方法——从 Git 仓库拉取代码

我们假设读者已经了解并安装了 Git。

如果没有,以下资源可能有所帮助:

在以下步骤中,我们将为我们的Todo示例项目安装 JavaScriptMVC:

  1. 在本地 web 服务器目录下,创建一个名为Todo的新文件夹:

    $ mkdir Todo && cd Todo
    
  2. Todo文件夹内,创建一个新的 Git 仓库:

    $ git init
    
  3. 将 JavaScriptMVC 组件作为子模块添加到项目中:

    $ git submodule add git://github.com/bitovi/steal.git
    $ git submodule add git://github.com/bitovi/documentjs.git
    $ git submodule add git://github.com/bitovi/funcunit.git
    $ git submodule add git://github.com/jupiterjs/jquerymx jquery
    
  4. 安装和更新子模块:

    $ git submodule init
    $ git submodule update
    
  5. 我们需要安装的最后一个模块是Syn。由于它已经是FuncUnit项目的子模块,我们只需要初始化并更新它:

    $ cd funcunit
    $ git submodule init
    $ git submodule update
    
  6. Syn切换到master分支:

    $ cd syn/
    $ git checkout master
    
  7. 返回项目的根目录:

    $ cd ../..
    
  8. js命令移动到项目的根目录:

    $ ./steal/js steal/make.js
    

验证安装

项目目录应该有以下的文件夹结构:

.git
.gitmodules
documentjs
funcunit
jquery
js
js.bat
steal

就这些;我们已经设置好了,准备出发。

注意

更多关于 Git 子模块的信息:git-scm.com/book/en/Git-Tools-Submodules

第三种方法——Vagrant

要使用此方法安装 JavaScriptMVC,我们需要安装 Vagrant,它是一个围绕 Oracle VM VirtualBox 的虚拟化开发工具包装器,Oracle VM VirtualBox 是一个 x86 和 AMD64/Intel64 虚拟化软件包。

  1. 下载并安装 Oracle VM VirtualBox (www.virtualbox.org).

  2. 下载并安装 Vagrant (downloads.vagrantup.com).

  3. 下载并解压 JavaScriptMVC 启动器 (github.com/wbednarski/JavaScriptMVC_kick-starter/archive/master.zip).

  4. 在 JavaScriptMVC 启动器文件夹中输入 vagrant up

    此命令创建一个虚拟环境和项目目录。它还安装了 Web 服务器。JavaScriptMVC 框架将被放置在 Todo 目录中。

我们在项目目录内所做的任何更改都会立即在 http://192.168.111.111/ 的 Web 浏览器中可见。

文档和 API

良好的文档和 API、许多教程以及良好的代码库文档是 JavaScriptMVC 的优势:

论坛和 Stack Overflow 上的活跃社区:

JavaScriptMVC 的架构

JavaScriptMVC 的架构是模块化的。强大的堆栈包含我们构建一个组织良好、经过测试和文档化的应用程序所需的一切。

这里列出了 JavaScriptMVC 的关键组件以及下一章中涵盖的主题。

DocumentJS

DocumentJS 是一个独立的 JavaScript 文档应用程序,并提供以下功能:

  • 带有源代码和 HTML 面板的内联演示

  • 向文档添加标签

  • 添加文档为收藏

  • 自动建议搜索

  • 测试结果页面

  • 评论

  • 扩展 JSDoc 语法

  • 添加未记录的代码,因为它理解 JavaScript

FuncUnit

FuncUnit 是一个独立的 Web 测试框架,并提供以下功能:

  • 测试点击、输入、移动鼠标光标和拖放实用程序

  • 在页面间跟踪用户

  • 多浏览器和操作系统支持

  • 持续集成解决方案

  • 在浏览器中编写和调试测试

  • 与 jQuery 并行的链式 API

jQueryMX

jQueryMX 是 JavaScriptMVC 的 MVC 部分,并提供以下功能:

  • 鼓励逻辑分离、确定性代码

  • MVC 层

  • 统一的客户端模板接口(支持 jq-tmpl、EJS、JAML、Micro 和 Mustache)

  • Ajax 固定值

  • 有用的 DOM 工具

  • 语言助手

  • JSON 工具

  • 类系统

  • 自定义事件

StealJS

StealJS是一个独立的代码管理和构建工具,并提供以下强大功能:

依赖关系管理

  • 加载 JavaScript 和 CoffeeScript

  • 加载 CSS、Less 和 Sass 文件

  • 加载客户端模板,如 TODO

  • 只加载单个文件一次

  • 从不同域名加载文件

连接和压缩

  • Google Closure 压缩机

  • 创建多页构建

  • 预处理TODO

  • 可以有条件地从生产构建中删除指定的代码

  • 构建独立的 jQuery 插件

记录器

  • 在开发模式下记录消息

代码生成器

  • 生成应用程序骨架

  • 添加创建自定义生成器的可能性

包管理

  • 从 SVN 和 Git 仓库下载并安装插件

  • 安装依赖项

  • 运行安装脚本

  • 只加载单个文件一次

  • 从不同域名加载文件

代码清理器

  • 对你的代码库运行 JavaScript 美化器

  • 对你的代码库运行 JSLint

构建简单应用

我们安装了 JavaScriptMVC,简要了解了其组件。现在,我们准备好构建第一个 JavaScriptMVC 应用程序。

激动吗?让我们来施展魔法。

待办事项列表

我们将学习 JavaScriptMVC,在经典的示例应用——待办事项列表中。

注意

如果你好奇,并想根据todos应用程序示例比较不同的 JavaScript 框架,那么 GitHub 项目绝对很棒。你可以在github.com/tastejs/todomvc/tree/gh-pages/architecture-examples找到它。项目主页在todomvc.com/

加载器

在安装 JavaScriptMVC 期间创建的Todo文件夹中,创建一个名为todo的文件夹。在todo文件夹内创建名为todo.htmltodo.js的文件。

项目目录应具有以下结构:

Todo/
    .git
    .gitmodules
    todo/
        todo.html
        todo.js
    documentjs
    funcunit
    jquery
    js
    js.bat
    steal

将以下代码复制并粘贴到todo.html中,以加载StealJStodo.js文件:

<!doctype html>

<html>
    <head>
        <title>Todo List</title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <ul id="todos">
            <li>all done!</li>
        </ul>

        <script src="img/steal.js?todo"></script>
    </body>
</html>

注意

../steal/steal.js?todo等同于../steal/steal.js?todo/todo.js。如果未提供文件名,StealJS将尝试加载与给定文件夹同名的 JavaScript 文件。

todo.js中添加以下代码以加载jQueryMX插件。它们是实现此应用程序所必需的:

steal(
    'jquery/class',
    'jquery/model',
    'jquery/dom/fixture',
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',

    function ($) {

    }
);

通过输入http://YOUR_LOCAL_WEB_SERVER/Todo/todo.html在网页浏览器中打开页面,并使用像 Google Chrome Inspector 这样的网页开发工具检查StealJS和所有列出的插件是否正确加载。

模型

下一步是将模型添加到我们的应用程序中,通过扩展jQueryMX项目中的$.Model

第一个参数是模型名称(字符串),第二个参数是具有类属性和方法的对象。最后一个参数是原型实例属性,在这个例子中我们将其留为空对象:

steal(
    'jquery/class',
    'jquery/model',
    'jquery/dom/fixture',
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',

    function ($) {
 $.Model('Todo', {
 findAll: 'GET /todos',
 findOne: 'GET /todos/{id}',
 create:  'POST /todos',
 update:  'PUT /todos/{id}',
 destroy: 'DELETE /todos/{id}'
 },
 {

 }
 );
    }
);

注意

类属性不是随机的;它们在模型 API 中有描述。javascriptmvc.com/docs.html#!jquerymx

我们已经为我们的 todo 列表应用程序创建了 Todo 模型。现在,是时候玩转它了。

  1. 打开一个网络浏览器,并在 JavaScript 控制台中输入以下行:

    var todo = new Todo({name: 'write a book'});
    

    todo 现在是 Todo 的一个实例,属性名为 write a book,属性值为 write a book

  2. 按如下方式获取属性值:

    todo.attr('name');
    
  3. 如果属性存在,则设置属性值,如下所示:

    todo.attr('name', 'write JavaScript book');
    

    或者通过 attrs,我们可以同时设置多个属性,以及添加新的属性:

    todo.attrs({name: 'write JavaScriptMVC book!'});
    
  4. 添加两个新属性:

    todo.attrs({
        person: 'Wojtek',
        dueDate: '1 December 1012'
    });
    
  5. 列出所有属性:

    Todo.attrs();
    

以下截图显示了前面命令的执行:

模型

固定装置

由于在我们的前端应用程序中没有后端服务来处理 /todo API 调用,因此尝试在 Todo 模型上调用模型的一个 CRUD 方法将导致网络错误。

注意

创建、读取、更新、删除CRUD)是持久存储的四个基本功能。

固定装置

到目前为止,$ .fixture 出现了救援。有了这个特性,我们可以在后端代码尚未准备好的情况下工作项目。

Todo 模型创建固定装置:

steal(
    'jquery/class',
    'jquery/model',
    'jquery/util/fixture',
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',

    function ($) {
        $.Model('Todo', {
                findAll: 'GET /todos',
                findOne: 'GET /todos/{id}',
                create:  'POST /todos',
                update:  'PUT /todos/{id}',
                destroy: 'DELETE /todos/{id}'
            },
            {

            }
        );

 // Fixtures
 (function () {

 var TODOS = [
 // list of todos
 {
 id:   1,
 name: 'read The Good Parts'
 },
 {
 id:   2,
 name: 'read Pro Git'
 },
 {
 id:   3,
 name: 'read Programming Ruby'
 }
 ];

 // findAll
 $.fixture('GET /todos', function () {
 return [TODOS];
 });

 // findOne
 $.fixture('GET /todos/{id}', function (orig) {
 return TODOS[(+orig.data.id) - 1];
 });

 // create
 var id = 4;
 $.fixture('POST /todos', function () {
 return {
 id: (id++)
 };
 });

 // update
 $.fixture('PUT /todos/{id}', function () {
 return {};
 });

 // destroy
 $.fixture('DELETE /todos/{id}', function () {
 return {};
 });

 }());
    }
);

现在,我们可以像后端服务在这里一样使用我们的 Todo 模型方法。

例如,我们可以列出所有 todos

Todo.findAll({}, function(todos) {
    console.log('todos: ', todos);
});

以下截图显示了 console.log('todos: ', todos); 命令的输出:

固定装置

查看

现在是添加一些 HTML 代码的好时机,以便在浏览器控制台之外看到一些内容。为此,使用开源客户端模板系统 嵌入式 JavaScriptEJS)。

todo 目录(与 todo.js 所在的同一文件夹)中创建一个新的文件 todos.ejs,并向其中添加以下代码:

<% $.each(this, function(i, todo) { %>

    <li <%= ($el) -> $el.model(todo) %>>
        <strong><%= todo.name %></strong>
        <em class="destroy">delete</em>
    </li>

<% }) %>

然后,在控制台中输入以下内容:

$('#todos').html('todos.ejs', Todo.findAll());

现在,我们可以看到所有 todos 被打印出来:

视图

基本上,EJS 模板是一个带有在 <%%><%=%>(以及一些其他方式)之间注入的 JavaScript 代码的 HTML 文件。

不同之处在于,在第二种情况下,JavaScript 代码返回的所有值都被转义并打印出来。在第一种情况下,它们只被评估。

第一行是 jQuery 的 each 循环——这里没有魔法。然而,下一行对于许多读者来说可能是一个新事物。它是 ECMAScript Harmony 类似的箭头函数语法,用于 EJS 解析器,它的简单性不会使整个画面变得暗淡。

以下语法:

($el) -> $el.model(todo)

可以解释如下:

function ($el) {
    return $el.model(todo)
}

控制器

让我们在用户界面中添加一些动作。

将以下代码添加到 todo.js 文件中,并在浏览器中刷新应用程序:

$.Controller('Todos', {
    // init method is called when new instance is created
    'init': function (element, options) {
        this.element.html('todos.ejs', Todo.findAll());
    },

    // add event listener to strong element on click
    'li strong click': function (el, e) {
        // trigger custom event
        el.trigger('selected', el.closest('li').model());

        // log current model to the console
        console.log('li strong click', el.closest('.todo').model());
    },

    // add event listener to em element on click
    'li .destroy click': function (el, e) {
        // call destroy on the model to prevent memory leaking
        el.closest('.todo').model().destroy();
    },

    // add event listener to Todo model on destroyed
    '{Todo} destroyed': function (Todo, e, destroyedTodo) {
        // remove element from the DOM tree
        destroyedTodo.elements(this.element).remove();

        console.log('destroyed: ', destroyedTodo);
    }
});

// create new controller instance
new Todos('#todos');

现在,你可以点击todo名称来查看控制台日志或删除它。

当一个新的控制器实例化时,会调用init方法。

controller元素从 DOM 树中移除(在我们的例子中是#todos)时,会自动调用destroy方法,解绑所有controller事件处理器,并释放其元素以防止内存泄漏。

路由

替换以下代码:

// create new Todo controller instance
new Todos('#todos');

使用:

// routing
$.Controller('Routing', {
    init: function () {
        new Todos('#todos');
    },

    // the index page
    'route': function () {
        console.log('default route');
    },

    // handle URL witch hash
    ':id route': function (data) {
        Todo.findOne(data, $.proxy(function (todo) {
            // increase font size for current todo item
            todo.elements(this.element).animate({fontSize: '125%'}, 750);
        }, this));
    },

    // add event listener on selected
    '.todo selected':  function (el, e, todo) {
        // pass todo id as a parameter to the router
        $.route.attr('id', todo.id);
    }
});

// create new Routing controller instance
new Routing(document.body);

刷新应用并尝试点击todo列表元素。你会发现点击带有相应 ID 的todo项后,URL 会更新。

完整应用代码

这是Todo应用的完整代码:

steal(
    'jquery/class',
    'jquery/model',
    'jquery/util/fixture',
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',

    function ($) {
        $.Model('Todo', {
                findAll: 'GET /todos',
                findOne: 'GET /todos/{id}',
                create:  'POST /todos',
                update:  'PUT /todos/{id}',
                destroy: 'DELETE /todos/{id}'
            },
            {

            }
        );

        // Fixtures
        (function () {
            var TODOS = [
                // list of todos
                {
                    id:   1,
                    name: 'read The Good Parts'
                },
                {
                    id:   2,
                    name: 'read Pro Git'
                },
                {
                    id:   3,
                    name: 'read Programming Ruby'
                }
            ];

            // findAll
            $.fixture('GET /todos', function () {
                return [TODOS];
            });

            // findOne
            $.fixture('GET /todos/{id}', function (orig) {
                return TODOS[(+orig.data.id) - 1];
            });

            // create
            var id = 4;
            $.fixture('POST /todos', function () {
                return {
                    id: (id++)
                };
            });

            // update
            $.fixture('PUT /todos/{id}', function () {
                return {};
            });

            // destroy
            $.fixture('DELETE /todos/{id}', function () {
                return {};
            });
        }());

        $.Controller('Todos', {
            // init method is called when new instance is created
            'init': function (element, options) {
                this.element.html('todos.ejs', Todo.findAll());
            },

            // add event listener to strong element on click
            'li strong click': function (el, e) {
                // trigger custom event
                el.trigger('selected', el.closest('li').model());

                // log current model to the console
                console.log('li strong click', el.closest('.todo').model());
            },

            // add event listener to em element on click
            'li .destroy click': function (el, e) {
                // call destroy on the model to prevent memory leaking
                el.closest('.todo').model().destroy();
            },

            // add event listener to Todo model on destroyed
            '{Todo} destroyed': function (Todo, e, destroyedTodo) {
                // remove element from the DOM tree
                destroyedTodo.elements(this.element).remove();

                console.log('destroyed: ', destroyedTodo);
            }
        });

        // routing
        $.Controller('Routing', {
            init: function () {
                new Todos('#todos');
            },

            // the index page
            'route': function () {
                console.log('default route');
            },

            // handle URL witch hash
            ':id route': function (data) {
                Todo.findOne(data, $.proxy(function (todo) {
                    // increase font size for current todo item
                    todo.elements(this.element).animate({fontSize: '125%'}, 750);
                }, this));
            },

            // add event listener on selected
            '.todo selected':  function (el, e, todo) {
                // pass todo id as a parameter to the router
                $.route.attr('id', todo.id);
            }
        });

        // create new Routing controller instance
        new Routing(document.body);
    }
);

摘要

在本章中,我们学习了什么是 JavaScriptMVC,以及为什么它是一个好且稳固的框架。我们还学习了如何安装它,浏览文档和 API。通过构建一个简单的应用,我们对其架构有了概览。

如果你能够理解我们在这章中编写的所有代码,你将能够轻松快速地深入研究框架。恭喜你!

第二章. DocumentJS

仅源代码是不够的;文档是软件工程的重要组成部分。DocumentJS 是一个强大且简单的工具,旨在轻松为任何 JavaScript 代码库创建可搜索的文档。

在本章中,我们将了解 DocumentJS 的概述。我们将学习它是如何工作的,并学习如何生成其文档。

以下是一些 DocumentJS 的关键特性:

  • 灵活且易于扩展

  • 支持 Markdown:en.wikipedia.org/wiki/Markdown

  • 集成文档查看器和 API 搜索

  • 与任何 JavaScript 代码一起工作,而不仅仅是与 JavaScriptMVC 一起工作

如果你熟悉 JSDoc、YUIDoc、YARD 或类似的文档语法/工具,那么 DocumentJS 可以在几分钟内学会。

DocumentJS 的文档可以在 javascriptmvc.com/docs.html#!DocumentJS 找到。

注意

Markdown 是一个文本到 HTML 转换工具,它允许你使用易于阅读和易于书写的纯文本格式编写(daringfireball.net/projects/markdown)。

DocumentJS 是如何工作的?

DocumentJS 的架构是围绕类型和标签组织的。

类型代表我们可能想要注释的 JavaScript 代码的每个相对独立的部分,例如类、函数(方法)或属性。

标签为类型提供额外的信息,例如参数和返回值。

DocumentJS 解析 JavaScript 和 Markdown 文件以生成用于由 JMVCDoc 渲染文档的 JSONP 文件。

编写文档

让我们在第一章 JavaScriptMVC 入门 中添加关于我们的 Todo 列表应用的文档,JavaScriptMVC 入门

要添加主文档页面,在 Todo/todo 目录中创建一个 Markdown 文件 todo.md,内容如下:

@page index TodoApp
@description TodoApp is simple todo application.

# TodoApp documentation

Here we can add some more documentation formatted by [Markdown][1]!

[1]: http://daringfireball.net/projects/markdown/syntax "Check out Markdown syntax"

然后,将这些文档块添加到 todo.js 文件中:

steal(
    'jquery/class',
    'jquery/model',
    'jquery/util/fixture',
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/controller/route',
    function ($) {

        /**
 * @class Todo
 * @parent index
 * @constructor
 * @author Wojciech Bednarski
 * Creates a new todo.
 */
        $.Model('Todo',{

 /**
 * @function findAll
 * Get all todos
 * @return {Array} an array contains objects with all todos
 */
                findAll: 'GET /todos',

 /**
 * @function findOne
 * Get todo by id
 * @return {Object} an objects contains single todo
 */
                findOne: 'GET /todos/{id}',

 /**
 * @function create
 * Create todo
 * @param {Object} todo
 * Todo object
 * @codestart
 * {name: 'read a book by Alfred Szklarski'}
 * @codeend
 *
 * @return {Object} an object contains newly created todo
 * @codestart
 * {
 *     id:   577,
 *     name: 'read a book by Alfred Szklarski'
 * }
 * @codeend
 *
 * ### Example:
 * @codestart
 * var todo = new Todo({name: 'read a book by Alfred Szklarski'});
 * todo.save(function (todo) {
 *     console.log(todo);
 * });
 * @codeend
 */
                create:  'POST /todos',

 /**
 * @function update
 * Update todo by id
 * @return {Object} an object contains updated todo
 */
                update:  'PUT /todos/{id}',

 /**
 * @function destroy
 * Destroy todo by id
 * @return {Object} an object contains destroyed todo
 */
                destroy: 'DELETE /todos/{id}'
            },
            {

            }
        );

        // Fixtures
        (function () {
            var TODOS = [
                // list of todos
                {
                    id:   1,
                    name: 'read The Good Parts'
                },
                {
                    id:   2,
                    name: 'read Pro Git'
                },
                {
                    id:   3,
                    name: 'read Programming Ruby'
                }
            ];

            // findAll
            $.fixture('GET /todos', function () {
                return [TODOS];
            });

            // findOne
            $.fixture('GET /todos/{id}', function (orig) {
                return TODOS[(+orig.data.id) - 1];
            });

            // create
            var id = 4;
            $.fixture('POST /todos', function () {
                return {
                    id: (id++)
                };
            });

            // update
            $.fixture('PUT /todos/{id}', function () {
                return {};
            });

            // destroy
            $.fixture('DELETE /todos/{id}', function () {
                return {};
            });
        }());

 /**
 * @class Todos
 * Creates a new Todos controller
 * @parent index
 * @constructor
 * @param {String} DOMElement DOM element
 * @return {Object}
 */
        $.Controller('Todos', {
            // init method is called when new instance is created
            'init': function (element, options) {
                this.element.html('todos.ejs', Todo.findAll());
            },

            // add event listener to strong element on click
            'li strong click': function (el, e) {
                // trigger custom event
                el.trigger('selected', el.closest('li').model());

                // log current model to the console
                console.log('li strong click', el.closest('.todo').model());
            },

            // add event listener to em element on click
            'li .destroy click': function (el, e) {
                // call destroy on the model to prevent memory leaking
                el.closest('.todo').model().destroy();
            },

            // add event listener to Todo model on destroyed
            '{Todo} destroyed': function (Todo, e, destroyedTodo) {
                // remove element from the DOM tree
                destroyedTodo.elements(this.element).remove();

                console.log('destroyed: ', destroyedTodo);
            }
        });

 /**
 * @class Routing
 * Creates application router
 * @parent index
 * @constructor
 * @param {String} DOMElement DOM element
 * @return {Object}
 */
        $.Controller('Routing', {
            init: function () {
                new Todos('#todos');
            },

            // the index page
            'route': function () {
                console.log('default route');
            },

            // handle URL witch hash
            ':id route': function (data) {
                Todo.findOne(data, $.proxy(function (todo) {
                    // increase font size for current todo item
                    todo.elements(this.element).animate({fontSize: '125%'}, 750);
                }, this));
            },

            // add event listener on selected
            '.todo selected':  function (el, e, todo) {
                // pass todo id as a parameter to the router
                $.route.attr('id', todo.id);
            }
        });

        // create new Routing controller instance
        new Routing(document.body);
    }
);

类型指令

类型指令代表你可能想要记录的 JavaScript 构造:

  • @page:这添加了一个独立页面

  • @attribute:这些是在对象上的文档值

  • @function:这些是文档函数

  • @class:这记录了一个类

  • @prototype:这被添加到之前的类或构造函数的原型函数中

  • @static:这被添加到之前的类或构造函数的静态函数中

  • @add:这会将文档添加到另一个文件中描述的类或构造函数

标签指令

标签指令为注释提供额外的信息:

  • @alias:这指定了类或构造函数的其他常用名称

  • @author:这指定了类的作者

  • @codestart:这指定了代码块的开始

  • @codeend:这指定了代码块的结束

  • @constructor:这记录了一个构造函数及其参数

  • @demo:这是应用程序演示的占位符

  • @description:这用于添加简短描述

  • @download: 这用于添加下载链接

  • @iframe: 这用于添加带有示例代码的 iframe

  • @hide: 这隐藏了类视图

  • @inherits: 这指定了类或构造函数继承的内容

  • @parent: 这指定了当前类型应该位于哪个父类型下

  • @param: 这指定了函数参数

  • @plugin: 这指定了一个通过它来获取对象的插件

  • @return: 这指定了函数返回的内容

  • @scope: 这强制当前类型开始作用域

  • @tag: 这指定了搜索的标签

  • @test: 这指定了测试用例的链接

  • @type: 这为当前注释的代码设置类型

  • @image: 这添加了一个图片

生成文档

要生成文档,我们只需要从命令行(在Todo目录内)运行doc命令:

$ ./documentjs/doc todo
PROCESSING SCRIPTS

 todo/todo.js
 todo/todo.md

GENERATING DOCS -> todo/docs

Using default page layout.  Overwrite by creating: todo/summary.ejs

这将生成文档,我们可以通过打开位于Todo/todo目录中的docs.html来浏览:

生成文档

我们可以通过更改summary.ejs模板文件来自定义文档的外观和感觉。只需将模板从documentjs/jmvcdoc复制到Todo/todo并修改它。

摘要

在本章中,我们学习了什么是 DocumentJS 以及如何编写和生成其文档。

每个程序员都应该养成的一个好习惯是,他们必须记录代码库并保持其更新。

第三章。FuncUnit

FuncUnit 是一个具有类似 jQuery 语法的功能测试框架。它建立在 QUnit 单元测试框架之上。

使用 FuncUnit,我们可以在 OS X、GNU/Linux 或 Windows 下的所有现代网络浏览器中运行测试。

编写测试非常简单快捷,尤其是如果读者熟悉 jQuery 语法和/或 QUnit 框架。

FuncUnit 允许我们在网络浏览器中运行测试,同时将其与自动化工具(如 Selenium)集成,或使用包装器(如 PhantomJS)从命令行运行测试。

FuncUnit 可以与构建工具集成,例如 Maven,作为构建过程的一部分运行。它还可以与持续集成工具集成,例如 Jenkins。有关 FuncUnit 的更多信息,可以在以下网址找到:

根据维基百科,功能测试被定义为如下:

功能测试是一种黑盒测试,其测试用例基于正在测试的软件组件的规范。通过提供输入并检查输出来测试函数,很少考虑内部程序结构。

在本章中,我们将概述 FuncUnit 功能测试框架,创建测试,并将它们运行在我们的 Todo 应用程序上。

注意

单元测试与功能测试

单元测试测试单个类似单元的方法或函数,而功能测试通过产品用户界面测试整个功能。

创建测试

创建测试是编写运行针对应用程序代码的代码,以确保代码符合其设计和预期行为。编写测试可以在早期开发阶段发现错误,从而节省时间。

让我们为 Todo 应用程序添加第一个测试:

  1. Todo/todo 文件夹中,创建一个名为 tests 的文件夹。在其内部,创建一个名为 todo_test.html 的文件,包含以下内容:

    <!doctype html>
    
    <html>
      <head>
     <title>Todo List - tests</title>
        <meta charset="UTF-8" />
        <link rel="stylesheet" href="../../funcunit/qunit/qunit.css" />
      </head>
      <body>
        <h1 id="qunit-header">AutoSuggest Test Suite</h1>
    
        <h2 id="qunit-banner"></h2>
    
        <div id="qunit-testrunner-toolbar"></div>
        <h2 id="qunit-userAgent"></h2>
        <ol id="qunit-tests"></ol>
     <script src="img/todo_test.js"></script>	
      </body>
    </html>
    

    此文件提供了一个页面骨架,其中包含 FuncUnit 输出,对于所有未来的测试用例都保持不变——将其用作模板;只有标题和测试文件的路径会更改(高亮代码)。

  2. 接下来,创建一个包含以下内容的 todo_test.js 文件:

    steal(
      'funcunit',
      function ($) {
    
        module('Todo app', {
          setup: function () {
            S.open('//todo/todo.html');
          }
        });
    
        test('page has #todos placeholder', function () {
          ok(S('body > #todos').size(), 'The #todo is child of the body');
        });
      }
    );
    

模块

模块签名由 module(name, [lifecycle]); 提供。

模块方法来自 QUnit 项目,它提供了将测试划分为模块的功能。

第一个参数是一个包含模块名称的字符串。第二个参数是一个包含两个可能方法的对象,setupteardownsetup 回调方法在每个测试之前运行,而 teardown 在模块中的每个测试之后运行。

打开

open(path, [success], [timeout]) 提供了一个开放签名。

在我们的示例中,我们使用了 open 方法来打开给定的 URL 并对其运行测试 'page has #todos placeholder'

测试

测试签名由 test(name, [expected], test) 提供。

此方法运行实际的测试代码。

第一个参数是测试的名称,而第二个是实际运行代码所必需的。

Ok

一个好的签名由 ok(state, [message]) 提供。

ok 方法是一个布尔断言。如果第一个参数评估为真,则测试通过。第二个参数是可选的,它描述了测试。

S

S 主要是 jQuery 短路 $ 的副本,通过 FuncUnit 特定方法进行了扩展。

运行测试

运行测试的方法有很多。使用网络浏览器,命令行工具将在测试执行完成后打开并关闭浏览器。我们还可以使用独立的 JavaScript 环境运行测试。

网络浏览器

禁用弹出窗口阻止程序,并在浏览器中打开 tests/todo_test.html。测试将打开 Todo 应用程序并对其运行测试用例。之后,你应该能够看到以下截图类似的内容:

网络浏览器

Selenium

Todo 应用程序目录中运行以下命令:

$ ./js funcunit/run selenium todo/tests/todo_test.html

此命令将打开 Firefox,运行与网络浏览器示例中完全相同的测试,关闭浏览器,并在命令行上打印结果。

PhantomJS

使用 PhantomJS 运行测试是一个更快的解决方案,因为它不会启动网络浏览器。

执行以下命令以运行测试:

$ ./js funcunit/run phantomjs todo/tests/todo_test.html

此前命令将在 PhantomJS 环境中运行测试,因此它不会像上一个案例那样打开任何网络浏览器。但是,它将在 WebKit 包装器内运行测试。

命令行输出应类似于以下内容:

Opening file:///Users/wbednarski/Sites/DEV/JMVC/Todo/todo/tests/todo_test.html
starting steal.browser.phantomjs
steal.js INFO: Opening //todo/todo.html
steal.js INFO: using a dynamic fixture for GET /todos
steal.js INFO: ajax request to todos.ejs, no fixture found
steal.js INFO: ajax request to todos.ejs, no fixture found
default route

Todo app
 page has #todos placeholder
  [x] The #todo is child of the body

Time: 3 seconds, Memory: 81.06 MB

OK (1 tests, 0 assertions)

EnvJS

运行测试的另一种方式是使用用 JavaScript 编写的 EnvJS 模拟浏览器环境。

EnvJS 只能用于运行单元测试,因为它没有准确实现事件模拟。

通过执行以下命令来运行测试:

$ ./js funcunit/run envjs todo/tests/todo_test.html

集成

集成是可能的,可以使用流行的构建或 CI 工具,如 Jenkins 或 Maven:

摘要

如我们所见,FuncUnit 是一个易于使用且功能强大的测试框架。

编写测试用例既快又简单。能够在多种方式下运行它们以及与自动化和构建工具集成,使 FuncUnit 成为一个可靠的工具。

现在,我们没有借口不编写测试了。

第四章:jQueryMX

jQueryMX 是一组 jQuery 库,提供了实现和组织大型 JavaScript 应用程序所需的功能。

它提供了经典的继承模拟,模型-视图-控制器层以提供逻辑上分离的代码库。它还提供了有用的 DOM 辅助工具、自定义事件和语言辅助工具。

在本章中,我们将介绍最常见或最有趣的例子。

注意

要获取完整的插件列表,请访问 javascriptmvc.com/docs.html#!jquerymx

我们使用现有的 Todo 应用程序文件夹结构来尝试 jQueryMX 插件。

Todo 文件夹中创建一个 jquerymx_playground 文件夹,包含两个文件,使用以下代码片段:

<!doctype html>

<html>
<head>
    <title>jQueryMX playground</title>
    <meta charset="UTF-8"/>
</head>
<body>

<script src="img/jquerymx_playground_0.js"></script>
</body>
</html>

当代码片段指示在控制台中运行时,意味着将示例粘贴并执行在打开的 index.html 页面上的 Google Chrome 控制台。我们可以使用任何网络浏览器控制台,如 Firebug,然而,目前 Google Chrome(和 Safari)似乎是最好的,并且具有非常方便的代码补全功能。

$.Class

$.Class 提供了基于 John Resig 的 Simple JavaScript Inheritance 的经典继承模拟,该模拟可在 ejohn.org/blog/simple-javascript-inheritance/ 找到。

提示

类签名是 $.Class( [NAME , STATIC,] PROTOTYPE ) -> Class

class 方法对所有类实例都可用,而 instance 方法仅对特定实例可用。

让我们在文件 jquerymx_playground_0.js 中编写一些示例:

steal(
    'jquery/class',
    function ($) {
        $.Class('Bank.Account', {
                setup: function () {
                    console.log('Bank.Account class: setup');
                },

                init: function () {
                    console.log('Bank.Account class: init');
                },

                getType: function () {
                    return 'Bank.Account class method';
                }
            },
            {
                setup: function () {
                    console.log('Bank.Account instance: setup');
                },

                init:  function () {
                    console.log('Bank.Account instance: init');
                },

                getType: function () {
                    return 'Bank.Account instance method';
                }
            }
        );

        Bank.Account('Bank.Account.SavingsAccount', {
                setup: function () {
                    console.log('Bank.Account.SavingsAccountclass: setup');
                },

                init: function () {
                    console.log('Bank.Account.SavingsAccountclass: init');
                }
            },
            {
                setup: function () {
                    console.log('Bank.Account.SavingsAccountinstance: setup');
                },

                init: function () {
                    console.log('Bank.Account.SavingsAccountinstance: init');
                },

//                getExtendedType: function () {
//                    return 'Hello ' + this.getType();
//                },

                getType: function () {
                    return 'Hello ' + this._super();
                }
            }
        );

//        console.log('instantiate: acc, savingAcc');
//        window.acc = new Bank.Account();
//        window.savingAcc = new Bank.Account.SavingsAccount();
    }
);

在控制台中,我们可以看到类已被创建。让我们按照以下方式创建一些实例:

var acc = new Bank.Account();
var savingAcc = new Bank.Account.SavingAccount();

我们可以按照以下方式执行实例方法:

acc.getType();
savingAcc.getType();

在以下章节中,让我们分解代码并看看这里发生了什么。

第一个参数

使用 $.Class,我们创建了一个名为 Account 的新类,将字符串 Bank.Account 作为第一个参数传递。通过使用点表示法,我们创建了一个命名空间 Bank。这就是为什么我们创建了一个名为 Bank.AccountAccount 类的新实例。在这种情况下,Bank 只是一个空对象,帮助我们创建整洁的应用程序对象结构。

例如,可以有一个替代命名空间,如 CompanyName.Product.SomeClass

第二个参数

作为第二个参数,我们传递了一个具有属性的对象,这些属性是所有类实例共享的类属性。

在我们的案例中,Account 类的 getType 类方法在 SavingAccount 类中可用。因此,我们可以在控制台中输入以下内容:

Bank.Account.SavingsAccount.getType();

第三个参数

作为第三个参数,我们传递了一个具有属性的对象,这些属性是所有实例共享的实例属性。因此,我们在控制台中输入以下命令:

savingAcc.getType();

方法重写

getType 实例方法示例中,我们可以看到如何在子对象中重写方法。

SavingAccount中,我们通过向同名祖先方法添加额外的Hello字符串来覆盖getType方法,并使用以下方式调用祖先方法:

this._super();

如果我们不希望使用相同的名称,可以使用以下:

getExtendedType: function () {
    return 'Hello ' + this.getType();
}

生命周期

在类和实例中,我们都可以使用预定义的方法setupinit

如果它存在,它总是会被调用,因此没有必要手动调用它。

setup方法首先被调用,然后是init方法。在大多数情况下,不需要使用setup方法。

$.Model

$.Model 是应用程序数据层。它提供了一种简单的方法来连接提供 RESTful API 的服务,监听数据变化,并将 HTML 元素绑定到模型、延迟器和验证。

\(.Model 非常方便;我们不需要手动使用 jQuery 的 Ajax 方法编写 XHR 调用。我们可以使用\).Model 映射我们的后端 API,然后使用其方法从服务器拉取/推送数据。

我们可以使用$.Model.List列表来组织$.Models,这与 Backbone.js 的集合(backbonejs.org/#Collection)类似。

让我们在文件jquerymx_playground_1.js中编写一些代码:

steal(
    'jquery/model',
    'jquery/dom/fixture',
    function ($) {
        $.Model('AccountModel', {
                findAll: 'GET /accounts',
                findOne: 'GET /accounts/{id}',
                create:  'POST /accounts',
                update:  'PUT /accounts/{id}',
                destroy: 'DELETE /accounts/{id}'
            },
            {

            }
        );

        // Fixtures
        (function () {
            var accounts = [
                {
                    id: 1,
                    type: 'USD'
                },
                {
                    id: 2,
                    type: 'EUR'
                }
                ];

            // findAll
            $.fixture('GET /accounts', function () {
                return [accounts];
            });

            // findOne
            $.fixture('GET /accounts/{id}', function () {
                return accounts;
            });

            // create
            $.fixture('POST /accounts', function () {
                return {};
            });

            // update
            $.fixture('PUT /accounts/{id}', function (id, acc) {
                return acc;
            });

            // destroy
            $.fixture('DELETE /accounts/{id}', function () {
                return {};
            });
        }());

        AccountModel.findAll({}, function (accounts) {
            $.each(accounts, function (i, acc) {
                $('<p>').model(acc).text(acc.type).appendTo('body');
            });
        });

        AccountModel.bind('updated', function (e, acc) {
acc.elements($('body')).remove();
        });

        AccountModel.bind('created', function (e, acc) {
            console.log('AccountModel.bind: event: ', e,'Account: ', acc);
        });
    }
);

让我们分解这段代码,看看这里发生了什么:

  • $.Model开始的代码负责将 API 映射到我们的模型。

  • $.fixtures开头的行负责模拟服务器响应。当我们需要在没有准备好或不可用的 Web 服务器 API 的情况下开始开发时,固定值非常有用。

  • model类中的bind方法负责绑定模型方法updatecreate。我们可以尝试使用它们,通过在AccountModel实例上执行这些方法来查看它们在浏览器控制台中的工作方式。

$.View

$.View 是一个客户端模板解决方案。它使用数据填充 HTML 模板。

它包含四个预包装的模板引擎,可以从以下网站下载:

通过使用$.View.register很容易扩展它。

模板可以嵌入到 HTML 文档中,或者从外部文件同步或异步加载。$.View 支持生产构建中的模板缓存和打包。

嵌入

模板如下嵌入到 HTML 文档中:

让我们将以下代码复制到index.html文件中:

<script type='text/ejs' id="accounts">
    <p>JavaScriptMVC is <%= message %></p>
</script>

还可以将以下代码复制到文件jquerymx_playground_2.js中:

steal(
    'jquery/view',
    'jquery/view/ejs',
    function ($) {

    }
);

在控制台中,输入以下内容:

$('body').html('accounts', {message: 'Awesome'});

因此,应该创建以下 DOM 节点:

<p>JavaScriptMVC is Awesome</p>

外部

这种使用模板的方法是最常见的,因为它允许更好地组织项目文件的结构:

创建一个名为message.ejs的文件,并将之前的模板复制到其中。文件内容应如下所示:

<p>JavaScriptMVC is <%= message %></p>

在控制台中输入以下内容:

$('body').html('message.ejs', {message: 'Awesome'});

具有 message 属性的 object 被传递到 HTML 方法中,该方法使用message.ejs文件将文本"Awesome"渲染到<%= message %>的位置,并将其附加到 body DOM 节点中。

结果应该与内嵌的结果相同。

子模板

在模板内部我们可以嵌入另一个模板,如下所示:

<%= $.View('sub-message.ejs', message); %>

$.Controller

$.Controller插件帮助创建一个有组织、无内存泄漏的 JavaScript 代码。

使用$.Controller的一个很好的例子是来自第一章的Todos控制器,使用 JavaScriptMVC 入门,如下所示:

$.Controller('Todos', {
            // init method is called when new instance is created
            'init': function (element, options) {
                this.element.html('todos.ejs', Todo.findAll());
            },

            // add event listener to strong element on click
            'li strong click': function (el, e) {
                // trigger custom event
                el.trigger('selected', el.closest('li').model());

                // log current model to the console
                console.log('li strong click', el.closest('.todo').model());
            },

            // add event listener to em element on click
            'li .destroy click': function (el, e) {
                // call destroy on the model to prevent memoryleaking
                el.closest('.todo').model().destroy();
            },

            // add event listener to Todo model on destroyed
            '{Todo} destroyed': function (Todo, e, destroyedTodo) {
                // remove element from the DOM tree
                destroyedTodo.elements(this.element).remove();

                console.log('destroyed: ', destroyedTodo);
            }
        });

DOM 助手

DOM 助手扩展为 DOM 添加了一组有用的插件。它们将在以下章节中描述。

$.cookie插件包含用于管理 cookie 的有用方法。

将以下代码粘贴到jquerymx_cookie.js文件中:

steal(
    'jquery/dom/cookie',
    function ($) {

    }
);

我们可以使用以下命令在控制台中创建一个 cookie:

$.cookie('CookieName', 'CookieValue');

在资源标签中我们可以看到已经创建了 cookie。

我们可以使用 cookie 名称来获取 cookie,如下所示:

$.cookie('CookieName');

我们也可以使用以下命令删除 cookie:

$.cookie('CookieName', null);

$.fn.compare

$.fn.compare插件比较两个节点,并返回一个数字,描述它们相对于彼此的位置。

将以下代码粘贴到jquerymx_compare.js文件中:

steal(
    'jquery/dom/compare',
    function ($) {
        $('body').append('<p>paragraph</p><strong>strong</strong>');
    }
);

在控制台中运行以下命令:

$('p').compare($('strong'));

接下来,运行以下命令:

$('strong').compare($('p'));

在第一种情况下我们应该得到4,在第二种情况下得到2

这里数字的含义如下:

  • 0: 元素是相同的

  • 1: 节点在不同的文档中(或一个在文档之外)

  • 2: strongp之前

  • 4: pstrong之前

  • 8: strong包含p

  • 16: p包含strong

$.fn.selection

$.fn.selection插件设置或获取任何元素上的当前文本选择。

将以下代码粘贴到jquerymx_selection.js文件中:

steal(
    'jquery/dom/selection',
    function ($) {
        $('body').append('<p>Hello from paragraph!</p>');
    }
);

在控制台中运行以下命令:

$('p').selection();

它应该返回null

现在,选择文本的一部分并再次运行命令,它应该返回以下对象:

{
 end: 15,
 start: 4
}

要设置选择,使用以下命令:

$('p').selection(6, 10);

$.fn.within

$.fn.within插件返回给定位置内的元素。

将以下代码粘贴到jquerymx_within.js文件中:

steal(
    'jquery/dom/within',
    function ($) {
        $('body').append('<p>Hello from paragraph!</p>');
    }
);

在控制台中运行以下命令:

$('p').within(30, 20);

它应该返回一个包含所有左位置为 30 px 和上位置为 20 px 的p元素的数组。

$.Range

$.Range插件包含对文本选择进行操作的有用方法,以支持创建、移动和比较选择。

将以下代码粘贴到jquerymx_range.js文件中:

steal(
    'jquery/dom/range',
    function ($) {
        $('body').append('<p>Hello from paragraph!</p>');
    }
);

在控制台中运行以下命令:

$.Range.current();

要获取当前范围,选择文本的一部分并再次执行代码,然后比较返回的对象。

要获取当前选择文本,请在控制台中运行以下命令:

$.Range.current().toString();

$.route

$.route 插件包含用于管理应用程序状态的有用方法。

将以下代码粘贴到 jquerymx_route.js 文件中:

steal(
    'jquery/dom/route',
    function ($) {
        $.route.bind('change', function (e, attr, how, newVal, oldVal) {
            console.log('event: ', e, '| attribute changes: ',attr, '| how changes: ', how, '| new value: ',newVal, '| old value: ', oldVal);
        });
    }
);

在 URL 的末尾,输入以下内容:

#!&type=UTC

然后,输入以下命令并观察控制台输出:

#!&type=GTM

另一个路由示例可以在 Todo 应用程序的 第一章 使用 JavaScriptMVC 入门 中找到。

特殊事件

特殊事件扩展添加了一组特殊事件插件。

$.Drag 和 $.Drop

$.Drag$.Drop 插件包含拖放事件。

将以下代码粘贴到 jquerymx_draganddrop.js 文件中:

steal(
    'jquery/event/drag',
    'jquery/event/drag/limit',
    'jquery/event/drag/scroll',
    'jquery/event/drag/step',
    'jquery/event/drop',
    function ($) {

        $('body').append('<p>Drag me, but not too far...</p>');

        $('p').bind('dragmove', function (e, drag) {
            if (drag.location.top() > 150 || drag.location.left() > 450) {
                console.log('limiter');
                e.preventDefault();
            }
        });
    }
);

语言辅助工具

语言辅助工具是一组 jQuery 插件。它们将在以下章节中描述。

$.Object

$.Object 插件包含以下三个有用方法:

  • same:它比较两个对象

  • subset:它检查一个对象是否是另一个对象的集合

  • subsets:它返回对象的子集

same

same 方法可以比较两个对象。它支持嵌套对象。我们还可以指定比较是否区分大小写或是否跳过特定属性的比较。

将以下代码粘贴到 jquerymx_object.js 文件中:

steal(
    'jquery/lang/object',
    function ($) {
        window.object_1 = {
            property_1: 'foo',
            property_2: {
                property_1: 'bar',
                property_2: {
                    property_1: 'Hello JMVC!'
                }
            }
        };

        window.object_2 = {
            property_1: 'foo',
            property_2: {
                property_1: 'bar',
                property_2: {
                    property_1: 'HELLO JMVC!'
                }
            }
        };
    }
);

在控制台中运行以下命令:

$.Object.same(object_1, object_2);

它应该返回 false

现在尝试忽略大小写:

$.Object.same(object_1, object_2, {property_2: 'i'});

它应该返回 true,因为 property_2 及其所有子项都与忽略大小写标志进行比较。

要忽略特定属性的区分大小写,我们可以指定如下:

$.Object.same(object_1, object_2, {property_2: {property_2: { property_1: 'i'}}});

结果也应该为 true

$.Observe

$.Observe 插件为 JavaScript 对象和数组提供观察者模式。

将以下代码粘贴到 jquerymx_observe.js 文件中:

steal(
    'jquery/lang/observe',
    'jquery/lang/observe/delegate',
    function ($) {
        window.data = {
            accNumber: {
                iban: 'SWISSQX',
                number: 6987687
            },
            owner: {
                fName: 'Nicolaus',
                lName: 'Copernicus'
            }
        };

        window.oData = new $.Observe(data);

        oData.bind('change', function (e, attr, how, newVal, oldVal) {
            console.log('event: ', e, '| attribute changes: ', attr, '| how changes: ', how, '| new value: ', newVal, '| old value: ', oldVal);
        });
    }
);

在控制台中运行以下命令:

oData.attr('accNumber.number');

接下来,运行以下命令:

data.accNumber.number;

应该显示相同的数字。

使用以下命令更改 number 属性的值:

oData.attr('accNumber.number', 123456);

由于我们将匿名函数绑定到 change 事件,该事件在任何可观察对象属性发生变化时发出,因此应该显示带有所有传递信息的 console.log

请注意,oData 是数据的副本,因此命令:

oData.attr('accNumber.number');

与以下内容不同:

data.accNumber.number;

$.String

$.String 插件包含有用的字符串方法。

将以下代码粘贴到 jquerymx_string.js 文件中:

steal(
    'jquery/lang/string',
    'jquery/lang/string/deparam',
    'jquery/lang/string/rsplit',
    function ($) {

    }
);

deparam

此方法将 URL 参数转换为对象字面量。

在控制台中运行以下命令:

$.String.deparam('en=1&home=a3373dsf6wfd&page[main]=uy7887d');

它应该将字符串转换为以下对象:

{
    en: '1',
    home: 'a3373dsf6wfd',
    page: {
        main: 'uy7887d'
    }
}

$.toJSON

$.toJSON 插件包含有用的对象方法。

将以下代码粘贴到 jquerymx_tojson.js 文件中:

steal(
    'jquery/lang/json',
    function ($) {
        window.object_1 = {
            property_1: 'foo',
            property_2: {
                property_1: 'bar',
                property_2: {
                    property_1: 'Hello JMVC!'
                }
            }
        };
    }
);

在控制台中运行以下命令:

$.toJSON(object_1);

它应该返回给定对象的 JSON 表示形式。

$.Vector

$.Vector 插件包含用于创建和操作向量的有用方法。

让我们将以下代码粘贴到 jquerymx_vector.js 文件中:

steal(
    'jquery/lang/vector',
    function ($) {

    }
);

在控制台中,运行以下命令:

new jQuery.Vector(1,2);

它应该返回一个新的 Vector 实例。

摘要

在本章中,我们学习了 jQueryMX 插件能提供什么,以及我们如何使用它们来使我们的日常编码更加高效。

在下一章中,我们将学习关于依赖管理工具 StealJS。

第五章. StealJS

StealJS是一个独立的代码管理和打包工具,它允许我们将 JavaScript 和其他文件类型加载到应用程序中,连接多个 JavaScript 或 CSS 文件,并压缩其内容。StealJS 提供跨浏览器消息日志、代码生成器和简单的包管理工具。

在本章中,我们将介绍 StealJS 的所有功能。

注意

StealJS 需要 Java 1.6 或更高版本。

依赖管理

依赖管理是一个工具,它提供了一种有组织的方式来管理软件组件协同工作作为一个单一系统。

以下是一些 StealJS 的关键特性:

  • 只加载单个文件一次

  • 从不同领域加载文件

  • 加载 JavaScript 和 CoffeeScript

  • 加载 CSS less

我们在前面章节中多次使用了 StealJS。现在让我们更详细地看看它。

使用 StealJS,我们可以按如下方式加载文件:

steal(
    'file_one',
    'file_two',
    function ($) {

    }
);

这些文件并行且随机顺序加载。如果file_twofile_one中有依赖,我们可以在开始获取file_two之前等待file_one,如下所示:

steal(
    'file_one').then(

    'file_two',
    function ($) {

    }
);

记录器

steal.dev提供了两个类似于流行console.log()函数的日志功能,并自动从生产构建中移除它们。

我们可以这样使用它:

steal.dev.log('See me in the console');
steal.dev.warn('Me too!');

注意

所有日志都从生产构建中移除。

代码清理器

steal.clean美化 JavaScript 代码,并使用JSLint代码质量工具进行检查:

我们可以使用cleanjs命令来美化单个文件中的代码:

$ ./js steal/cleanjs todo/todo.js

或者我们项目中的所有文件:

$ ./js steal/cleanjs todo/todo.html

要对 JSLint 运行我们的代码,请添加-jslint true参数:

$ ./js steal/cleanjs todo/todo.js -jslint true

我们可以通过添加类似以下注释来忽略文件被清理:

//!steal-clean

连接和压缩

steal.build将 CSS 和 JavaScript 文件压缩和连接到单个或多个文件中。默认情况下,它使用 Google Closure compressor。

由于它通过 Envjs 打开应用程序,它可以针对不使用 StealJS 的应用程序运行。

要制作我们的Todo应用程序的生产就绪文件,我们可以运行以下命令:

$ ./js steal/buildjs todo/todo.html -to todo_prod

我们可以对 URL 运行脚本:

$ ./js steal/buildjs http://YOUR_SERVER/todo/todo.html -to todo_prod

摘要

在本章中,我们学习了如何将文件加载到项目中,使用了在生产构建中移除的跨浏览器日志系统,清理了代码并制作了一个生产就绪的应用程序。

第六章:构建应用程序

在过去的几章中,我们学习了什么是 JavaScriptMVC,如何安装它,并了解了其组件。

现在是任何开发者最激动人心的章节。我们将构建一个真实世界的应用程序。由于本书的范围限制,我们不会编写后端 API 设置服务器等,而是将使用浏览器存储。

由于 JavaScriptMVC 中的层分离,这可以通过更改模型中的代码轻松完成,将应用程序持久层从浏览器存储选项切换到任何后端语言、框架或系统,例如 Sinatra、Ruby on Rails、Django 和 Node.js。

本章的目标是展示如何从概念到设计、实现、文档和测试构建一个真实世界的应用程序。我们将开发一个真正有用的应用程序,对读者有帮助,并且可以轻松定制以满足读者的需求。

自由职业者的时间跟踪和开票

本章我们将构建的应用程序称为自由职业者的时间跟踪和开票;让我们简称它为 TTI。

应用开发将从这里开始。我们不会编写完整的代码库,因为这会太大,无法在这里容纳。这就像一个家庭作业练习,当学生在大学开始写应用程序并在家里完成它们时。要有创意!

规划

好的,所以我们将编写一个应用程序。现在是我们回答最重要的问题的时候了:我们的应用程序将要解决什么问题?

我们可以清楚地识别出两个主要的应用程序领域:

  • 跟踪我们在任务上花费的时间

  • 制作发票

让我们把应用程序的主要区域分解成一个功能列表,如下所示:

  • 客户列表

  • 时间跟踪器

    • 跟踪时间

    • 固定成本任务

  • 报告

    • 每日

    • 每周

  • 统计数据

    • 每月

    • 每年

  • 开票

  • 导出和导入数据

一个功能列表将帮助我们制定开发计划。现在我们可以考虑完成每个功能所需的时间。我们可以只用日历来写下我们的估计,或者使用许多免费的问题跟踪工具之一,例如trello.com/trac.edgewall.org/

理想解决方案是使用一种方法,如 Scrum—en.wikipedia.org/wiki/Scrum_(development)或商业中最优秀的任务跟踪工具之一,JIRA—en.wikipedia.org/wiki/JIRA

准备线框

下一步是准备应用程序线框。这是应用程序开发周期中的一个非常重要的步骤。它允许我们快速绘制不同页面的应用程序界面,以及非常快速地重新设计页面,并在未来的开发中节省时间。一旦我们开始编写代码,任何更改都将比更改线框更困难且成本效益更低。

下一步是创建原型和线框。然而,我们这里没有图形设计师,也没有客户来展示业务逻辑,最终这超出了本书的范围,所以我们直接进入下一步。

线框通常是应用程序中使用的组件的基本草图,用于展示用户界面和应用程序功能。

原型是线框的下一级,基本上包含了线框上所有我们能找到的内容,但它们是在实际设计中。

原型是半功能性的应用程序,用于展示业务逻辑。

我们可以使用一张纸和一支铅笔来创建线框;很多人更喜欢这种方式。有许多不同的软件可以帮助我们在这一步。我将使用 Balsamiq Mockups,但实际上任何工具都适用。

为了更好地了解 TTI 应用程序,让我们看看线框:

由于本书的定位是纵向而网页浏览器的定位是横向,因此要求读者从不同的角度查看以下线框。

下面的线框显示了时间追踪器的主要页面。

主要菜单位于左上角,允许我们在主要应用程序功能之间切换。

面包屑导航位于顶部中央,允许我们轻松地指示我们当前在应用程序的哪个部分。

设置导出/导入数据标签位于右上角。

时间追踪器位于中心位置,有两个主要标签:活动任务存档任务。每个任务都有字段:小时数成本任务 ID描述备注添加新任务按钮位于底部,允许我们添加新任务。当鼠标悬停在任务上时,存档按钮可见。要编辑任务,请双击它。时间追踪器页面的 URL 是/timetracker

准备线框

下面的线框显示了发票的主要页面。URL 是/invoice

准备线框

下面的线框显示了客户端的主要页面。URL 是/clients

准备线框

设置项目

我们假设读者已经安装了网络服务器,例如 Apache 或 Nginx。在服务器工作目录中,我们需要创建TTI文件夹。另一个选项是使用专门为本书创建并由 Vagrant 提供的环境,可在github.com/wbednarski/JavaScriptMVC_kick-starter找到。

在这个文件夹中,我们将初始化 Git 仓库以跟踪所有更改,安装 JavaScriptMVC,并创建应用程序结构。

在 VCS 下跟踪更改

从一开始就保持所有项目文件在版本控制系统下是一个好主意。这样做的原因非常简单,并且对未来的开发有益——我们可以轻松地回滚任何更改并跟踪它们。

使用去中心化的 VCS 比集中式 VCS 具有无价的优点,因为我们可以在不推送的情况下提交更改,因此即使在代码库中进行小的更改后,我们也可以频繁提交。另一个好的做法是使用一个分支来处理一个特性。

在这本书中我们将使用 Git,但实际上任何分布式版本控制系统DVCS)都是好的。Mercurial 是另一个流行的 DVCS。

创建新的 Git 仓库、添加所有文件并提交它们的以下步骤应该执行:

  1. TTI目录下,输入以下命令安装 JavaScriptMVC:

    $ git init
    $ git submodule add git://github.com/jupiterjs/steal.git
    $ git submodule add git://github.com/jupiterjs/documentjs.git
    $ git submodule add git://github.com/jupiterjs/funcunit.git
    $ git submodule add git://github.com/jupiterjs/jquerymx.git jquery
    
    
  2. 使用以下命令安装和更新 JavaScriptMVC 子模块:

    $ git submodule init
    $ git submodule update
    
    
  3. 使用以下命令安装 Syn:

    $ cd funcunit
    $ git submodule init
    $ git submodule update
    
    
  4. js命令移动到项目的根目录(从根目录运行):

    $ ./steal/js steal/make.js
    
    

    默认情况下,所有仓库都在 master 分支上。让我们切换到 JavaScriptMVC 的最新版本,本书编写时为 3.2.2。

  5. 在所有子模块目录中,输入以下命令:

    $ git checkout v3.2.2
    
    
  6. TTI目录下创建我们的应用程序目录tti并将其添加到 Git 中。

    $ mkdir tti
    $ git add .
    $ git commit -m "initial commit"
    
    

注意

如果读者希望将代码库副本保留在服务器上,他们可以使用github.combitbucket.org提供的免费代码托管解决方案来完成此操作。

我们将要开发的全部代码都将放置在tti文件夹中。

应用程序结构

我们的应用程序结构将类似于以下层次结构:

TTI/
   |
   |tti/
   |   |controllers/
   |   |
   |   |docs/
   |   |
   |   |models/
   |   |
   |   |tests/
   |   |     |unit/
   |   |     |    |models/
   |   |     | 
   |   |     |functional/
   |   |
   |   |views/
   |         |styles/
   |         |      |css/
   |         |      |
   |         |      |sass/
   |         |
   |         |templates/
   |                   |tasks
   |                   |
   |                   |clients
   |
   |vendors/
           |jquery_ui/
           |
           |pouchdb/

IndexedDB

由于本地存储对我们应用程序来说太简单,而 Web SQL 数据库已被弃用,因此自然的选择是 IndexedDB。

在根级别创建vendors目录以存储所有第三方代码、插件等。

下载并将 PouchDB 复制到vendors目录库,该库为 IndexedDB 提供了良好的跨浏览器 API。您可以从以下位置下载 PouchDB:

创建模型

让我们在models目录下创建一个task.js文件。在Task模型中,我们将保留所有与任务相关的 CRUD 方法,这些方法在本地数据库上操作。

steal(
    'jquery/model',
    'vendors/pouchdb.js',

    function ($) {
        'use strict'

        // local variable to keep reference to time-tracker database
        var db;

        /**
         * @class TTI.Models.Task
         * @parent index
         * @constructor
         * @author Wojciech Bednarski
         */
        $.Model('TTI.Models.Task', {

                /**
                 * @function init
                 * @hide
                 * Creates database time-tracker or get it if exists
                 */
                init: function () {
                    Pouch('idb://time-tracker', function (err, timeTracker) {
                        db = timeTracker;

                        console.log('TTI.Models.Task.init() | idb://time-tracker | err:', err, 'db:', db);
                    })
                },

init方法负责创建一个time-tracker数据库或获取其引用(如果它已存在)。idb://协议告诉 PouchDB 使用IndexedDB作为存储选项。

                /**
                 * @function findAll
                 * Get all tasks
                 * @return {Object} an object contains objects with all tasks
                 *
                 * ### Example:
                 * @codestart
                 * TTI.Models.Task.findAll(function (tasks) {
                 *      // do something with tasks
                 * },
                 * function (error) {
                 *      // handle error here
                 * });
                 * task.save(function (task) {
                 *     console.log(task);
                 * });
                 * @codeend
                 */
                findAll: function (success, error) {
                    return db.allDocs(
                        {
                            include_docs: true // this is needed to return not only task ID but task it self
                        },
                        function (err, response) {
                            console.log('TTI.Models.Task.findAll() | GET | err:', err, 'client:', response);

                            if (response) {
                                success(response);
                            }
                            else if (err) {
                                error(err);
                            }
                        }
                    );
                },

findAll方法负责从我们的数据库中检索包含所有条目的对象。读者可以查看前述代码列表中的注释中的示例用法。

                /**
                 * @function findOne
                 * Find task by given ID
                 * @param {String} task ID
                 * Task object
                 * @codestart
                 * String (UUID)
                 * @codeend
                 *
                 * @return {Object} an object contains requested task
                 * @codestart
                 * {
                 *     id: String (UUID),
                 *     hours: Number,
                 *     cost: {
                 *          rate: Number,
                 *          total: Number
                 *     },
                 *     taskID: String,
                 *     description: String,
                 *     note: String
                 * }
                 * @codeend
                 *
                 * ### Example:
                 * @codestart
                 * TTI.Models.Task.findOne('UUID', function (success, error) {
                 *      // code goes here
                 * });
                 * @codeend
                 */
                findOne: function (id, success, error) {
                    return db.get(id, function (err, doc) {

                        if (doc) {
                            success(doc);
                        }
                        else if (err) {
                            error(err);
                        }

                    });
                },

findOne方法负责从我们的数据库中检索具有特定条目的对象。读者可以查看前述代码列表中的注释中的示例用法。

                /**
                 * @function create
                 * Create new task
                 * @param {Object} task
                 * Task object
                 * @codestart
                 * {
                 *     hours: Number,
                 *     cost: {
                 *          rate: Number,
                 *          total: Number
                 *     },
                 *     taskID: String,
                 *     description: String,
                 *     note: String
                 * }
                 *
                 * {
                 *      hours: 7,
                 *      cost: {
                 *          rate: 100,
                 *          total: 700
                 *      },
                 *      taskID: 'JIRA-2789',
                 *      description: 'Implement new awesome feature!',
                 *      note: ''
                 *  }
                 * @codeend
                 *
                 * @return {Object} an object contains newly created task UUID
                 * @codestart
                 * {
                 *      id: "8D812FF6-4B96-4D73-8D18-01FACEF33531"
                 *      ok: true
                 *      rev: "1-c5a4055b6c3edac099083cc0b485d4e3"
                 * }
                 * @codeend
                 *
                 * ### Example:
                 * @codestart
                 * var task = new TTI.Models.Task({ task object goes here });
                 * task.save(function (task) {
                 *     console.log(task);
                 * });
                 * @codeend
                 */
                create: function (task, success, error) {
                    return db.post(task, function (err, response) {
                        console.log('TTI.Models.Task.create() | POST | err:', err, 'client:', response);

                        if (response) {
                            success(response);
                        }
                        else if (err) {
                            error(err);
                        }
                    });
                },

create方法负责在我们的数据库中创建一个新的条目。读者可以查看前述代码列表中的注释中的示例用法。

                /**
                 * @function update
                 * Update task by given ID
                 * @param {Object} task
                 * Task object
                 * @codestart
                 * {
                 *      _id: String (UUID),
                 *      hours: Number,
                 *      cost: {
                 *          rate: Number,
                 *          total: Number
                 *      },
                 *      taskID: String,
                 *      description: String,
                 *      note: String
                 * }
                 * @codeend
                 *
                 * @return {Object} an object contains updated task UUID
                 * @codestart
                 * {
                 *      id: "8D812FF6-4B96-4D73-8D18-01FACEF33531"
                 *      ok: true
                 *      rev: "1-c5a4055b6c3edac099083cc0b485d4e3"
                 * }
                 * @codeend
                 *
                 * ### Example:
                 * @codestart
                 * TTI.Models.Task.update({ task object goes here });
                 * @codeend
                 */
                update: function (task, success, error) {
                    return db.put(task, function (err, response) {
                        console.log('TTI.Models.Task.update() | POST | err:', err, 'client:', response);

                        if (response) {
                            success(response);
                        }
                        else if (err) {
                            error(err);
                        }
                    });
                },

update 方法负责更新数据库中的特定项。读者可以查看前面代码列表中的注释中的示例用法。

                /**
                 * @function destroy
                 * Destroy task by given ID
                 * @param {Object} task
                 * Task object
                 * @codestart
                 * String (UUID)
                 * @codeend
                 *
                 * @return {Object} an object contains destroyed task UUID
                 * @codestart
                 * {
                 *      id: "8D812FF6-4B96-4D73-8D18-01FACEF33531"
                 *      ok: true
                 *      rev: "1-c5a4055b6c3edac099083cc0b485d4e3"
                 * }
                 * @codeend
                 *
                 * ### Example:
                 * @codestart
                 * TTI.Models.Task.destroy('UUID', function (success, getError, removeError) {
                 *      // handle errors here
                 * });
                 * @codeend
                 */
                destroy: function (id, success, getError, removeError) {
                    return db.get(id, function (getErr, doc) {

                        if (getErr) {
                            getError(getErr);
                        }

                        db.remove(doc, function (removeErr, response) {

                            if (response) {
                                success(response);
                            }
                            else if (removeErr) {
                                removeError(removeErr);
                            }

                        });
                    });
                }
            },
            {

            }
        );

    }
);

这个 destroy 方法负责销毁数据库中的特定项。读者可以查看前面代码列表中的注释中的示例用法。

让我们在 models 目录下创建一个名为 client.js 的文件。在 Client 模型中,我们将保留所有与任务相关的 CRUD 方法,这些方法在本地数据库上操作。创建一个引导文件:

steal(
    'jquery/model',

    function ($) {
        'use strict';

        $.Model('TTI.Models.Client', {
                init: function () {
                    // create database clients or get it if exists.

                    console.log('TTI.Models.Client.init() | idb://clients | err:');

                },

                findAll: function () {

                },

                findOne: function () {

                },

                create: function () {

                },

                update: function () {

                },

                destroy: function () {

                }
            },
            {

            }
        );
    }
);

创建控制器

让我们在 controllers 目录下创建一个 tasks.js 文件,以便我们可以处理所有应用程序操作。

steal(
    'jquery/view/ejs',
    'jquery/controller',
    'tti/models/task.js'
).then(
    function ($) {
        'use strict';

        console.log('TTI.Controllers.Tasks');

        /**
         * @class TTI.Controllers.Tasks
         * Creates a new Tasks controller
         * @parent index
         * @constructor
         * @param {String} DOMElement DOM element
         * @return {Object}
         */
        $.Controller('TTI.Controllers.Tasks', {
            'init': function (element, options) {
                var self = this;

                $('title').text('Time Tracker | TTI');

                TTI.Models.Task.findAll(function (data) {
                    if (!data.rows.length) {
                        data.rows = [
                            {
                                doc: {
                                    hours: '',
                                    cost: {
                                        total: ''
                                    },
                                    taskID: '',
                                    description: 'No tasks so far!',
                                    note: ''

                                }
                            }
                        ];
                    }

                    self.element.html('tti/views/templates/tasks/tasks.ejs', data.rows);

                });

            },

            '{TTI.Models.Task} created': function (Task, e, task) {
                console.log('task', task);
                console.log('this.element', this.element);
                $('tbody tr:last', this.element).after('tti/views/templates/tasks/task.ejs', task);
                $('tbody tr:last', this.element).effect('highlight', {}, 3000);
            },

            '{TTI.Models.Task} destroyed': function (Task, e, task) {
                task.elements(this.element).remove();
            },

            '.add-task click': function () {
                this.element.append('tti/views/templates/tasks/add_task.ejs', {}).find('.create-new-task-dialog-form').dialog({
                    autoOpen: false,
                    modal:    true,
                    buttons:  {
                        'Create New Task': function () {
                            var self = this;

                            window.task = new TTI.Models.Task({
                                hours: $('input[name="hours"]', this).val(),
                                taskID: $('input[name="task-id"]', this).val(),
                                cost: {
                                    rate: 0,
                                    total: 0
                                },
                                description: $('input[name="description"]', this).val(),
                                note: $('input[name="note"]', this).val()
                            });

                            window.task.save(function () {
                                $(self).dialog('destroy').remove();
                            });

                        },
                        Cancel: function () {
                            $(this).dialog('destroy').remove();
                        }
                    },
                    close: function () {
                        $(this).dialog('destroy').remove();
                    }
                }).dialog('open');

            }

        });

    }
);

让我们在 controllers 目录下创建一个 clients.js 文件。

steal(
    'jquery/view/ejs',
    'jquery/controller'
).then(
    function ($) {
        'use strict';

        console.log('TTI.Controllers.Client');

        /**
         * @class TTI.Controllers.Client
         * Creates a new Tasks controller
         * @parent index
         * @constructor
         * @param {String} DOMElement DOM element
         * @return {Object}
         */
        $.Controller('TTI.Controllers.Client', {
            'init': function () {

                $('title').text('Clients | TTI');

                var testData = [
                    {
                        name: 'The First Awesome Client!'
                    },
                    {
                        name: 'The Second Awesome Client!'
                    }
                ];

                this.element.html('tti/views/templates/clients.ejs', testData);

            }

        });

    }
);

让我们在 controllers 目录下创建一个 router.js 文件。

steal(
    'tti/controllers/navigation.js',
    'tti/controllers/client.js',
    'tti/controllers/tasks.js',
    'jquery/controller',
    'jquery/controller/route'
).then(
    function ($) {
        'use strict';

        /**
        * @class TTI.Controllers.Router
        * Creates application router
        * @parent index
        * @constructor
        * @param {String} DOMElement DOM element
        * @return {Object}
        */
        $.Controller('TTI.Controllers.Router', {
            init: function () {
                console.log('r init');
            },

            // the index page
            'route': function (e) {
                console.log('default route', e);
            },

            ':page route': function (data) {
                $('#content').empty().append('<div>');

                if (data.page === 'time-tracker') {
                    new TTI.Controllers.Tasks('#content div');
                }
                else if (data.page === 'clients') {
                    new TTI.Controllers.Client('#content div');
                }
            }

        });

        // create new Router controller instance
        $('body').bind('TTI/db-ready', function () {
            new TTI.Controllers.Router(document.body);
        });
    }
);

让我们在 controllers 目录下创建一个 navigation.js 文件。

steal(
    'jquery/view/ejs',
    'jquery/controller',
    'jquery/dom/route'
).then(
    function ($) {
        'use strict';

        /**
         * @class TTI.Controllers.Navigation
         * Creates application main navigation controller
         * @parent index
         * @constructor
         * @param {String} DOMElement DOM element
         * @return {Object}
         */
        $.Controller('TTI.Controllers.Navigation', {
            init: function () {
                var navItems = [
                    {
                        name: 'Time Tracker',
                        className: 'time-tracker'
                    },
                    {
                        name: 'Invoice',
                        className: 'invoice'
                    },
                    {
                        name:      'Clients',
                        className: 'clients'
                    },
                    {
                        name: 'Reports',
                        className: 'reports'
                    },
                    {
                        name: 'Statistics',
                        className: 'statistics'
                    }
                    ];

                this.element.html('tti/views/templates/navigation.ejs', navItems);
            },

            '.time-tracker click': function (e) {
                $.route.attr('page', 'time-tracker');
            },

            '.clients click': function (e) {
                $.route.attr('page', 'clients');
            }

        });

    }
);

创建视图

让我们在 tti 目录下创建一个 views 文件夹,并在其中创建两个文件夹:stylestemplates

templates 目录下创建一个名为 client.ejs 的文件,内容如下:

<h2>Clients List</h2>

<ol>
    <% $.each(this, function(i, client) { %>

        <li <%= ($el) -> $el.model(client) %>>
            <strong><%= client.name %></strong>
        </li>

    <% }) %>
</ol>

templates 目录下创建一个名为 navigation.ejs 的文件,内容如下:

<% $.each(this, function(i, item) { %>

    <li class="<%= item.className %>">
        <%= item.name %>
    </li>

<% }) %>

templates 目录下创建一个名为 tasks 的文件夹。创建一个名为 tasks.ejs 的文件,内容如下:

<table summary="Time Tracker list of tasks.">
    <thead>
        <tr>
            <th scope="col">Hours</th>
            <th scope="col">Cost</th>
            <th scope="col">Task ID</th>
            <th scope="col">Description</th>
            <th scope="col">Note</th>
        </tr>
    </thead>
    <tbody>
        <% $.each(this, function(i, task) { %>
            <tr <%= ($el) -> $el.model(task) %>>
                <td>
                    <%= task.doc.hours %>
                </td>
                <td>
                    <%= task.doc.cost.total %>
                </td>
                <td>
                    <%= task.doc.taskID %>
                </td>
                <td>
                    <%= task.doc.description %>
                </td>
                <td>
                    <%= task.doc.note %>
                </td>
            </tr>
        <% }) %>
    </tbody>
</table>

<span class="add-task">Add Task</span>

tasks 目录下创建一个名为 task.ejs 的文件,内容如下:

<tr <%= ($el) -> $el.model(task) %>>
    <td>
        <%= task.hours %>
    </td>
    <td>
        <%= task.cost.total %>
    </td>
    <td>
        <%= task.taskID %>
    </td>
    <td>
        <%= task.description %>
    </td>
    <td>
        <%= task.note %>
    </td>
</tr>

tasks 目录下创建一个名为 add_task.ejs 的文件,内容如下:

<div class="create-new-task-dialog-form" title="Create New Task">
    <form>
        <fieldset>
            <label><span>Hours</span> <input type="text" name="hours" /></label>
            <label><span>Task ID</span> <input type="text" name="task-id" /></label>
            <label><span>Description</span> <input type="text" name="description" /></label>
            <label><span>Note</span> <input type="text" name="note" /></label>
        </fieldset>
    </form>
</div>

styles 目录下创建两个文件夹:csssass

sass 目录下创建一个名为 tti.scss 的文件,内容如下:

@import 'reset';
@import 'static';
@import 'mixins';
@import 'skelton';

sass 目录下创建一个名为 _static.scss 的文件,内容如下:

$blue: #5C94BF;
$black: #3E4246;
$white: #F6F6F7;
$vlGreen: #B7D190;
$lGreen: #9CBA6E;
$dGreen: #424A38;
$mGrey: #5B5B5B;
$yellow: #F8AE03;
$lBlue: #167BBE;
$dBlue: #0E69B3;
$gBlue: #7489A1;

sass 目录下创建一个名为 _mixins.scss 的文件,内容如下:

@mixin link {
    color: $lGreen;
    cursor: pointer;
    text-decoration: none;

    &:hover {
        text-decoration: underline;
    }
}

@mixin borderRadius($topLeft, $topRight, $bottomRight, $bottomLeft) {
    -moz-border-radius-topleft: $topLeft;
    -moz-border-radius-topright: $topRight;
    -moz-border-radius-bottomright: $bottomRight;
    -moz-border-radius-bottomleft: $bottomLeft;
    -webkit-border-radius: $topLeft $topRight $bottomRight $bottomLeft;
    border-radius: $topLeft $topRight $bottomRight $bottomLeft;
}

@mixin button {
    @include borderRadius(5px, 5px, 5px, 5px);
    display: inline-block;
    padding: 0 7px;
    line-height: 20px;
    height: 20px;
    cursor: pointer;
}

sass 目录下创建一个名为 _reset.scss 的文件,内容如下:

//   http://meyerweb.com/eric/tools/css/reset/
//   v2.0 | 20110126
//   License: none (public domain)

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
// HTML5 display-role reset for older browsers
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
    display: block;
}

body {
    line-height: 1;
}

ol, ul {
    list-style: none;
}

blockquote, q {
    quotes: none;
}

blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}

table {
    border-collapse: collapse;
    border-spacing: 0;
}

textarea {
    min-height: 100px;
}

sass 目录下创建一个名为 skelton.scss 的文件,内容如下:

html,
body {
    color: $black;
    background: $white;
    font: 11px/18px "Helvetica Neue", Helvetica, Verdana, sans-serif;
}

input,
select,
textarea {
    font: 12px/18px "Helvetica Neue", Helvetica, Verdana, sans-serif;
}

table {
    width: 100%;
    border: 1px solid $gBlue;

    thead {
        color: $white;
        background: $blue;

        tr {

            &:last-child {
                @include borderRadius(5px, 5px, 5px, 5px);
            }

            th {
                padding: 7px 0;
            }
        }
    }

    tbody {

        tr {

            td {
                padding: 3px 0;
                border-bottom: 1px solid $gBlue;
                text-align: center;
            }
        }
    }

}

#container {
    width: 1100px;
    margin: 0 auto;

    #header {
        padding: 10px;
        height: 50px;

        #main-navigation {

            li {
                @include link;
                margin-right: 7px;
                display: inline-block;
            }
        }
    }

    h2 {
        font-size: 14px;
    }

    ol {
        margin: 7px;
        list-style-type: decimal;
        list-style-position: inside;
    }

    .add-task {
        @include button;
        margin-top: 20px;
        color: $white;
        background: $lGreen;

        &:hover {
            background: $yellow;
        }
    }
}

创建引导

在根目录下创建一个名为 index.html 的文件,代码如下。引导负责加载应用程序运行所需的所有文件。

<!doctype html>

<html>
    <head>
        <title>TTI</title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <div id="container">
            <header id="header">
                <nav id="main-navigation">
                    <ul></ul>
                </nav>
                <div id="breadcrumb"></div>
                <nav id="secondary-navigation"></nav>
            </header>

            <div id="content">
                <div><p>Loading...</p></div>
            </div>

            <footer id="footer">

            </footer>

        </div>

        <script src="img/steal.js?tti"></script>

    </body>
</html>

tti 目录下创建一个名为 tti.js 的文件,代码如下:

steal(
    function ($) {
        console.log('tti.js');
    },
    'vendors/jquery_ui/css/smoothness/jquery.ui.core.css',
    'vendors/jquery_ui/css/smoothness/jquery.ui.dialog.css',
    'vendors/jquery_ui/css/smoothness/jquery.ui.theme.css',
    'tti/views/styles/css/tti.css',
    'tti/models/task.js',
    'tti/models/client.js',
    'tti/controllers/tasks.js',
    'tti/controllers/router.js',
    'tti/controllers/navigation.js'
).then(
    'vendors/jquery_ui/jquery.ui.core.js'
).then(
    'vendors/jquery_ui/jquery.effects.core.js'
).then(
    'vendors/jquery_ui/jquery.effects.highlight.js'
).then(
    'vendors/jquery_ui/jquery.ui.widget.js'
).then(
    'vendors/jquery_ui/jquery.ui.position.js',
    'vendors/jquery_ui/jquery.ui.dialog.js'
).then(
    function ($) {
        new TTI.Controllers.Navigation('#main-navigation ul');
    }
);

运行应用程序

为了运行我们的应用程序,我们将 SASS 文件转换为浏览器可以读取的 CSS 文件。

我们使用 SASS 而不是纯 CSS 来将代码拆分成许多小文件,以提高可读性和更好的代码重用。这个方面非常重要,尤其是在大型应用程序中。

可以通过执行 $ gem install sass 命令或从 Git 仓库 sass-lang.com/download.html 下载来安装 SASS。

要将 SASS 代码编译成 CSS 代码,请转到视图文件夹并输入:

$ sass --watch sass:css

然后运行 Web 服务器并导航到 index.html

概述

在本章中,我们学习了如何构建一个 JavaScriptMVC 应用程序,以及如何以更高效和更少出错的方式组织和编写代码以及工作流程。

posted @ 2025-09-29 10:35  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报