AICon全球人工智能与机器学习技术大会周四开幕,点击查看完整日程>> 了解详情
写点什么

StackOverFlow 精彩问答赏析:有 jQuery 背景的开发者如何建立起 AngularJS 的思维模式?

  • 2013 年 11 月 22 日
  • 本文字数:5815 字

    阅读完需:约 19 分钟

【编辑注】本文来自 StackOverFlow 上 How do I “think in AngularJS” if I have a jQuery background? 一题中得票最高的回答。该回答得票超过 3000 次,回答者 Josh David Miller 是活跃于开源社区的开发者,也是 Emergenesis 公司的联合创始人。该答案最初由数云架构师韩铮翻译并发布在自己的博客上,在征得Josh 同意后由韩铮本人推荐给 InfoQ 进行分享,并在经过InfoQ 社区编辑崔康审校后发布在此。

1. 不要先设计页面,然后再使用 DOM 操作来改变它的展现

在 jQuery 中,你通常会设计一个页面,然后再给它动态效果。这是因为 jQuery 的设计就是为了扩充 DOM 并在这个简单的前提下疯狂的生长的。

但是在 AngularJS 里,必须从头开始就在头脑中思考架构。必须从你想要完成的功能开始,然后设计应用程序,最后来设计视图,而非“我有这么一个 DOM 片段,我想让他可以实现 XXX 效果”。

2. 不要用 AngularJS 来加强 jQuery

类似的,不要以这样的思维开始:用 jQuery 来做 X,Y 和 Z,然后只需要把 AngularJS 的 models 和 controllers 加在这上面。这在刚开始的时候显得非常诱人,这也是为什么我总是建议 AngularJS 的新手完全不使用 jQuery,至少不要在习惯使用“Angular Way”开发之前这么做。

我在邮件列表里看到很多开发者使用 150 或 200 行代码的 jQuery 插件创造出这些复杂的解决方案,然后使用一堆 callback 函数以及 $apply 把它粘合到 AngularJS 里,看起来复杂难懂;但是他们最终还是把它搞定了!问题是在大多数情况下这些 jQuery 插件可以使用很少的 AngularJS 代码重写,而且所有的一切都很简单直接容易理解。

这里的底线是:当你选择解决方案时,首先“think in AngularJS”;如果想不出一个解决方案,去社区求助;如果还是没有简单的解决方案,再考虑使用 jQuery。但是不要让 jQuery 成为你的拐杖,导致你永远无法真正掌握 AngularJS。

3. 总是以架构的角度思考

首先要知道 Single-page 应用是应用,不是网页。所以我们除了像一个客户端开发者般思考外,还需要像一个服务器端开发者一样思考。我们必须考虑如何把我们的应用分割成独立的,可扩展且可测试的组件。

那么如何做到呢?如何“think in AngularJS”?这里有一些基本原则,对比 jQuery。

视图是“Official Record”

在 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();

当我们只关注视图,这里不会立即明显的体现出任何(业务)功能。对于小型应用,这没什么不妥。但是在规模较大的应用中,事情就会变得难以理解且难以维护。

而在 AngularJS 里,视图是基于视图的功能。ul 声明就会像这样:

复制代码
<ul class="main-menu" dropdown-menu> ... </ul>

这两种方式做了同样的东西,但是在 AngularJS 的版本里任何人看到这个模版都可以知道将会发生什么事。不论何时一个新成员加入开发团队,他看到这个就会知道有一个叫做 dropdownMenu 的 directive 作用在这个标签上;他不需要靠直觉去猜测代码的功能或者去看任何代码。视图本身告诉我们会发生什么事。清晰多了。

首次接触 AngularJS 的开发者通常会问这样一个问题:如何找到所有的某类元素然后给它们加上一个 directive。但当我们告诉他:别这么做时,他总会显得非常的惊愕。而不这么做的原因是这是一种半 jQuery 半 AngularJS 的方式,这么做不好。这里的问题在于开发者尝试在 AngularJS 的环境里“do jQuery”。这么做总会有一些问题。视图是 official record(译者注:作者可能想表达视图是一等公民)。在一个 directive 外,绝不要改变 DOM。所有的 directive 都应用在试图上,意图非常清晰。

记住:不要设计,然后写标签。你需要架构,然后设计。

数据绑定

这是到现在为止最酷的 AngularJS 特性。这个特性使得前面提到的很多 DOM 操作都显得不再需要。AngularJS 会自动更新视图,所以你自己不用这么做!在 jQuery 里,我们响应事件然后更新内容,就像这样:

复制代码
$.ajax({
url: '/myEndpoint.json',
success: function ( data, status ) {
$('ul#log').append('<li>Data Received!</li>');
}
});

对应的视图:

复制代码
<ul class="messages" id="log"> </ul>

除了要考虑多个方面,我们也会遇到前面视图中的问题。但是更重要的是,需要手动引用并更新一个 DOM 节点。如果我们想要删除一个 log 条目,也需要针对 DOM 编码。那么如何脱离 DOM 来测试这个逻辑?如果想要改变展现形式怎么办?

这有一点凌乱琐碎。但是在 AngularJS 里,可以这样来实现:

复制代码
$http('/myEndpoint.json').then(function (response) {
$scope.log.push({
msg: 'Data Received!'
});
});

视图看起来是这个样子的:

复制代码
<ul class="messages"> <li ng-repeat="entry in log"></li> </ul>

但是其实还可以这样来做:

复制代码
<div class="messages"> <div class="alert" ng-repeat="entry in log"> </div> </div>

现在如果我们想使用 Bootstrap 的 alert boxes,而不是一个无序列表,根本不需要改变任何的 controller 代码!更重要的是,不论 log 在何处或如何被更新,视图便会随之更新。自动的。巧妙!

尽管我没有在这里展示,数据绑定其实是双向的。所以这些 log 信息在视图里也可以是可编辑的。只需要这么做:

复制代码
<input ng-model="entry.msg" />

。简单快乐。

清晰的模型(Model)层

在 jQuery 里,DOM 在一定程度上扮演了模型的角色。但在 AngularJS 中,我们有一个独立的模型层可以灵活的管理。完全与视图独立。这有助于上述的数据绑定,维护了关注点的分离(独立的考虑视图和模型),并且引入了更好的可测性。后面还会提到这点。

关注点分离

上面所有的内容都与这个愿景相关:保持你的关注点分离。视图负责展现将要发生的事情;模型表现数据;有一个 service 层来实现可复用的任务;在 directive 里面进行 DOM 操作和扩展;使用 controller 来把上面的东西粘合起来。这在其他的答案里也有叙述,我在这里只增加关于可测试性的内容,在后面的一个段落里详述。

依赖注入

依赖注入帮我们实现了关注点分离。如果你来自一个服务器语言(java 或 php),可能对这个概念已经非常熟悉,但是如果你是一个来自 jQuery 的客户端开发者,这个概念可能看起来有点傻而多余。但其实不是的。。。

大体来讲,DI 意味着可以非常自由的声明组件,然后在另一个组件里,只需要请求一个该组件的实例,就可以得到它。不需要知道(关心)加载顺序,或者文件位置,或类似的事情。这种强大可能不会立刻显现,但是我只提供一个(常见。。)的例子:测试。

就说在你的应用里,我们需要一个服务通过 REST API 来实现服务器端存储,并且根据不同的应用状态,也有可能使用(客户端)本地存储。当我们运行 controller 的测试时,不希望必须和服务器交互 —— 毕竟是在测试 controller 逻辑。我们可以只添加一个与本来使用的 service 同名的 mock service,injector 会确保 controller 自动得到假的那个 service —— controller 不会也不需要知道有什么不同。

说起测试……

4. 总是 —— 测试驱动开发

这其实是关于架构的第 3 节。但是它太重要了,所以我把它单独拿出来作为一个顶级段落。

在所有那些你见过,用过或写过的 jQuery 插件中,有多少是有测试集的?不多,因为 jQuery 经不起测试的考验。但是 AngularJS 可以。

在 jQuery 中,唯一的测试方式通常是独立地创建附带 sample/demo 页面的组件,然后我们的测试在这个页面上做 DOM 操作。所以我们必须独立的开发一个组件,然后集成到应用里。多不方便!在使用 jQuery 开发时,太多的时间,我们挑选迭代而非测试驱动开发。谁又能责怪我们呢?

但是因为有了关注点分离,我们可以在 AngularJS 中迭代地做测试驱动开发!例如,想要一个超级简单的 directive 来展现我们的当前路径。可以在视图里声明:

复制代码
<a href="/hello" when-active>Hello</a>

OK,现在可以写一个测试:

复制代码
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 了:

复制代码
.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');
}
});
}
};
});

测试现在通过了,然后我们的 menu 按照请求的方式执行。开发过程既是迭代的也是测试驱动的。太酷了。

5. 概念上,Directives 并不是打包的 jQuery

你经常会听到“只在 directive 里做 DOM 操作”。这是必需的。请给它应有的尊重!

但让我们再深入一点……

一些 directive 仅仅装饰了视图中已经存在的东西(想想 ngClass)并且因此有时候仅仅直接做完 DOM 操作然后就完事了。但是如果一个 directive 像一个“widget”并且有一个模版,那么它也要做到关注点分离。也就是说,模版本身也应该很大程度上与其 link 和 controller 实现保持独立。

AngularJS 拥有一整套工具使这个过程非常简单 ; 有了 ngClass 我们可以动态地更新 class;ngBind 使得我们可以做双向数据绑定。ngShow 和 ngHide 可编程地展示和隐藏一个元素;以及更多地 —— 包括那些我们自己写的。换句话说,我们可以做到任何 DOM 操作能实现的特性。DOM 操作越少,directive 就越容易测试,也越容易给它们添加样式,在未来也越容易拥抱变化,并且更加的可复用和发布。

我见过很多 AngularJS 新手,把一堆 jQuery 扔到 directive 里。换句话说,他们认为“因为不能在 controller 里做 DOM 操作,就把那些代码弄到 directive 里好了”。虽然这么做确实好一些,但是依然是错误的。

回想一下我们在第 3 节里写的那个 logger。即使要把它放在一个 directive 里,我们依然希望用“Angular Way”来做。它依然没有任何 DOM 操作!有很多时候 DOM 操作是必要的,但其实比你想的要少得多!在应用里的任何地方做 DOM 操作之前,问问你自己是不是真的需要这么做。有可能有更好的方式。

这里有一个示例,展示出了我见过最多的一种模式。我们想做一个可以 toggle 的按钮。(注意:这个例子有一点牵强、啰嗦,这是为了表达出使用同样方式处理问题的更复杂的情况。)

复制代码
.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;
});
}
};
});

这里有一些错误的地方。首先,jQeury 根本没必要出现。我们在这里做的事情都根本用不着 jQuery!其次,即使已经将 jQuery 用在了页面上,也没有理由用在这里。第三,即使假设这个 directive 依赖 jQuery 来工作,jqLite(angular.element) 在加载后总会使用 jQuery!所以我们没必要使用 $ —— 用 angular.element 就够了。第四,和第三条紧密关联,jqLite 元素不需要被 $ 封装 —— 传到 link 里的元素本来就会是一个 jQuery 元素!第五,我们在前面段落中说过,为什么要把模版的东西混到逻辑里?

这个 directive 可以(即使是更复杂的情况下!)写得更简单:

复制代码
.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;
};
}
};
});

再一次地,模版就在模版里,当有样式需求时,你(或你的用户)可以轻松的换掉它,不用去碰逻辑。重用性 —— boom!

当然还有其他的好处,像测试 —— 很简单!不论模版中有什么,directive 的内部 API 从来不会被碰到,所以重构也很容易。可以不碰 directive 就做到任意改变模版。不论你怎么改,测试总是通过的。

所以如果 directive 不仅仅是一组类似 jQuery 的函数,那他们是什么?Directive 实际是 HTML 的扩展。如果 HTML 没有做你需要它做的事情,你就写一个 directive 来实现,然后就像使用 HTML 一样使用它。

换句话说,如果 AngularJS 库没有做的一些事情,想想开发团队会如何完成它来配合 ngClick,ngClass 等。

总结

不要用 jQuery。连 include 也不要。它会让你停滞不前。如果遇到一个你认为已经知道如何使用 jQuery 来解决的问题,在使用 $ 之前,试试想想如何在 AngularJS 的限制下解决它。如果你不知道,问!20 次中的 19 次,最好的方式不需要 jQuery。如果尝试使用 jQuery 会增加你的工作量。


这是我目前最长的 Stack Overflow 回答。事实上,这个答案太长了,我都要填一个 Captcha 了。但是就如我常说的:能说多时候说的少其实就是懒。

希望这个答案对你有用。

原文英文地址: http://stackoverflow.com/questions/14994391/how-do-i-think-in-angularjs-if-i-have-a-jquery-background


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2013 年 11 月 22 日 17:1615027

评论

发布
暂无评论
发现更多内容

头条二面:你们公司怎么处理MySQL的 Binlog 日志?

Java架构师迁哥

智慧党建系统搭建,干部管理平台开发

13823153121

时代之盾,国之重器:华为带给政务数据保护的新可能

脑极体

2021字节大厂面经分享:Java面试高分宝典!GitHub已标星86.7K

比伯

Java 编程 架构 面试 计算机

javascript

wudaxue

JavaScript

dubbo几个问题整理:优雅停机、线程池配置

程序员架构进阶

dubbo 28天写作 4月日更 优雅停机 线程配置

Redis split-brain 脑裂

escray

redis 学习 极客时间 Redis 核心技术与实战 4月日更

Python OpenCV 之图像乘除与像素的逻辑运算,图像处理取经之旅第 17 天

梦想橡皮擦

Python OpenCV 4月日更

重点人员可视化管理平台搭建,公安指挥调度平台

13823153121

制作颜色选择器(全)

空城机

JavaScript Vue 前端 4月日更 颜色选择器

从小白程序员到大厂高级技术专家我看过哪些书籍?

冰河

程序员 程序人生 冰河 推荐书单

升级dubbo,小心default.version

捉虫大师

Java dubbo

新基建:“区块链+物联网”,是否生活将会改变?

电微13828808271

物联网 区块链标准

手写函数

wudaxue

JavaScript vue.js

Linux chmod命令

一个大红包

4月日更

树莓派简介

IT蜗壳-Tango

IT蜗壳教学 4月日更

不想写代码偷懒之配置化

顿晓

前端 配置化开发 4月日更

区块链赋能文化旅游,推动旅游行业转型升级

13828808769

区块链 #区块链#

智慧公安重点人员管控系统搭建,助推公安智慧化发展

13828808769

区块链+ #区块链#

基于角色访问控制RBAC权限模型的动态资源访问权限管理实现

crudapi

spring security 权限 rbac crudapi 角色

【死磕JVM】给同事讲了一遍GC后,他要去面试,年轻人,就是容易冲动!

牧小农

JVM 垃圾回收 垃圾收集 垃圾回收算法

架构实战营 - 模块1 - 作业

Ming

软件架构

大数据计算生态之数据计算(一)

小舰

4月日更

打完新冠疫苗后要注意的两件事

石云升

28天写作 新冠疫苗 4月日更

Apache Oozie基本原理与工作流类型

大数据技术指南

大数据 oozie 4月日更

人生好走的路那么多,你偏要选这条难走的走

小天同学

自我思考 个人感悟 人生修炼 4月日更

Kafka又出问题了!

冰河

kafka 消息队列 消息中间件 异步编程

新版犀牛书该不该入手?

清秋

JavaScript ecmascript ES6 技术书籍 4月日更

云计算防火墙对象(组)的实践

Python研究所

云计算 防火墙 对象组

Java一套拳法刷掉n个遍历树的问题 John 易筋 ARTS 打卡 Week 43

John(易筋)

ARTS活动

区块链技术推动自然资源领域信息化发展

13828808769

区块链+ #区块链#

数据cool谈(第2期)寻找下一代企业级数据库

数据cool谈(第2期)寻找下一代企业级数据库

StackOverFlow精彩问答赏析:有jQuery背景的开发者如何建立起AngularJS的思维模式?-InfoQ