本文编译自 Mark Rajcok 在 StackOverflow 的同名问答题《How do I “think in Angular.js” if I have a jQuery background?》。下面是 Mark Rajcok 的提问:
“我可以熟练使用jQuery进行客户端应用的开发,但是现在我希望开始使用Angular.js。哪位能描述一下这个过程中必要的模式变化吗?希望您的答案能够围绕下面这些具体的问题:
1. 我如何对客户端web应用进行不同方式的架构和设计?它们之间最大的区别是什么?(译者注:指jQuery和Angular.js)
2. 有什么是我不该做或者不该使用的;而又有什么是我应该做或者应该使用的呢?
3. 有没有一些服务端的考量/约束呢?
我在寻找的就是一个关于jQuery和Angular.js之间的详细的比较。”
下面是来自 Josh David Miller 的最佳回答:
1. 绝不要先设计你的页面,然后用DOM操作去改变它
在jQuery中,你会先设计一个页面,然后让它变得动态化。这是因为jQuery是为了扩展而设计的,并在这个前提下变得越来越臃肿。
但是在Angular.js中, 你必须从一开始就在脑子里挂着架构的弦。不要一开始就想着“我有这样一个DOM,我想让它做X”, 你必须从你要完成的目标开始思考,然后设计你的应用, 最后才是设计你的视图。
2. 不要用 Angular.js 扩展 jQuery
类似地,不要一开始就带着这样的想法:jQuery可以完成X,Y,Z,所以我只要在其上为模型和控制器添加Angular.js就行了。在起步阶段这确实很容易勾引你,这也是为什么我总是推荐Angular.js新手根本不要使用jQuery,至少要在他们习惯了“angular 方式”之后。
我在这里(译者注:指stackoverflow)和邮件列表上看到过很多开发者,他们用150或者200行代码的jQuery插件,然后利用一堆让人困惑的复杂的回调和$apply与Angular.js粘合起来建立这些详尽的解决方案;最终确实可以跑起来! 但是其实这个问题在大多数情况下,我们可以用一小段Angular.js代码来重写jQuery插件即可,而这种方式会让一切刹那间简单明了可理解。
我觉得这类问题的底线是:当你在解决问题时,首先利用“Angular.js思想”去做;如果你不能想出一个方案,那么就在社区里询问;如果还是没有简单的解决方法,那么再请随意使用jQuery吧。但是注意,千万别让jqeury成为你的拐杖,不然你将永远无法真正精通Angular.js。
3. 永远根据架构去思考
首先你要知道,单页面结构也是应用。它不是网页。所以我们需要有服务端开发者思想加上客户端开发者思想。 我们必须考虑如何将我们的应用拆分为独立,可扩展,可测试的组件。
那么你要怎么做呢?你如何做到利用“angualrjs思想”呢?这里有一些普遍的原则,与jQuery作为比照。
视图是“正式记录”
在jQuery中,我们通过编程方式来改变视图。我们可以像下面这样通过ul标签来定义一个下拉菜单:
<ul class="main-menu">
<li class="active">
<a href="#/home">Home</a>
</li>
<li>
<a href="#/menu1">Menu 1</a>
<ul>
<li><a href="#/sm1">Submenu 1</a></li>
<li><a href="#/sm2">Submenu 2</a></li>
<li><a href="#/sm3">Submenu 3</a></li>
</ul>
</li>
<li>
<a href="#/home">Menu 2</a>
</li>
</ul>
在jQuery中,根据我们应用的逻辑,可以用类似下面的语句来激活它。
$('.main-menu').dropdownMenu();
当我们只是看着视图的时候,不会立刻看出它的功能。对于小应用而言,这样是没问题的。但是对于大型的应用,情况就一下子变得令人困惑并且难以维护。
但是在Angular.js中,视图是基于视图的功能的正式记录。我们的ul是像下面这样声明的:
<ul class="main-menu" dropdown-menu>
...
</ul>
这两者其实做了同样的事情,但是在Angular.js的版本中,任何看到这个模板的人都知道将要发生什么。不论何时,开发团队里有任何新的开发人员加入,她可以一眼看出有一个叫做dropdownMenu的指令作用在视图上;她根本不需要凭直觉猜测或者研究下代码才找到正确的答案。视图本身就告诉我们将会发生什么了。清晰多了。
angualrjs的新手经常会问这样一个问题: 我如何找到某一类所有的链接并且给它们添加一个指令呢?当看到我们回复的时候小伙伴都震惊了:压根别去这样做。但是劝你不要这样做的原因是,这样做就像是一半jQuery,一半angulrjs,而这真心很糟。这里的问题是,开发者想在angualrjs的情境中使用jQuery方式。而这绝对不会玩得转。视图是正式记录。超出指令的范围(这点下文会谈论更多),你绝不要去改变DOM。而且指令是应用在视图中的,目的自然也一目了然。
记住:不要先设计再修饰。你必须先进行架构,然后再考虑设计。
数据绑定
这是Angular.js目前最酷的特性之一,并且秒杀我前文提到的各种需要的DOM操作。不需要你自己动手,Angular.js将自动更新你的视图有木有!
在jQuery里, 我们响应事件并更新内容,大概是这个样子:
$.ajax({
url: '/myEndpoint.json',
success: function ( data, status ) {
$('ul#log').append('<li>Data Received!</li>');
}
});
视图则看上去是这样的:
<ul class="messages" id="log">
</ul>
除了关注点混合的问题,这里同样有之前提到的表征目的的问题。更重要的是,我们不得不手动引用并更新dom节点。并且如果我们想要删除一个日志,我们不得不再次对dom编程操作。我们怎样才能抛开dom来测试逻辑呢?还有,如果我们希望改变展现呢?
真是让人凌乱。。。
但是在Angular.js中,我们可以这样做:
$http( '/myEndpoint.json' ).then( function ( response ) {
$scope.log.push( { msg: 'Data Received!' } );
});
我们的视图看上去是这样的:
<ul class="messages">
<li ng-repeat="entry in log">{{ entry.msg }}</li>
</ul>
但是考虑到刚才提到的问题,我们的视图看上去可以是这样的:
<div class="messages">
<div class="alert" ng-repeat="entry in log">
{{ entry.msg }}
</div>
</div>
现在,替换掉了无序列表,我们使用Bootstrap警告框。同时我们根本不需要改变控制器代码!更重要的是,不论日志何时或者如何更新,视图也会跟着改变。自动的!漂亮!
虽然我没有在这里演示出来,但是数据绑定是双向的。所以这些日志信息同样可以在视图中被编辑,就像这样:
<input ng-model="entry.msg" />
是不是更开心了?
不同的模型层
在jQuery中,dom有点像模型。但是在angualrjs中,我们有一个分离的模型层, 而这个模型层可以让我们用任何方式管理,完全独立于视图。这对于上面说的数据绑定很有帮助, 还可以维护关注点分离,并且引入更多的可测试性。其它的答案提到了这点,所以我这里就不再赘述了。
关注点分离
以上所有的这些把我们带入了这样的主题:保持你的关注点分离。你的视图表现的像记录什么会发生(大部分情况)的正式记录;你的模型表现你的数据;你有一个服务层来执行可重用的任务;你执行dom操作并通过指令扩展你的视图;并且你用控制器来组合这些。这些同样已经在其它答案中提到,我在这里唯一还要提出的一个事情就是可测试性,我会在下文的另一节里专门讨论。
依赖注入
依赖注入是让我们实现关注点分离的方法。如果你是一个服务器端的开发者(从java到php),你可能对这个概念已经非常熟悉了,但是如果你是一个来自jQuery的客户端的朋友,那么你可能会认为这个概念是傻浅挫。但是它可不是:)
从一个更广的观点来看, 依赖注入意味着你可以非常自由的声明组件,然后你可以通过任意其它组件,呼叫一个它的实例,然后授权。
你不需要知道载入顺序,或者文件位置,或者其它类似的东西。这种强大的力量可能不会立刻显现,但是我这里会提供一个(通常)的例子:测试。
比如在我们的应用中,需要一个通过REST API,同时也依赖于应用状态,本地存储实现了服务器端存储的服务。当在我们的控制器上跑测试的时候,我们不希望与服务器端通讯-毕竟我们在测试控制器。我们能够仅仅添加一个与我们原始组件同名的mock服务,注入器将确保我们的控制器自动获取伪造对象–我们的控制器不会也不需要知道它们的区别。
那么既然提到测试……
4. 保持测试驱动的开发
这个其实是关于架构的第三节的一部分,但是这个主题非常重要,所以我需要将其提出来作为自成体系的部分。
那些你看过,用过,写过的所有jQuery插件,它们中有多少有相应的测试包?不是很多吧? 因为jQuery可不是很遵守这个规矩。但是angualrjs是。
在jQuery中, 测试的唯一方法是用示例页面来独立地创建组件,针对该页面我们的测试可以实施dom操作。于是我们就不得不分离地开发一个组件然后将其集成进我们的应用。多么不方便啊!
那么多时间啊,当我们使用jQuery开发时,我们选择使用迭代开发代替测试驱动的开发。可以谁又能怪我们呢?
但是因为我们有关注点分离,我们能够在Angular.js里反复使用测试驱动开发。举个例子,我们想要一个超简单的指令来指示在菜单中我们目前的路径是什么。我们可以在视图中这样声明我们想获取的:
<a href="/hello" when-active>Hello</a>
好了,我们现在可以写个测试:
it( 'should add "active" when the route changes', inject(function() {
var elm = $compile( '<a href="/hello" when-active>Hello</a>' )( $scope );
$location.path('/not-matching');
expect( elm.hasClass('active') ).toBeFalsey();
$location.path( '/hello' );
expect( elm.hasClass('active') ).toBeTruthy();
}));
我们运行测试,并确认它是失败的。那么我们来写下我们的指令:
.directive( 'whenActive', function ( $location ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
scope.$on( '$routeChangeSuccess', function () {
if ( $location.path() == element.attr( 'href' ) ) {
element.addClass( 'active' );
}
else {
element.removeClass( 'active' );
}
});
}
};
});
现在我们的测试通过了,并且我们的菜单按照请求运行。我们的开发是迭代并且测试驱动的,太酷了哦!
5. 从概念上来看,指令不是打包的jQuery
你会经常听到“只在指令里做dom操作”。这是必要的。请对它表示出尊重!
但是让我们谈点更深入的。。。
一些指令只是装饰那些在视图里的已有的(想想ngClass),因此有时候就直接进行dom操作,基本上都能搞定。但是如果一个指令像个“widget”并有一个模板,
那它同样要遵守关注点分离原则。也就是说,这个模板也应该与它在链接和控制器函数里的实现保持最大的独立。
Angular.js自带着一套工具让这件事变得简单; 使用ngClass我们能够动态的更新类;ngBind允许双向的数据绑定;ngShow和ngHide以编程的方式显示或隐藏一个元素;还有更多–包括我们自己写的那些。换句话说, 我们可以不用DOM操作来实现所有的酷炫的事儿。 dom操作越少,指令越容易测试,它们也更容易样式化,在未来它们也更容易改变,并且也变得更加可重用和可分发。
我看到很多Angular.js开发新手将指令当做放置一堆jQuery的地方。换句话说, 他们认为:“既然我不能在控制器里做dom操纵,那么我就把这段代码放到指令里”。当然这看上去好多了,但是通常这仍然是错的。
想一下在第三节里我们编写的日志记录。即使我们将其放到一个指令里,我们仍然希望用“anggualjs方式”来做这件事情。这仍然没有做任何dom操作!在很多情况下dom操作是必须的,但是这种情况其实比你想的少得多!在你在你的应用中到处使用dmo操作之前,问问你自己,这真的是必须的吗。可能有更好的方法呢。
这里用一个简单的例子来展示我们经常会看到的一个模式。我们需要一个切换按钮。(注意:这个例子有那么一点人为设计的技巧并且有点啰嗦,但是很多更复杂的情况其实也完全可以根据这个例子的方式来解决。)
.directive( 'myDirective', function () {
return {
template: '<a class="btn">Toggle me!</a>',
link: function ( scope, element, attrs ) {
var on = false;
$(element).click( function () {
if ( on ) {
$(element).removeClass( 'active' );
}
else {
$(element).addClass( 'active' );
}
on = !on;
});
}
};
});
这里有一些错误。第一,jQuery不是必须的。我们这里做的一切都不需要jQuery!第二, 即使是我们的页面上已经有jQuery,也没有理由一定要在这里使用它;我们可以简单的使用angular.element,而且就算在一个没有jQuery的项目中我们的组件仍然可以工作。第三,即使我们假设为了让这个指令工作,jQuery是必须的,如果jqury被加载,那么jqLite(angler.element)一定会使用jQuery。所以我们不需要使用$(译者注:jQuery的一个标识符号,是jQuery函数的别名)–我们可以使用angular.element。第四, 紧接第三点,jqLite元素不需要被包裹在$里–传递给link函数的element已经是一个jQuery元素!第五, 我上一节已经提过的,为什么我们要将模板混合进我们的逻辑呢?
这个指令可以更精简的重写(即时在更复杂的例子里也一样):
.directive( 'myDirective', function () {
return {
scope: true,
template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
link: function ( scope, element, attrs ) {
scope.on = false;
scope.toggle = function () {
scope.on = !$scope.on;
};
}
};
});
再次强调,模板的那些代码都是在模板里的,所以你或者你的使用者能够很方便的将其换成一个符合任何需要的样式,同时逻辑不被改变。这就是重用性-赞!
当然,还有其它很多好处–比如测试 – 这很容易! 不论什么在模板中,指令的内部API绝不会被接触,所以重构就变得容易。你可以在不接触指令的情况下随意改变你的模板。并且不论你怎么变,你的测试仍可以通过。
耶!
所以如果指令不只是一组jQuery风格的函数,那么它们是什么呢?指令其实就是html的扩展。如果html不能完成你希望它做的一些事情,你就写一个指令来做,并且当它是html的一部分来使用。
换句话说, 如果Angular.js一下子无法完成手头的工作,想想看你的团队是否能用ngClick、ngClass等来搞定它。
总结
别用jQuery。甚至都别引用它。它会阻碍你。当你有个问题已经知道怎么用jQuery来解决的时候,在你将手伸向$的时候,试试能不能在Angular.js的规范内解决。如果你不知道,就问!
十有八九最佳的方法是不需要jQuery的,而当你选择用jQuery的时候反而会导致更多的工作呢。