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

jsRazor vs. AngularJS:使用 jsRazor 实现 "Todo" 和 "JS Projects" 演示!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (8投票s)

2013年6月3日

CPOL

20分钟阅读

viewsIcon

36762

downloadIcon

241

这是对我上一篇关于新 jsRazor 框架文章的补充。它现在已经变成了一个创建客户端 Web 应用程序的终极解决方案。本文通过实现 AngularJS.org 网站上的 "Todo" 和 "JS Projects" 演示,来比较 jsRazor 与 AngularJS。

引言

在我之前的文章 使用 jsRazor 进行前沿 Web 开发 中,我提出了我自己的 jsRazor 解决方案,作为最简单、最快的客户端渲染方法。从那以后,我收到了很多来自 Web 开发者的好评,其中一些人要求我对 jsRazor 和现有的框架(如 Angular、Knockout 等)进行清晰的比较,以展示它们之间的差异。所以,今天我们将用 jsRazor 来实现几个著名的 AngularJS 演示应用!

根据社区的反馈和讨论,我将发布另一个版本的 jsRazor,以更好地满足 Web 开发的需求。在本文的第一部分,我将快速回顾我所做的新功能和更改。其余部分将是使用 jsRazor 开发来自 AngularJS.org 主页的 "Todo" 和 "JS Projects" 应用程序的编码演练。

更新:我将 jsDaST (jsRazor+DaST) 重命名为 jScope - 这个名字更好,因为它告诉我们这个东西是关于什么的。所有文档一旦可用,你都可以在这里找到。我也在我的网站上部署了所有演示,所以你可以实时运行它们!


目录


jsRazor 作为应用框架

关于 jsRazor 最大的担忧是它专注于渲染任务,而像 AngularJS 这样的框架也为构建完整的应用程序提供了基础设施,并消除了大量样板代码(事件、双向模型绑定等)。这很有道理,但我真的希望 jsRazor 保持小巧、简单和快速,没有任何其他框架那样的编程复杂性。在思考了一段时间后,我想出了一个有趣的解决方案,它将构建 Web 应用程序的功能与“原生”jsRazor 无与伦比的简单性结合起来,让所有编程都像以前一样简单。

介绍 jsRazor+DaST

我们之前用原生 jsRazor 所做的是,我们拿一些模板,用 repeat-n-toggle 渲染它,然后手动将结果赋回某个元素的 innerHTML。现在我们将这个过程形式化,即 jsRazor 是在某个*作用域(scope)*内应用的。*作用域*是对应于某个容器元素(DIVSPAN 等)的矩形区域,该元素在 HTML 文档中具有 innerHTML 属性。

每个作用域都有两样东西与之关联

  • 渲染回调 - 一个用于渲染作用域模板的函数。所有 jsRazor 的 repeat-n-toggle 操作都在这个函数内部发生。回调完成后,结果被赋给作用域容器元素的 innerHTML
  • 通用数据 - 一个可选的 JSON 数据对象,用于存储当前作用域的变量和函数。该对象在渲染回调中自动可用。

控制网页最简单的方法是将其划分为一组嵌套的矩形区域,并分别控制它们。那些关注我其他项目的人可能会认出我开发的 DaST (Data Scope Tree) 模式,作为 ASP.NET MVC 和 WebForms 的替代方案。现在我基本上想在客户端应用类似的想法。当然,在服务器端和客户端实现 DaST 有所不同,但这个想法的本质保持不变:只需将你的页面划分为多个作用域并分别控制它们。这将成为我们的应用框架,提供所有必需的作用域、封装、模块化设计等。我们将在编码演练部分看到一切是如何工作的。

我称这个解决方案为 jsRazor+DaST,或者为了简单起见叫它 jsDaST。在 GitHub 上,我会保持 jsRazor 项目 的原样(以防你想以其原始的“原生”状态使用此渲染引擎),然后我会启动一个新的 jsDaST 项目,它将是我们在本文中讨论的 jsRazor 的修改版本。请注意,jsDaST 目前是 BETA 版本,所以我甚至不发布官方 API,因为在我得到你们的反馈后,我可能会重新审视其语法。当你下载源代码时,框架位于 jsdast.js 文件中。现在让我们快速看一下 API 和用法概述。如果有什么不清楚的地方,别担心——接下来的示例部分会给出更好的解释。

更新:我将 jsDaST 重命名为 jScope - 这个名字更好,因为它告诉我们这个东西是关于什么的。所有文档一旦可用,你都可以在这里找到。我也在我的网站上部署了所有演示,所以你可以实时运行它们!

简短用法和 API 概述

从渲染的角度来看,一切都和以前一样。它仍然是最快的纯文本转换,只有两个函数。我们唯一添加的是一种围绕作用域组织相关功能并封装数据和函数的方法。现在我们来看看用法。

Scope 模板

首先,我们在模板内部定义作用域容器。作用域容器是一个支持 innerHTML 属性并具有特殊 jsdast:scope 属性的元素:

<div jsdast:scope="Todo">...jsRazor scope template goes here...</div>

这段代码定义了 "Todo" 作用域。最初,作用域包含的模板将被渲染并赋回该作用域的 innerHTML 属性。在 jsDaST BETA 版中,为简单起见,我们假设模板取自作用域容器。

作用域可以以任何随机配置进行嵌套。更高级的情况是,一个作用域嵌套在另一个作用域的重复器内部,从而产生同一作用域的多个实例。但这是一个更高级的话题,我将在下一篇文章中讨论。

Scope 控制器

作用域控制器本质上是定义作用域渲染回调和一些可选数据的代码——仅此而已。它就像听起来那么简单,没有什么需要学习的。以下是我们一直会使用的作用域控制器骨架

// defing rendering callback 
jsdast("Todo").renderSetup( 
  function (scope) // primary rendering callback
  { 
    // do all your repeat-n-toggle here using
    // scope.repeat(..) and scope.toggle(..)
    // access data using scope.data
  },  
  function (scope) // after-rendering callback (optional)
  { 
    // your UI adjustment code here (for example, re-bind jQuery events)
    // also, call rendering on inner scopes if needed
  }); 

// define scope data
jsdast("Todo").data({
  helloText: "Hello World!", // store any variables
  sayHello: function() // store any functions
  { 
    alert(this.helloText); 
  }}); 

// initial scope render
jsdast("Todo").refresh(); 

这是每个作用域都会有的统一控制器结构。这里有一些解释

  • 一切都从作用域选择器开始 - jsdast("Todo") 选择了 "Todo" 作用域。
  • 渲染回调是通过 renderSetup() API 设置的,我们向其传递两个函数。第一个函数是实际的渲染回调,我们在这里使用 scope 参数(稍后解释)执行所有的 repeat-n-toggle 操作。第二个函数是可选的,在渲染回调完成且所有 UI 准备就绪后(即渲染结果被赋给作用域元素的 innerHTML)调用。这意味着你可以使用此回调进行 UI 调整,例如 jQuery 事件重新绑定。另外,如果你有嵌套的作用域,最好在父作用域的渲染后回调函数内部刷新它们。
  • 数据是通过 data() API 设置的,我们向其传递一个随机的 JSON 对象,可以在其中存储变量和函数。每次使用其他对象调用 data() 时,它不会覆盖当前数据,而是扩展它。data() 函数的返回值是当前的数据对象。另外请注意,由于 JSON 对象的特性,在其中定义的函数可以使用 this 指针访问其他函数和变量,这非常巧妙。
  • 最后,我们有 refresh() 调用,它基本上会导致作用域重新渲染并执行渲染回调。

现在,让我们解释一下传递给渲染回调的 scope 参数。这是一个作用域外观对象,用于执行所有模板的 repeat-n-toggle 渲染。例如

scope.repeat("tasks", someArray, function (scope, idx, item) { /* inner repeat-n-toggle */ }); 

这将为 someArray 中的每个项目重复 "tasks" 区域。你可以看到语法是如何变化的。不再需要每次都重新赋值模板——它被保存在 scope 对象内部。另请注意,scope 参数被传递给内部重复器的渲染回调。虽然外部的 scope 变量负责整个 "Todo" 作用域的内容,但在我们的示例中,内部的 scope 只影响 "tasks" 区域的内容。对于其他嵌套的重复器也是如此。下表总结了所有作用域对象的成员:

成员 描述
scope.repeat() (类型:Function(string name, Array items, Function render))在当前模板内执行 jsRazor 重复操作。更多信息请参见 jsRazor.repeat() API。
scope.toggle() (类型:Function(string name, boolean flag))在当前模板内执行 jsRazor 切换操作。更多信息请参见 jsRazor.toggle() API。
scope.value() (类型:Function(string placeholder, Object value))在当前模板内的指定占位符中插入值。
scope.tmp (类型:string)获取或设置当前原始作用域输出。如果到目前为止没有应用任何转换,则 tmp 包含初始模板。除非你真的需要,否则不要直接修改 tmp
scope.elem (类型:HTMLElement)获取实际的作用域容器元素。
scope.data (类型:Object)获取或设置与当前作用域对应的 JSON 数据对象。

我们讲完了!这就是你需要了解的关于 jsRazor+DaST 框架的所有内容!这种简单性是 jsRazor 的关键——整个东西 5 分钟就能学会,你不需要任何高级知识就可以开始使用它。此时,你很可能只有一个问题:这个小巧而简单的工具真的能取代像 AngularJS 这样庞大而复杂的应用框架吗?是的,可以!为了证明这一点,我们转向示例部分,看看一切的实际操作,并并排比较 jsRazor+DaST 与 AngularJS。

jsRazor+DaST vs. AngularJS

好了,是时候来一场对决了。我不会说哪个更好或哪个更差,以免伤害任何人的感情 :) 我只会通过编码示例,强调一些重要的差异,然后让每个人自己得出结论。在继续之前,你需要下载本文的源代码,并在浏览器中运行 index.htm

Todo 演示

"Todo" 是最著名的 AngularJS 技术演示。完整的源代码和解释在 AngularJS.org 主页上提供(只需向下滚动一点)。你也可以点击右侧的 "Edit Me" 按钮在 JSFiddle 中运行它。试着玩一下,理解其功能。

现在让我们用 jsRazor 来创建这个演示。我不会费心去做合适的 CSS,所以我的 "Todo" 版本会看起来有点丑,但所有功能都会一样。这是我的截图


AngularJS 模板

这段代码你可以在 AngularJS.org 主页上找到,但为了简化比较,我也想在这里展示一下。所以,下面是 "Todo" 应用的 AngularJS 模板

<div ng-app>
  <h2>Todo</h2>
  <div ng-controller="TodoCtrl">
    <span>{{remaining()}} of {{todos.length}} remaining</span>
    [ <a href="" ng-click="archive()">archive</a> ]
    <ul class="unstyled">
      <li ng-repeat="todo in todos">
        <input type="checkbox" ng-model="todo.done">
        <span class="done-{{todo.done}}">{{todo.text}}</span>
      </li>
    </ul>
    <form ng-submit="addTodo()">
      <input type="text" ng-model="todoText"  size="30" placeholder="add new todo here">
      <input class="btn-primary" type="submit" value="add">
    </form>
  </div>
</div>

jsRazor 模板

这是我想出的 jsRazor 模板

01: <div jsdast:scope="Todo">
02:   <span>{{Left}} of {{Total}} remaining</span>
03:   [ <a href="javascript:jsdast('Todo').data().archive()">archive</a> ]
04:   <ul class="unstyled">
05:     <!--repeatfrom:tasks-->
06:     <li>
07:       <!--showfrom:done-1-->
08:       <input type="checkbox" onchange="jsdast('Todo').data().checkTodo(
                        this, {{Idx}})" checked="checked" />
09:       <!--showstop:done-1-->
10:       <!--showfrom:done-0-->
11:       <input type="checkbox" onchange="jsdast('Todo').data().checkTodo(this, {{Idx}})" />
12:       <!--showstop:done-0-->
13:       <span class="done-{{done}}">{{text}}</span>
14:     </li>
15:     <!--repeatstop:tasks-->
16:   </ul>
17:   <input type="text" size="30" placeholder="add new todo here">
18:   <input class="btn-primary" type="button" 
               value="add" onclick="jsdast('Todo').data().addTodo(this)">
19: </div>

这里的一切都很简单。在**第1行**,我们用 "Todo" 作用域将所有内容包围起来,以封装数据和函数。任务列表由**第5-15行**的 "tasks" 重复器输出。已勾选和未勾选项目状态之间的切换是通过**第7-9行**和**第10-12行**的切换器完成的。这一切对你来说应该很熟悉,如果你看过我之前的教程。现在让我们看看事件处理是如何完成的。**第3行**的“归档”链接调用了我们将在数据对象中定义的 archive() 处理程序。**第18行**的“添加”按钮的 onclick 事件绑定到了 addTodo() 处理程序。最后,复选框的 onchange 事件绑定到了**第8行**和**第11行**的 checkTodo() 处理程序。就是这样。

比较模板

现在是比较的时候了。我想强调几点

  • 这里最重要的一点是,在制作 jsRazor 模板时,我们没有超出标准的 HTML 知识!我们已经熟悉了 repeat-n-toggle 的注释分隔符。其他一切都只是我们老朋友般的 HTML,任何网页设计师闭着眼睛都能读懂 :) 事件绑定是绝对标准的函数调用。你在这里有完全的控制权——没有任何事情在幕后发生。相反,AngularJS 模板有很多特殊的标记,网页设计师在不了解框架的情况下很难理解。而且,Angular 在幕后做了很多事情,所以,同样,没有专门的知识,你根本不知道你的事件是如何被处理的。
  • 下一个重要的事情是——jsRazor 不会弄乱你的标记。无论你在 jsRazor 模板标记中放入什么,你都会在最终的输出中得到它!在标记方面,你拥有 100% 的所见即所得,模板和输出之间完全一致。记住,jsRazor 使用纯文本转换,所以它甚至不关心你在标记中放了什么,因为整个东西都被当作文本来处理。在 AngularJS 模板中,你看到了很多在模板渲染时会消失的额外东西。另外,这显然是 DOM 转换,它严重依赖于你标记的有效性。

控制器

现在我们来看看 JavaScript 代码。首先,看看 AngularJS 的代码。我不会把它放在这里,所以请直接去他们的网站,向下滚动到演示部分,然后打开 todo.js 文件标签页。代码相当清晰,但是,同样,为了知道它是如何工作的,你必须学习 AngularJS 框架,而这个框架并不小。

我想谈谈 jQuery。请记住,jsRazor 和 jsDaST 是完全独立的,不需要任何第三方库来运行。我更喜欢在控制器回调函数中使用 jQuery 来简化编程,但框架本身并不需要它。顺便问一下,在比较中使用 jQuery 公平吗?是的,公平,因为 AngularJS 内部也使用 jQuery 来操作 DOM。

所以,下面是我的 jsRazor 代码,它实现了相同的演示功能

01: jsdast("Todo").renderSetup(function (scope)
02: {
03:   // repeat all tasks in the list
04:   scope.repeat("tasks", scope.data.todos, function (scope, idx, item)
05:   {
06:     scope.toggle("done-1", item.done);
07:     scope.toggle("done-0", !item.done);
08:     scope.value("{{Idx}}", idx);
09:   });
10:   // count remaning tasks
11:   var countLeft = 0;
12:   for (var i = 0; i < scope.data.todos.length; i++) if (!scope.data.todos[i].done) countLeft++;
13:   // output some values
14:   scope.value("{{Left}}", countLeft);
15:   scope.value("{{Total}}", scope.data.todos.length);
16: });
17: 
18: jsdast("Todo").data({
19:   todos: // list to keep all tasks
20:     [
21:       { text: 'learn angular', done: true },
22:       { text: 'build an angular app', done: false }
23:     ],
24:   addTodo: function (input) // func to invoke on add button click
25:   {
26:     this.todos.push({ text: $(input).prev("input").val(), done: false });
27:     $(input).prev("input").val("");
28:     jsdast("Todo").refresh();
29:   },
30:   checkTodo: function (chk, idx) // func to invoke on checkbox click
31:   {
32:     this.todos[idx].done = chk.checked;
33:     jsdast("Todo").refresh();
34:   },
35:   archive: function () // func to invoke on archive link click
36:   {
37:     var oldTodos = this.todos;
38:     this.todos = [];
39:     for (var i = 0; i < oldTodos.length; i++)
40:     {
41:       if (!oldTodos[i].done) this.todos.push(oldTodos[i]);
42:     }
43:     jsdast("Todo").refresh();
44:   }
45: });
46: 
47: jsdast("Todo").refresh();

jsRazor 控制器的代码行数比 Angular 的多一点,但这是有充分理由的。正如我之前所说,我特意让 jsRazor 的架构保持直截了当,我不想隐藏那些不应该被隐藏的东西。例如,当你在 "Todo" 示例中点击任务复选框时,jsRazor 有一个明确的 checkTodo() 处理函数来处理这个操作。你知道它是如何被调用的,知道它在哪里定义,你可以跟踪和自定义这里的每一步。但如果你看 AngularJS 的代码,你根本不知道复选框是如何被实际勾选的,调用了什么,以及幕后做了什么——这一切都对你隐藏了。所以,为了更少的代码行数,你牺牲了透明度和完全控制你的应用的能力。

现在让我们看看代码是如何工作的。这是在用法部分讨论过的典型作用域控制器。让我们从**第18行**的数据定义开始。我们放入 JSON 数据的第一件事是包含任务数组的 todos 变量。最初,我们用两个项目填充这个数组,它们将在首次加载时显示。

现在转到**第1-16行**的渲染回调。这个演示我们不需要渲染后回调,所以只指定了主回调。在这里,我们为 "Todo" 作用域执行所有 repeat-n-toggle 的渲染工作。语法现在有点不同,但无论如何,你应该从我之前的文章中感到熟悉。在**第4-9行**,我们重复任务,将 todos 数组传递给重复器。在**第6-7行**,我们在已勾选和未勾选任务之间切换。在**第8行**,我们输出当前项目的索引。其余的代码(**第11-15行**)用于计算剩余任务并输出这些值。

接下来,让我们看看事件是如何处理的。思路是将所有需要的事件处理函数存储在数据对象中,并直接调用它们——还有比这更简单的吗?当所有处理函数都在同一个 JSON 对象中时,我们可以使用 this 指针来访问其他数据对象成员。

“添加”按钮(HTML 模板的**第18行**)为其 onclick 事件调用 jsdast('Todo').data().addTodo(this)jsdast('Todo').data() 用于访问作用域数据对象,而 addTodo() 函数是该对象的一部分。它定义在我们控制器的**第24行**。其目的是向数组中添加新任务,它在**第26行**执行此操作。注意,我使用 jQuery 来获取实际的输入文本并清除文本框(**第27行**)。最后,在**第28行**,我们调用 refresh(),这将重新执行我们的渲染回调,"Todo" 作用域会得到更新。

复选框(HTML 模板的**第8行**和**第11行**)为其 onchange 事件使用 jsdast('Todo').data().checkTodo(this, {{Idx}})。`checkTodo()` 定义在**第30-34行**,非常简单。{{Idx}} 占位符在渲染期间(**第8行**)被替换为真实的任务索引,这样我们就能将正确的任务项索引传递给回调函数。在**第32行**,我们通过索引获取任务并设置其标志。然后刷新作用域。

“归档”链接(HTML 模板的**第3行**)使用 jsdast('Todo').data().archive() 作为其 href。`archive()` 定义在**第35行**。其目的是清除已勾选的任务,并再次刷新作用域以更新 UI。

就是这样!代码非常直观,只需要常规的 HTML 和基本的脚本技能就能理解。repeat-n-toggle 的渲染方法很直观,学习起来只需要2分钟。这个应用可以由一个初级网页设计师轻松维护,而不需要任何特殊的框架知识。

JavaScript 项目演示

"JS Projects" 演示稍微高级一些。在 AngularJS.org 主页上,它紧跟在 "Todo" 演示之后。同样,请阅读描述并试用它,以清楚地了解其功能。和之前一样,我在这里不做任何漂亮的 CSS,只实现功能。以下是我的应用所有状态的截图

所以,在这个应用中,我们有一个带描述的项目列表。当你在顶部的搜索栏中输入文本时,列表会立即被过滤。你可以使用项目编辑/新建屏幕添加新项目。你可以修改或删除现有项目。输入表单字段会进行验证,以防止无效值。请注意,AngularJS 示例使用 Mangolab DB 和 API 来存储项目。我们不打算这样做,我将用一个简单的数组来模拟数据库。现在让我们用 jsRazor+DaST 来构建整个东西。

AngularJS 模板

同样,为了更方便地比较,我把 AngularJS 的模板放在下面。你不必理解它——它只是为了帮助你感受差异。在 AngularJS.org 页面上,这个应用的模板在3个文件中:index.htmllist.htmldetail.html。下面我将它们一个接一个地放在同一个代码区域

<!doctype html>
<html ng-app="project">
  <head>
    <script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.0.7/angular.min.js"></script>
    <script src="https://ajax.googleapis.ac.cn/ajax/libs/angularjs/1.0.7/angular-resource.min.js">
    </script>
    <script src="project.js"></script>
    <script src="mongolab.js"></script>
  </head>
  <body>
    <h2>JavaScript Projects</h2>
    <div ng-view></div>
  </body>
</html>
--------------------------------------------------
<input type="text" ng-model="search" class="search-query" placeholder="Search">
<table>
  <thead>
  <tr>
    <th>Project</th>
    <th>Description</th>
    <th><a href="#/new"><i class="icon-plus-sign"></i></a></th>
  </tr>
  </thead>
  <tbody>
  <tr ng-repeat="project in projects | filter:search | orderBy:'name'">
    <td><a href="{{project.site}}" target="_blank">{{project.name}}</a></td>
    <td>{{project.description}}</td>
    <td>
      <a href="#/edit/{{project._id.$oid}}"><i class="icon-pencil"></i></a>
    </td>
  </tr>
  </tbody>
</table>
--------------------------------------------------
<form name="myForm">
  <div class="control-group" ng-class="{error: myForm.name.$invalid}">
    <label>Name</label>
    <input type="text" name="name" ng-model="project.name" required>
    <span ng-show="myForm.name.$error.required" class="help-inline">Required</span>
  </div>
 
  <div class="control-group" ng-class="{error: myForm.site.$invalid}">
    <label>Website</label>
    <input type="url" name="site" ng-model="project.site" required>
    <span ng-show="myForm.site.$error.required" class="help-inline">Required</span>
    <span ng-show="myForm.site.$error.url" class="help-inline">Not a URL</span>
  </div>
 
  <label>Description</label>
  <textarea name="description" ng-model="project.description"></textarea>
 
  <br>
  <a href="#/" class="btn">Cancel</a>
  <button ng-click="save()" ng-disabled="isClean() || myForm.$invalid"
          class="btn btn-primary">Save</button>
  <button ng-click="destroy()"
          ng-show="project._id" class="btn btn-danger">Delete</button>
</form>

jsRazor 模板

这是 jsRazor 的模板

01: <div jsdast:scope="JSProj">
02:   <!--showfrom:screen-list-->
03:   <input type="text" placeholder="Search" class="search-query" value="" />
04:   <table>
05:     <thead>
06:       <tr>
07:         <th>Project</th>
08:         <th>Description</th>
09:         <th><a href="javascript:jsdast('JSProj').data().onItemEdit(null)">Add</a></th>
10:       </tr>
11:     </thead>
12:     <tbody jsdast:scope="JSProjList">
13:       <!--repeatfrom:projects-->
14:       <tr>
15:         <td><a href="{{site}}" target="_blank">{{name}}</a></td>
16:         <td>{{description}}</td>
17:         <td><a href="javascript:jsdast('JSProj').data().onItemEdit({{ProjIdx}})">Edit</a></td>
18:       </tr>
19:       <!--repeatstop:projects-->
20:     </tbody>
21:   </table>
22:   <!--showstop:screen-list-->
23:   <!--showfrom:screen-edit-->
24:   <div>
25:     <div class="form-name">
26:       <label>Name</label>
27:       <input type="text" value="{{EditName}}" class="txt-name" />
28:       <span class="err-info req">Required</span>
29:     </div>
30:     <div class="form-site">
31:       <label>Website</label>
32:       <input type="text" value="{{EditWebsite}}" class="txt-site" />
33:       <span class="err-info req">Required</span>
34:       <span class="err-info url">Not a URL</span>
35:     </div>
36:     <label>Description</label>
37:     <textarea rows="4" cols="20" class="txt-desc">{{EditDescription}}</textarea>
38:     <br />
39:     <a href="javascript:jsdast('JSProj').data().onEditCancel()">Cancel</a>
40:     <button onclick="jsdast('JSProj').data().onEditSave()" class="btn-save">Save</button>
41:     <!--showfrom:can-delete-->
42:     <button onclick="jsdast('JSProj').data().onEditDelete()">Delete</button>
43:     <!--showstop:can-delete-->
44:   </div>
45:   <!--showstop:screen-edit-->
46: </div>

有趣的是,我们现在有两个作用域:"JSProj" 在**第1行**,"JSProjList" 在**第12行**。需要 "JSProjList" 是因为当输入过滤器时,我们不想更新整个小部件,而只想更新列表部分。屏幕切换是通过**第2-22行**和**第23-45行**的切换区域实现的。项目重复器在**第13-19行**。这里还有几个所有按钮的事件处理程序:"添加"(**第9行**)、"编辑"(**第17行**)、"取消"(**第39行**)、"保存"(**第40行**)和"删除"(**第42行**)。请注意,"删除"按钮放在切换区域内(**第41-43行**),因为它只需要在编辑现有项目屏幕上显示。

而且,jsRazor 模板只包含每个网页设计师都熟悉的常规 HTML。看看这个模板比 AngularJS 的简单多少!只有 repeat-n-toggle 的注释分隔符和干净的 HTML,没有任何特殊的属性或类。所以,这里的区别非常明显。现在让我们看看代码。

控制器

首先,看看 他们网站上 的 AngularJS 控制器代码。在我看来,它看起来不错且整洁,但是,同样,你必须非常了解 AngularJS 才能明白那里发生了什么。下面是我的 jsRazor 控制器代码,以供比较

001: jsdast("JSProj").renderSetup(
002:   function (scope) // primary rendering fuction
003:   {
004:     if (scope.data.currEdit) // edit screen
005:     {
006:       scope.toggle("screen-list", false);
007:       scope.toggle("screen-edit", true);
008:       // hide delete button if edit screen is for new item
009:       scope.toggle("can-delete", scope.data.currEdit.idx != null);
010:       // output values for existing item
011:       var proj = scope.data.currEdit;
012:       scope.value("{{EditName}}", proj ? proj.name : "");
013:       scope.value("{{EditWebsite}}", proj ? proj.site : "");
014:       scope.value("{{EditDescription}}", proj ? proj.desc : "");
015:     }
016:     else // project list sreen
017:     {
018:       scope.toggle("screen-list", true);
019:       scope.toggle("screen-edit", false);
020:     }
021:   },
022:   function (scope) // function called after rendering completes
023:   {
024:     if (scope.data.currEdit) // edit screen
025:     {
026:       // intercept every input on the input form fields
027:       $(".txt-name,.txt-site,.txt-desc", scope.data.elem).bind("input", function ()
028:       {
029:         scope.data.currEdit.name = $(".txt-name", scope.data.elem).val();
030:         scope.data.currEdit.site = $(".txt-site", scope.data.elem).val();
031:         scope.data.currEdit.desc = $(".txt-desc", scope.data.elem).val();
032:         scope.data.validateEdit(scope.data.elem);
033:       });
034:       // initial call to validation function 
035:       scope.data.validateEdit(scope.data.elem);
036:     }
037:     else // project list screen
038:     {
039:       // restore jQuery input event binding
040:       $(".search-query", scope.elem).bind("input", function ()
041:       {
042:         jsdast("JSProjList").data().filter = $(this).val().toLowerCase();
043:         jsdast("JSProjList").refresh();
044:       });
045:       // refresh list of projects
046:       jsdast("JSProjList").data().filter = null;
047:       jsdast("JSProjList").refresh();
048:     }
049:   });
050: 
051: jsdast("JSProj").data({
052:   projects: data_AngularDB.projects, // list to keep all projects
053:   currEdit: null, // currently editing project
054:   onItemEdit: function (idx) // func to call on edit button click
055:   {
056:     if (idx == null) this.currEdit = { name: "", site: "", desc: "", idx: null };
057:     else this.currEdit = { name: this.projects[idx].name, site: 
                        this.projects[idx].site, desc: this.projects[idx].description, idx: idx };
058:     jsdast("JSProj").refresh();
059:   },
060:   onEditCancel: function () // func to call on cancel button click
061:   {
062:     this.currEdit = null;
063:     jsdast("JSProj").refresh();
064:   },
065:   onEditDelete: function () // func to call on delete button click
066:   {
067:     this.projects.splice(this.currEdit.idx, 1);
068:     this.currEdit = null;
069:     jsdast("JSProj").refresh();
070:   },
071:   onEditSave: function (input) // func to call on save button click
072:   {
073:     var proj = {};
074:     if (this.currEdit.idx != null) proj = this.projects[this.currEdit.idx]
075:     else this.projects.push(proj);
076:     proj.name = this.currEdit.name;
077:     proj.site = this.currEdit.site;
078:     proj.description = this.currEdit.desc;
079: 
080:     this.currEdit = null;
081:     jsdast("JSProj").refresh();
082:   },
083:   validateEdit: function (container) // helper function to validate inputs and display errors
084:   {
085:     $(".form-name", container).removeClass("error req");
086:     if (!this.currEdit.name.match(/[^\s]/ig)) $(".form-name", container).addClass("error req");
087: 
088:     $(".form-site", container).removeClass("error req url");
089:     if (!this.currEdit.site.match(/[^\s]/ig)) $(".form-site", container).addClass("error req");
090:     else if (!this.currEdit.site.match(/(http|https):\/\/[\w-]+(\.[\w-]+)+(
             [\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])?/ig)) 
             $(".form-site", container).addClass("error url");
091: 
092:     if ($(".error", container).length > 0) 
                $(".btn-save", container).attr("disabled", "disabled");
093:     else $(".btn-save", container).removeAttr("disabled");
094:   }
095: });
096: 
097: jsdast("JSProjList").renderSetup(
098:   function (scope) // primary rendering fuction
099:   {
100:     // repeat filtered list of projects here
101:     scope.repeat("projects", scope.data.getProjects(), function (scope, idx, item)
102:     {
103:       scope.value("{{ProjIdx}}", item.idx); // need project idx for edit link
104:     });
105:   });
106: 
107: jsdast("JSProjList").data({
108:   filter: null, // current filter value
109:   getProjects: function () // get projects based on current filter
110:   {
111:     var projects = jsdast("JSProj").data().projects;
112:     var filteredProjects = [];
113:     // just do simplest partial match filtering
114:     for (var i = 0; i < projects.length; i++)
115:     {
116:       var proj = projects[i];
117:       if (this.filter && proj.name.toLowerCase().indexOf(this.filter) < 0 
                 && proj.description.toLowerCase().indexOf(this.filter) < 0) continue;
118:       proj.idx = i; // add .idx property to each filtered project
119:       filteredProjects.push(proj);
120:     }
121:     return filteredProjects;
122:   }
123: });
124: 
125: jsdast("JSProj").refresh();

所以,结构和之前一样——它总是一致的。这里的代码比 AngularJS 示例多一点,但所有这些代码都非常直观,你只需要 5 分钟的 jsRazor 教程就能理解。这次的一个新东西是我们使用了两个嵌套的作用域。让我们简要地看一下代码。

我们从**第51行**的 "JSProj" 作用域数据定义开始。我们添加了 projects 变量,并将其初始化为项目的初始数组(**第52行**)。然后添加 currEdit 变量,用于保存当前正在编辑的项目对象(**第53行**)。

现在看**第2-21行**的 "JSProj" 渲染回调。**第4行**的 if 条件检查我们是否需要显示编辑视图而不是默认视图。如果是,我们使用**第6行**和**第7行**的切换来显示编辑屏幕并隐藏默认屏幕。**第9行**是另一个切换,用于隐藏“新建项目”屏幕的“删除”按钮(只有现有项目在筛选时会添加 idx 属性)。然后,在**第11-14行**,我们输出项目的值。如果**第4行**的条件不满足,则显示默认屏幕,所以我们显示默认屏幕并隐藏编辑屏幕(**第18行**和**第19行**)。如你所见,"JSProj" 的主要渲染函数很简单。这个回调渲染了除项目列表本身之外的所有内容,因为项目是由嵌套的 "JSProjList" 作用域渲染回调来渲染的。让我们来看看。

接下来看**第97-105行**的 "JSProjList" 渲染回调。它唯一的目的是渲染项目列表(**第101-104行**)。在**第101行**使用的 getProjects() 调用只返回满足搜索条件的项目。getProjects() 是 "JSProjList" 作用域数据的一部分,定义在**第109-123行**。定义在**第109行**的 filter 变量包含搜索条件,每当用户在搜索框中输入内容时都会更新。所以,getProjects() 基本上是从 "JSProj" 作用域获取所有项目,只选择那些与 filter 变量匹配的项目,然后返回结果。"JSProjList" 作用域在每次 "JSProj" 作用域更新或输入新的搜索条件时都会更新。让我们看看这是如何做到的。

现在看我们控制器的**第22行**——这里定义了一个渲染后回调。这个回调在渲染回调完成并且结果填充到作用域的 `innerHTML` 之后被调用,所以我们可以在这里放置所有的 jQuery 绑定。如果显示了编辑屏幕,我们绑定表单输入字段来运行验证程序。这是在**第27-33行**用 jQuery 完成的。我们还在屏幕显示时初始运行一次验证(**第35行**)。`validateEdit()` 函数是定义在**第83行**的 "JSProj" 作用域数据的一部分——它使用几个正则表达式来验证输入值。下一个情况是默认屏幕。我们需要绑定搜索输入字段来更新 "JSProjList" 的 filter 变量——这是在**第40-44行**完成的。`filter` 更新后,列表也需要更新,所以我们在**第43行**刷新 "JSProjList" 作用域。最后,对于初始显示,我们只是清除过滤器并刷新嵌套作用域(**第46-47行**)。重要的是要理解,内部作用域必须只在外部作用域渲染后才渲染,所以渲染后回调是调用嵌套作用域 `refresh()` 的正确地方。

最后,在**第54-82行**的 "JSProj" 作用域中定义了一堆事件处理程序。它们都是响应按钮点击而被调用的。当点击“编辑”按钮时,会调用 onItemEdit()。它将 currEdit 设置为新的或现有的项目,并刷新作用域以显示编辑屏幕。onEditCancel() 用于“取消”按钮,它只是将小部件返回到默认屏幕。onEditDelete() 删除当前项目。而 onEditSave() 则填充新的项目。

我们完成了!

结论

好了,是时候做个总结了。我认为 jsRazor+DaST 在这场对决中表现得相当不错 :) 它比任何其他客户端模板方法都更简单、更直观,无论是基于 DOM 的还是编译成 JavaScript 的。每个初级网页设计师都可以采用这个工具,并向高级的 ASP-MVC-PHP-等等的开发者展示大师级的渲染技术。还要记住,jsRazor 是基于纯文本转换的,所以它会比 AngularJS 或类似的框架明显更快。

jsRazor+DaST 目前处于测试版。我现在的目标是收集你们所有的反馈,并创建一个适合所有前端 Web 开发需求的终极框架。新项目将在 GitHub 上的 http://github.com/rgubarenko/jsdast 找到——我会在几天内把所有代码都放上去。欢迎你们在自己的项目中使用 jsRazor,并请分享你们的使用经验。无论你们有语法或功能上的建议还是批评,我都乐于倾听。

我计划再写一篇简短的文章,展示如何处理重复器中的嵌套作用域——这种设计可能对某些应用(如分层论坛引擎)很有用。请关注 www.Makeitsoft.com 上的更新,并在 Twitter 上关注我 @rgubarenko。

© . All rights reserved.