RxJS 快速入门

这是一篇给新手的 RxJS 快速入门,它可能不精确、不全面,但力求对新手友好。

异步与“回调地狱”

我们都知道 JavaScript 是个多范式语言,它既支持过程式编程,又支持函数式编程,两者分别适用于不同的场合。在同步环境下,两者各有优缺点,甚至有时候过程式会更简明一些,但在异步环境下(最典型的场景是一个 Ajax 请求完成后紧接着执行另一个 Ajax 请求),由于无法控制执行和完成的顺序,所以就无法使用传统的过程式写法,函数式就会展现出其优势。

问题在于,传统的函数式写法实在太不友好了。

传统写法下,当我们调用一个 Ajax 时,就要给它一个回调函数,这样当 Ajax 完成时,就会调用它。当逻辑简单的时候,这毫无问题。但是我要串起 10 个 Ajax 请求时该怎么办呢?十重嵌套吗?恩?似乎有点不对劲儿!

这就是回调地狱。

不仅如此,有时候我到底需要串起多少个 Ajax 请求是未知的,要串起哪些也同样是未知的。这已经不再是地狱,而是《Mission: Impossible》了。

我,承诺(Promise),帮你解决

事实上,这样的问题早在 1976 年就已经被发现并解决了。注意,我没写错,确实是 1976 年。

承诺,英文是 Promise [ˈprɑmɪs],它的基本思想是借助一个代表回执的变量来把回调地狱拍平。

我们以购物为例来看看日常生活中的承诺。

  1. 你去电商平台下单,并付款
  2. 平台会给你一个订单号,这个订单号本质上是一个回执,代表商家做出了“稍后我将给你发货”的承诺
  3. 商家发货给你,在这个过程中你不用等待(异步)
  4. 过一段时间,快递到了
  5. 你签收(回调函数被调用)商品(回调参数)
  6. 这次承诺结束

这是最直白的单步骤回调,如果理解了它,再继续往下看。

你跟电商下的单,但是却从快递(并不属于商家)那里接收到了商品,仔细想想,你不觉得奇怪吗?虽然表面看确实是商家给你的商品,但我们分解开中间步骤就会发现还有一些幕后的步骤。

  1. 商家把商品交给快递公司,给快递公司一个订单号(老的回执)并拿回一个运单号(新的回执)
  2. 快递公司执行这个新承诺,这个过程中商家不用等待(异步)
  3. 快递公司完成这个新承诺,你收到这个新承诺携带的商品

所以,事实上,这个购物流程包括两个承诺:

  1. 商家对你的一个发货承诺
  2. 快递公司对商家的运货承诺

因此,只要把这些承诺串起来,这些异步动作也就同样串起来了。

当我们把每个承诺都抽象成一个对象时,我们就可以对任意数量、任意顺序的承诺进行组合,变成一个新的承诺。因此回调地狱不复存在,前述的 Mission 也变得 Possible 了。

Promise 的缺点

Promise 固然是一个重大的进步,但在有些场景下仍然是不够的。比如,Promise 的特点是无论有没有人关心它的执行结果,它都会立即开始执行,并且你没有机会取消这次执行。显然,在某些情况下这么做是浪费的甚至错误的。仍然以电商为例,如果某商户的订单不允许取消,你还会去买吗?再举个编程领域的例子:如果你发起了一个 Ajax 请求,然后用户导航到了另一个路由,显然,你这个请求如果还没有完成就应该被取消,而不应该发出去。但是使用 Promise,你做不到,不是因为实现方面的原因,而是因为它在概念层(接口定义上)就无法支持取消。

此外,由于 Promise 只会承载一个值,因此当我们要处理的是一个集合的时候就比较困难了。比如对于一个随机数列(总数未知),如果我们要借助 Web API 检查每个数字的有效性,然后对前一百个有效数字进行求和,那么用 Promise 写就比较麻烦了。

我们需要一个更高级的 Promise。

Observable

它就是可观察对象(Observable [əbˈzɜrvəbl]),Observable 顾名思义就是可以被别人观察的对象,当它变化时,观察者就可以得到通知。换句话说,它负责生产数据,别人可以消费它生产的数据。

如果你是个资深后端,那么可能还记得 MessageQueue 的工作模式,它们很像。如果不懂 MQ 也没关系,我还是用日常知识给你打个比方。

Observable 就像个传送带。这个传送带不断运行,围绕这个传送带建立了一条生产线,包括一系列工序,不同的工序承担单一而确定的职责。每个工位上有一个工人。

整个传送带的起点是原料箱,原料箱中的原料不断被放到传送带上。工人只需要待在自己的工位上,对面前的原料进行加工,然后放回传送带上或放到另一条传送带上即可,简单、高效、无意外 —— 符合程序员的审美。

而且这个生产线还非常先进 —— 不接单就不生产,非常有效地杜绝了浪费。

FRP

这种设计,看上去很美,对吧?但光看着漂亮可不行,在编程时要怎么实现呢?实际上,这是一种编程范式,叫做函数响应式编程(FRP)。它比 Promise 可年轻多了,直到 1997 年被人提出来。

顾名思义,FRP 同时具有函数式编程和响应式编程的特点。响应式编程是什么呢?形象的说,它的工作模式就是“饭来张口,衣来伸手”,也就是说,等待外界的输入,并做出响应。流水线每个工位上的工人正是这种工作模式。

工业上,流水线是人类管理经验的结晶,它所做的事情是什么呢?本质上就是把每个处理都局部化,以减小复杂度(降低对工人素质的要求)。而这,正是软件行业所求之不得的。响应式,就是编程领域的流水线。

那么函数式呢?函数式最显著的特征就是没有副作用,而这恰好是对流水线上每个工序的要求。显然,如果某个工序的操作会导致整个生产线平移 10 米,那么用不了多久这个生产线就要掉到海里了,这样的生产线毫无价值。

因此,响应式和函数式几乎是注定要在一起的。

ReactiveX

2012 年,微软 .NET 开发组的一个团队为了给 LinQ 设计扩展机制而引入了 FRP 概念,却发现 FRP 的价值不止于此。于是一个新的项目出现了,它就是 ReactiveX。

严格来说 ReactiveX 应该是一 FRP 库,因为它几乎在每个主流语言下都提供了实现,而且这些实现都是语言原生风格的,不是简单地迁移。如果你在任何语言下用过带有 Rx 前缀的库,那多半儿就是 ReactiveX 的一个实现了,如 RxJava、Rx.NET、RxGroovy、RxSwift 等等。

ReactiveX 本身其实并不难,难的是 FRP 编程范式以及对操作符(operator)的理解。所以,只要学会了任何一个 Rx* 库,那么其它语言的库就可以触类旁通了。

宝石图

为了帮助开发者更容易地理解 ReactiveX 的工作原理,ReactiveX 开发组还设计了一种很形象的图,那就是宝石图。这货长这样(英文注释不必细看,接下来我会简单解释下):

中间的带箭头的线就像传送带,用来表示数据序列,这个数据序列被称为“流”。上方的流叫做输入流,下方的流叫做输出流。输入流可能有多个,但是输出流只会有一个(不过,流中的每个数据项也可以是别的流)。

数据序列上的每个圆圈表示一个数据项,圆圈的位置表示数据出现的先后顺序,但是一般不会表示精确的时间比例,比如在一毫秒内接连出现的两个数据之间仍然会有较大的距离。只有少数涉及到时间的操作,其宝石图才会表现出精确的时间比例。

圆圈的最后,通常会有一条竖线或者一个叉号。竖线表示这个流正常终止了,也就是说不会再有更多的数据提供出来了。而叉号表示这个流抛出错误导致异常中止了。还有一种流,既没有竖线也没有叉号,这种叫做无尽流,比如一个由所有自然数组成的流就不会主动终止。但是要注意,无尽流仍然是可以处理的,因为需要多少项是由消费者决定的。你可以把这个“智能”传送带理解为由下一个工位“叫号”的,没“叫号”下一项数据就不会过来。

中间的大方框表示一个操作,也就是 operator —— 一个函数,比如这个图中的操作就是把输入流中的条目乘以十后放入输出流中。

看懂了宝石图,就能很形象的理解各种操作符了。

RxJS

主角登场了。RxJS 就是 ReactiveX 在 JavaScript 语言上的实现。对于 JavaScript 程序员来说,不管你是前端还是 NodeJS 后端,RxJS 都会令你受益。

由于 JavaScript 本身的缺陷,RxJS 不得不采用了很多怪异的写法。它对于 Java / C# 等背景的程序员来说可能会显得比较怪异,不过,你可以先忽略它们,聚焦在编程范式和接下来要讲的操作符语义上。

典型的写法

of(1,2,3).pipe(
  filter(item=>item % 2 === 1),
  map(item=>item * 3),
).subscribe(item=> console.log(item))
`</pre>

它会输出:
<pre>`3
9

其中 of 称为创建器(creator),用来创建流,它返回一个 Observable 类型的对象,filter 和 map 称为操作符(operator),用来对条目进行处理。这些操作符被当作 Observable 对象的 pipe 方法的参数传进去。诚然,这个写法略显怪异,不过这主要是被 js 的设计缺陷所迫,它已经是目前 js 体系下多种解决方案中相对好看的一种了。

Observable 对象的 subscribe 方法表示消费者要订阅这个流,当流中出现数据时,传给 subscribe 方法的回调函数就会被调用,并且把这个数据传进去。这个回调函数可能被调用很多次,取决于这个流中有多少条数据。

注意,Observable 必须被 subscribe 之后才会开始生产数据。如果没人 subscribe 它,那就什么都不会做。

简单创建器

广义上,创建器也是操作符的一种,不过这里我们把它单独拿出来讲。要启动生产线,我们得先提供原料。本质上,这个提供者就是一组函数,当流水线需要拿新的原料时,就会调用它。

你当然可以自己实现这个提供者,但通常是不用的。RxJS 提供了很多预定义的创建器,而且将来可能还会增加新的。不过,那些眼花缭乱的创建器完全没必要全记住,只要记住少数几个就够了,其它的有时间慢慢看。

of – 单一值转为流

它接收任意多个参数,参数可以是任意类型,然后它会把这些参数逐个放入流中。

from – 数组转为流

它接受一个数组型参数,数组中可以有任意数据,然后把数组的每个元素逐个放入流中。

range – 范围转为流

它接受两个数字型参数,一个起点,一个终点,然后按 1 递增,把中间的每个数字(含边界值)放入流中。

fromPromise – Promise 转为流

接受一个 Promise,当这个 Promise 有了输出时,就把这个输出放入流中。

要注意的是,当 Promise 作为参数传给 fromPromise 时,这个 Promise 就开始执行了,你没有机会防止它被执行。

如果你需要这个 Promise 被消费时才执行,那就要改用接下来要讲的 defer 创建器。

defer – 惰性创建流

它的参数是一个用来生产流的工厂函数。也就是说,当消费方需要流(注意不是需要流中的值)的时候,就会调用这个函数,创建一个流,并从这个流中进行消费(取数据)。

因此,当我们定义 defer 的时候,实际上还不存在一个真正的流,只是给出了创建这个流的方法,所以叫惰性创建流。

timer – 定时器流

它有两个数字型的参数,第一个是首次等待时间,第二个是重复间隔时间。从图上可以看出,它实际上是个无尽流 —— 没有终止线。因此它会按照预定的规则往流中不断重复发出数据。

要注意,虽然名字有相关性,但它不是 setTimeout 的等价物,事实上它的行为更像是 setInterval

interval – 定时器流

它和 timer 唯一的差别是它只接受一个参数。事实上,它就是一个语法糖,相当于 timer(1000, 1000),也就是说初始等待时间和间隔时间是一样的。

如果需求确实是 interval 的语义,那么就优先使用这个语法糖,毕竟,从行为上它和 setInterval 几乎是一样的。

思考题:假设点了一个按钮之后我要立刻开始一个动作,然后每隔 1000 毫秒重复一次,该怎么做?换句话说:该怎么移除首次延迟时间?

Subject – 主体对象

它和创建器不同,创建器是供直接调用的函数,而 Subject 则是一个实现了 Observable 接口的类。也就是说,你要先把它 new 出来(假设实例叫 subject),然后你就可以通过程序控制的方式往流里手动放数据了。它的典型用法是用来管理事件,比如当用户点击了某个按钮时,你希望发出一个事件,那么就可以调用 subject.next(someValue) 来把事件内容放进流中。

当你希望手动控制往这个流中放数据的时机时,这种特性非常有用。

当然,Subject 其实并没有这么简单,用法也很多,不过这部分内容超出了本文的范围。

合并创建器

我们不但可以直接创建流,还可以对多个现有的流进行不同形式的合并,创建一个新的流。常见的合并方式有三种:并联、串联、拉链。

merge – 并联

从图上我们可以看到两个流中的内容被合并到了一个流中。只要任何一个流中出现了值就会立刻被输出,哪怕其中一个流是完全空的也不影响结果 —— 等同于原始流。

这种工作方式非常像电路中的并联行为,因此我称其为并联创建器。

并联在什么情况下起作用呢?举个例子吧:有一个列表需要每隔 5 秒钟定时刷新一次,但是一旦用户按了搜索按钮,就必须立即刷新,而不能等待 5 秒间隔。这时候就可以用一个定时器流和一个自定义的用户操作流(subject)merge 在一起。这样,无论哪个流中出现了数据,都会进行刷新。

concat – 串联

从图中我们可以看到两个流中的内容被按照顺序放进了输出流中。前面的流尚未结束时(注意竖线),后面的流就会一直等待。

这种工作方式非常像电路中的串联行为,因此我称其为串联创建器。

串联的适用场景就很容易想象了,比如我们需要先通过 Web API 进行登录,然后取学生名册。这两个操作就是异步且串联工作的。

zip – 拉链

zip 的直译就是拉链,事实上,有些压缩软件的图标就是一个带拉链的钥匙包。拉链的特点是两边各有一个“齿”,两者会啮合在一起。这里的 zip 操作也是如此。

从图上我们可以看到,两个输入流中分别出现了一些数据,当仅仅输入流 A 中出现了数据时,输出流中什么都没有,因为它还在等另一个“齿”。当输出流 B 中出现了数据时,两个“齿”都凑齐了,于是对这两个齿执行中间定义的运算(取 A 的形状,B 的颜色,并合成为输出数据)。

可以看到,当任何一个流先行结束之后,整个输出流也就结束了。

拉链创建器适用的场景要少一些,通常用于合并两个数据有对应关系的数据源。比如一个流中是姓名,另一个流中是成绩,还有一个流中是年龄,如果这三个流中的每个条目都有精确的对应关系,那么就可以通过 zip 把它们合并成一个由表示学生成绩的对象组成的流。

操作符

RxJS 有很多操作符,事实上比创建器还要多一些,但是我们并不需要一一讲解,因为它们中的很大一部分都是函数式编程中的标配,比如 map、reduce、filter 等。有 Java 8 / scala / kotlin 等基础的后端或者用过 underscore/lodash 的前端都可以非常容易地理解它们。

本文重点讲解一些传统方式下没有的或不常用的:

retry – 失败时重试

有些错误是可以通过重试进行恢复的,比如临时性的网络丢包。甚至一些流程的设计还会故意借助重试机制,比如当你发起请求时,如果后端发现你没有登录过,就会给你一个 401 错误,然后你可以完成登录并重新开始整个流程。

retry 操作符就是负责在失败时自动发起重试的,它可以接受一个参数,用来指定最大重试次数。

这里我为什么一直在强调失败时重试呢?因为还有一个操作符负责成功时重试。

repeat – 成功时重试

除了重复的条件之外,repeat 的行为几乎和 retry 一模一样。

repeat 很少会单独用,一般会组合上 delay 操作,以提供暂停时间,否则就容易 DoS 了服务器。

delay – 延迟

这才是真正的 setTimeout 的等价操作。它接受一个毫秒数(图中是 20 毫秒),每当它从输入流中读取一个数据之后,会先等待 20 毫秒,然后再放到输出流中。

可以看到,输入流和输出流内容是完全一样的,只是时机上,输出流中的每个条目都恰好比输入流晚 20 毫秒出现。

toArray – 收集为数组

事实上,你几乎可以把它看做是 from 的逆运算。 from 把数组打散了逐个放进流中,而 toArray 恰好相反,把流中的内容收集到一个数组中 —— 直到这个流结束。

这个操作符几乎总是放在最后一步,因为 RxJS 的各种 operator 本身就可以对流中的数据进行很多类似数组的操作,比如查找最小值、最大值、过滤等。所以通常会先使用各种 operator 对数据流进行处理,等到要脱离 RxJS 的体系时,再转换成数组传出去。

debounceTime – 防抖

在 underscore/lodash 中这是常用函数。 所谓防抖其实就是“等它平静下来”。比如预输入(type ahead)功能,当用户正在快速打字的时候,你没必要立刻去查服务器,否则可能直接让服务器挂了,而应该等用户稍作停顿(平静下来)时再发起查询。

debounceTime 就是这样,你传入一个最小平静时间,在这个时间窗口内连续过来的数据一概被忽略,一旦平静时间超过它,就会往把接收到的下一条数据放到流中。这样消费者就只能看到平静时间超时之后发来的最后一条数据。

switchMap – 切换成另一个流

这可能是相对较难理解的一个 operator。

有时候,我们会希望根据一个立即数发起一个远程查询,并且把这个异步取回的结果放进流中。比如,流中是一些学生的 id,每过来一个 id,你要发起一个 Ajax 请求来根据这个 id 获取这个学生的详情,并且把详情放进输出流中。

注意,这是一个异步操作,所以你没法用普通的 map 来实现,否则映射出来的结果就会是一个个 Observable 对象。

switchMap 就是用来解决这个问题的。它在回调函数中接受从输入流中传来的数据,并转换成一个新的 Observable 对象(新的流,每个流中包括三个值,每个值都等于输入值的十倍),switchMap 会订阅这个 Observable 对象,并把它的值放入输出流中。注意图中竖线的位置 —— 只有当所有新的流都结束时,输出流才会结束。

不知道你有没有注意到这里一个很重要的细节。30 只生成了两个值,而不是我们所预期的三个。这是因为当输入流中的 5 到来时,会切换到以 5 为参数构建出的这个新流(S5),而这时候基于 3 构建的那个流(S3)尚未结束。虽然如此,但是已经没人再订阅 S3 了,因为同一时刻 switchMap 只能订阅一个流。所以,已经没人会再朝着 S3 “叫号”了,它已经被释放了。

规律:operator 打包学

当你掌握了一些基本操作符之后,就可以让自己的操作符知识翻倍了。

这是因为 RxJS 中的很多操作符都遵循着同样的命名模式。比如:

xxxWhen – 满足条件时 xxx

它接受一个 Observable 型参数作为条件流,一旦这个条件流中出现任意数据,则进行 xxx 操作。

retryWhen(notifier$),其中的 notifier$ 就是一个条件流。当输入流出现异常时,就会开始等待 notifier$ 流中出现数据,一旦出现了任何数据(不管是什么值),就会开始执行重试逻辑。

xxxCount – 拿到 n 个数据项时 xxx

它接受一个数字型参数作为阈值,一旦从输入流中取到了 n 个数据,则进行 xxx 操作。

bufferCount(3) 表示每拿到 3 个数据就进行一次 buffer 操作。

这个操作可以看做是 xxxWhen 的语法糖。

xxxTime – 超时后 xxx

它接受一个超时时间作为参数,从输入流中取数据,一旦到达超时时间,则执行 xxx 操作。

比如前面讲过的 debounceTime 其实遵循的就是这种模式。

这个操作可以看做 xxxWhen 的语法糖。

xxxTo – 用立即量代替 Lambda 表达式

它接受一个立即量作为参数,相当于 xxx(()=&gt;value))

比如 mapTo('a') 其实是 map(()=&gt;'a') 的语法糖,也就是说无论输入流中给出的值是什么,我往输出流中放入的都是这个固定的值。

坑与最佳实践

取消订阅

subscribe 之后,你的回调函数就被别人引用了,因此如果不撤销对这个回调函数的引用,那么与它相关的内存就永远不会释放,同时,它仍然会在流中有数据过来时被调用,可能会导致奇怪的 console.log 等意外行为。

因此,必须找到某个时机撤销对这个回调函数的引用。但其实不一定需要那么麻烦。解除对回调函数的引用有两种时机,一种是这个流完成(complete,包括正常结束和异常结束)了,一种是订阅方主动取消。当流完成时,会自动解除全部订阅回调,而所有的有限流都是会自动完成的。只有无尽流才需要特别处理,也就是订阅方要主动取消订阅。

当调用 Observablesubscribe 方法时,会返回一个 Subscription 类型的引用,它实际上是一个订阅凭证。把它保存下来,等恰当的时机调用它的 unsubscribe 方法就可以取消订阅了。比如在 Angular 中,如果你订阅了无尽流,那么就需要把订阅凭证保存在私有变量里,并且在 ngOnDestroy 回调中调用它的 unsubscribe 方法。

类型检查

只要有可能,请尽量使用 TypeScript 来书写 RxJS 程序。由于大量 operator 都会改变流中的数据类型,因此如果靠人力来追踪数据类型的变化既繁琐又容易出错。TypeScript 的类型检查可以给你提供很大的帮助,既省心又安全,而且这两个都是微软家的,搭配使用,风味更佳。

代码风格

如同所有 FP 程序一样,ReactiveX 的代码也应该由一系列小的、单一职责的、无副作用的函数组成。虽然 JavaScript 无法像 Java 中那样对 Lambda 表达式的副作用做出编译期限制,但是仍然要遵循同样的原则,坚持无副作用和数据不变性。

寄语 – 实践出真知

ReactiveX 大家族看似庞大,实则简单 —— 如果你已经有了 Java 8+ / Kotlin / underscore 或 lodash 等函数式基础知识时,新东西就很少了。而当你用过 Rx 大家族中的任何一个成员时,RxJS 对你几乎是免费的,反之也一样。

唯一的问题,就是找机会实践,并体会 FRP 风格的独特之处,获得那些超乎具体技术之上的真知灼见。


更多精彩洞见,请关注微信公众号:ThoughtWorks洞见

Share

什么是框架?

先摘录并翻译一段 wiki:

In computer programming, a software framework is an abstraction in which software providing generic functionality can be selectively changed by additional user-written code, thus providing application-specific software. A software framework provides a standard way to build and deploy applications. A software framework is a universal, reusable software environment that provides particular functionality as part of a larger software platform to facilitate development of software applications, products and solutions. Software frameworks may include support programs, compilers, code libraries, tool sets, and application programming interfaces (APIs) that bring together all the different components to enable development of a project or system.

在编程领域,软件框架是指一种抽象形式,它提供了一个具有通用功能的软件,这些功能可以由使用者编写代码来有选择的进行更改,从而提供服务于特定应用的软件。软件框架提供了一种标准的方式来构建并部署应用。

软件框架是一种通用的、可复用的软件环境,它提供特定的功能,作为一个更大的软件平台的一部分,用以促进软件应用、产品和解决方案的开发工作。软件框架可能会包含支撑程序、编译器、代码、库、工具集以及 API,它把所有这些部件汇集在一起,以支持项目或系统的开发。

Frameworks have key distinguishing features that separate them from normal libraries:

框架和普通的库在特性上具有一些关键性的区别:

  1. inversion of control_: In a framework, unlike in libraries or in standard user applications, the overall program’s flow of control is not dictated by the caller, but by the framework.[1]控制反转:与库或普通的应用不同,在框架中,应用的宏观控制流程不是由调用者决定的,而是由框架本身。
  2. extensibility_: A user can extend the framework – usually by selective overriding; or programmers can add specialized user code to provide specific functionality.可扩展性:用户可以扩展该框架 —— 通常是有选择的进行改写(Override)或者由程序员添加专门的用户代码来提供特定的功能。
  3. non-modifiable framework code_: The framework code, in general, is not supposed to be modified, while accepting user-implemented extensions. In other words, users can extend the framework, but should not modify its code.不可修改框架代码:通常,框架代码都不打算让你修改,而是接受由用户自己实现的某些扩展。换句话说,用户可以扩展该框架,但是不应该修改它的代码。

形象点比喻(但不够严谨),框架就是条生产线,这条生产线上有很多工人(代码)在工作。生产线的管理者(程序员)负责管理这条生产线,比如说有的工序是空的,那么你就可以安排自己的工人进去,让他去达成你的目标。有些工序上的工人干的工作和你预期的不同,你也可以安排自己的工人把他替换掉。

但是无论如何,你的工人除了执行你的意志之外,还要遵守那个工序的强制性要求,他想磨洋工或粗制滥造是不行的,因为这个流水线上的下一个工序可能有超时检查或质量检查,出了错直接就把这个流水线给你停掉,甚至对于一些强制性检查,你作为管理者都无权忽略它。

可以想见,一条好的生产线的价值有多大。生产线绝不仅仅是一组机器而已,它是很多年的管理经验的结晶,这些才是最值钱的,否则光靠那些机器能值几个钱?有了生产线,对工人(代码)的要求大大降低了,甚至对管理者(程序员)的要求也大大降低了。当然,如果你只想生产个“能穿”的鞋子,那么这条生产线几乎没有附加价值,甚至会提高你的成本。但是如果你想生产一个“高质量”的鞋子,那么这条生产线是别人的鞋子卖10块而你的鞋子能卖到1000块的根本保障。

总体来说,建立生产线的目标就是制定规矩,保障品质,让高品质可以用较低的代价进行复制。框架也是如此。

从代码结构上看,框架在宏观层面使用的都是注册、回调机制。这种机制有一个形象的名称,叫做好莱坞法则,为什么叫好莱坞法则呢?因为好莱坞想要成名的演员太多了,都想去找导演,这样下去导演的工作效率势必会受到严重的影响。于是导演就立下了“规矩”,不要打给我们(Don’t call us),等我们打给你(We’ll call you!)。由于这个 call 和程序调用的 call 是同一个词,于是编程界就把这句话搬过来,变成了回调(callback)的形象代言人。

比如在 Java 的 Spring 框架中,你只要给一个类加上 @Service 注解,它就会自动被 Spring 作为服务管理起来,当 Spring 认为需要的时候,就会创建这个类并且把它的实例传给需求方。在 Angular 中也是一样,你只要给一个类加上 @Component 注解(装饰器),它就会自动被 Angular 当做组件管理起来,当 Angular 认为需要的时候,就会创建这个类,并且把它的实例传给需求方(比如路由器)。

这些注解中还可以带一些额外信息,被称为元数据。所谓“元数据”就是 metadata,指的是关于数据的数据,这不是 Angular 自创的名词,其它编程领域已经使用了几十年了。当 Angular 准备创建一个组件的时候,它就会找到这些元数据,从中找出这个组件的模板(因为组件本身是纯类,没有携带任何模板信息),然后据此对 DOM 进行操纵。

而你写的这些组件类和模板,其实就是“由程序员添加专门的用户代码来提供特定的功能”,因为框架是不会关心你的组件的外观和逻辑的,它唯一关心的就是你必须遵循它的规范来工作,否则它就会给你报错(比如,“连模板都没有还敢说自己是 Angular 组件?”)。

而库则跟框架相反,宏观上说,它是等着你调用的,你要什么功能它就给你什么函数,然后你调用这个函数,把所需的参数传进去就行了,而不是让你遵守它的那么多规矩。所以你很自由,但是你也要自己为整件事负责。你要自己创建组件、创建服务等,自己来驱动整个流程,自己做必要的检查,当然也可以不做,反正对十块钱的鞋子别人可能只希望能穿一个月就行了。

那么,问题来了,框架一定会比库高级吗?显然不是,甚至连框架的功能都不一定比库多。它们只是定位不同、设计理念不同而已。对于 Angular 来说,它会更希望你遵守一些规矩,这样当系统需要长期维护、甚至要经历很多人员更迭的时候,才不至于腐化。它希望每个开发人员都不必了解应用的全貌就能很好地完成工作(因为有当前工序的操作手册和检查点)。当然,它也不会干涉那些它不需要关心的事情,比如组件模板中你放 h1 还是放 div 它是不在乎的。这些目标用库也能达到,不过对人员的架构观和做事的自律性会有相应的要求,毕竟没人管了,那你自己就不能放任自流。

然而,在现实中,很多应用的整个生命周期可能都不会超过一年,甚至还有很多生命周期几天的活动页,那么,这些应用和页面的可维护性其实并不重要,甚至连是否能让不同的人协作都无所谓。那么点功能,能出什么 bug?一次性的需求,管什么可扩展性?这时候,学习成本就会成为一个很重要的参考因素。

学习 Angular 最难的就是遵守并理解规矩。然而大部分人天生是不愿遵守规矩的,特别是有些规矩可能他都无法理解为什么(虽然这可能是前人根据血的教训总结出来的)。可是,一旦遵守并理解了这些规矩,那么一扇新的大门就对你敞开了,你会发现跨上这个台阶之后,无论前端技术还是后端技术还是移动端技术,都遵循着很多相同的理念,即使出现了新的挑战,你也可以套用这些理念去解决。

规矩即自由。孔子把“从心所欲,不逾矩”当做自己的最佳状态,其实很多事都是这样。一旦深刻理解了设计和使用框架的思维模式,你将迎来一个实质性的提升。

在我工作的前五年,编程时很“聪明”,用技巧解决了很多问题,但之后的十五年(恰好在那一年我知道了框架的概念),我爱上了规矩,不但自己给自己(根据血泪教训)立各种规矩,而且从别人那里借鉴了很多规矩,无论是宏观的还是微观的。可以说,规矩就是固化的好习惯,虽然有时候也会成为阻碍,但是如果你想在编程领域工作到退休,那么这些规矩就是你表面上最大的资产,而对这些规矩的来龙去脉的理解和领悟,则是你深层次中最大的资产。


更多精彩洞见,请关注微信公众号:思特沃克

Share

给Java程序员的Angular快速指南

太长不读版:

Spring + Angular 的全栈式开发,生产力高、入门难度低(此处省略一万字),是 Java 程序员扩展技术栈的上佳选择。

如果你动心了,接下来就是那省略的一万字……

痛点 – 团队分工与协作

在前后端分离的开发方式中,拆故事卡是个难题。

如果前后端同时工作于一张卡上,但配合不够默契或节奏不同步,就会出现一方空转的现象。如果前后端各一张卡,又不容易实现端到端验收,可能导致先做完的一方在另一个结束后还要再次返工的现象。而且,两个人都要深入理解这张卡所描述的业务细节,而这往往是不必要的。

更重要的是,BUG 最容易出现在边界处。

业务卡不像技术卡那样能跟其它卡片划出明确的边界,前后端之间必然具有千丝万缕的联系。这种联系越紧密,出 BUG 的机会也就越大。

技术架构上的挑战,也会反映到人员架构上。我们人类不是星灵,无法做到心灵相通。因此前后端开发者需要对合作方所拥有的知识进行很多主观假设。

如果这些假设中存在错误,又没能及时沟通来消除它(甚至可能都意识不到这些假设的存在),那么 BUGs 就要登场了。而像业务卡这种级别的密切协作中可能隐含的假设实在太多了,除非经过长时间的磨合,否则很难消除,但大多数项目上可没有那么多磨合时间。

解决方案 —— 全栈式开发

人员架构

该如何解决呢?克服上述问题的办法就是全栈式开发。也就是说,调整人员架构去适应技术架构。

简单来说:每个人都同时写前端和后端。他不必是前端专家也不必是后端专家,但是两边都要会写。他的关注点不是技术知识,而是业务知识。他的工作目标是贯穿前后端的价值流,对单个故事进行端到端交付。

但是,要如何克服实现中遇到的技术难题以及保障代码质量呢?那就要靠团队中的技术专家了。

总体来说,全栈式团队的人员架构就是大量全栈业务工程师 + 少量技术专家。当然,技术专家不一定要安排单独的人担任,只要技术满足要求,也可以由某位全栈工程师兼任,只是他做计划时要留出做技术支持的时间。

通过 Code Review、Pair 等敏捷实践,技术专家可以起到团队放大器的作用,让整个团队的生产力翻倍。

个人工作流

作为全栈工程师,你首先要对一个业务故事进行建模,包括业务模型、视图模型、领域模型、存储模型等,建模的过程也就是你理解业务的过程。这时候要注意多和 BA、UX、DBA 等沟通,以确保你的理解不存在方向性错误,不要太沉迷细节,防止见木不见林。

单源建模的优点是这些模型之间很容易保持一致,这无论是对前期开发还是对后期维护都是有帮助的。

建模完毕之后,就要开始设计前后端之间的接口了。接口是前后端分离式架构中最容易开裂的地方,也是对未来的演化影响最大的地方之一。它很重要,但也不必小心翼翼的 —— 全栈工程师对接口变化的适应能力要强大得多。因为接口的提供方和消费方都是你,信息非常透明,不存在任何额外的假设。对不完美的接口,你可以在后续开发过程中迭代好几个版本来把它打磨到最理想的形态,改接口将不再沉重和危险。

接口设计完之后,有两种路径,取决于界面和后台逻辑的特点。

如果对业务理解还不是很有信心,那就先用 Mock 的方式把前端写出来,然后把这个 Mock 版当做可执行的原型去跟 BA、QA,甚至客户进行实际操作演示,用可操作原型来验证你对业务的理解。对一般粒度的故事卡,线框图级的可操作原型通常能在半天内完成。通过原型尽早发现错误,可以避免以后沉重的返工。而且,这是一个可演化原型,不是一次性原型,不会浪费掉。

如果后端很容易实现(但先不必做优化工作),那么就可以不必 Mock,先初步完成后端开发,并让前端直接对接真实的后端。先拿这个比 Mock 版原型更逼真一点的原型串起流程,然后再进行优化和打磨工作。

在整个过程中,你可以根据不同的需要,来与不同的技术专家进行 Pair,并且你最终的代码也会在例行 Code Review 中得到前端专家、后端专家、DBA、DevOps 专家等人的点评和改进,不必担心自己在单项技术上的短板影响交付。

全栈的挑战

全栈固然美好,但也要迎接很多挑战,而 Angular 会帮你分担这些痛苦。

首先遇到的挑战是语言切换

前后端 JavaScript 全栈固然在特定场景下有效,但是在很多企业应用中是远远不够的。至少到目前为止,企业应用还主要是 Java 的天下。本文所讨论的也都是 Java + JavaScript 的全栈。

我们都知道,Java 和 JavaScript 之间的差异就像雷锋和雷峰塔之间的差异。Java 程序员通常很难适应 JavaScript,不过现在有了更像 Java 的 TypeScript。而 Angular 就是原生基于 TypeScript 的框架,稍后我会做一个摘要讲解,你会发现自己很熟悉它的味道。

(图片来自:http://t.cn/RobG5nA

其次是基础设施

基于 JRE 的构建体系和基于 NodeJS 的构建体系看似差异很大,实际上却有很大程度的相似性。但前端两年一换代的疯狂迭代,以及层出不穷的新名词、新工具,仍然难免会让后端心生恐惧。不过不用担心,Angular 替你封装了一切,你只需要装上 NodeJS 环境和 Angular CLI 就可以了。你不需要关心它封装了哪些第三方工具,至于今后的工具链怎么疯狂迭代,那都是 Angular 开发组需要操心的事。

最后是最佳实践

前后端从表面上看差异很大 —— 前端轻灵,后端稳重。

但在我看来它们很少存在本质性的差异,更像是不同的社区文化导致的结果。而在更高的层次上看,两边的技术具有很大的相似性。无论是函数式编程还是工程化开发,都不是某一方所特有的,而是 IT 领域的共同资产。况且,它们还一直在相互影响,相互渗透 —— 这两年后端变得越来越轻灵,而前端变得越来越工程化。长远来看,文化合流是必然的趋势。

事实上,前后端很多优秀设计和最佳实践都是殊途同归的。像 Spring 和 Angular,它们都采用了久经考验的面向对象范式;都使用依赖注入技术进行解耦;都拥抱函数式编程;都提供了丰富的 AOP 支持等。虽然细节上各有千秋,但仅从代码上就能感受到它们之间的相似性。

我该怎么办?

听完这些,你是否已经蠢蠢欲动?接下来,就跟我开始 Angular 之旅吧。

语言 – TypeScript

Angular 使用 TypeScript 作为主要开发语言。如果你还不熟悉 TypeScript,那可以把它看做 Java 和 JavaScript 的混合体。TypeScript 是 ES6 的超集,这就意味着,任何有效的 ES6 语法都同样是有效的 TypeScript 语法。

事实上,从 Java 出发学 TypeScript,可能比从 ES5/6 学 TypeScript 还要简单一些。不过,对于 Javaer 来说,学习 TypeScript 时有一些重要的不同点要特别注意。

TypeScript 的类型只存在于编译期

TypeScript 的一个首要设计约束就是要兼容 ES5/6,因此不能随意增加基础设施,而像 Java 这种级别的类型支持在原生 JavaScript 中是根本不存在的。

你可以把 TypeScript 的类型看做仅仅给编译器和 IDE 用的。因此,在运行期间没有任何额外的类型信息(只有 ES5 固有的那一小部分),像 Java 那样完善的反射机制是很难实现的(可以用装饰器/注解实现,但比较繁琐)。

TypeScript 的装饰器 vs. Java 的注解

TypeScript 的装饰器和 Java 的注解在语法上很相似,但其实在语法含义上有着本质的区别。TypeScript 的装饰器是个函数,而 Java 的注解是个数据。语法上,装饰器名字后面必须带括号,不能像注解那样省略。

不过,在 Angular 中,TypeScript 装饰器的实际用途就是为类或属性添加注解而已。因此,有些文章中,包括早期的官方文档中,用的都是注解的说法。当然,以后写新文章还是都用装饰器吧。

类与接口

TypeScript 中的类和 ES6 中的类几乎是一样的,和 Java 中的类也很相似。

接口则不同,我们前面说过,TypeScript 中的类型信息只存在于编译期,而接口作为“纯粹的”类型信息,也同样只存在于编译期。也就是说,在运行期间你无法判断某个对象的类是否实现了某个接口。在 Angular 中,实际上使用的是暴力探测法来判断的:查找这个接口中规定的方法(只匹配名称),如果存在,则认为实现了这个接口。

这也意味着,你就算不显式 implements 接口,但只要声明了其中的方法,Angular 也会正确的识别它。但这不是一个好习惯,你应该始终显式 implements 接口,删除时也要同时删除接口声明和对应的方法。不过也不用担心,Angular 自带的 lint 工具会帮你检查是否有忘了显式 implements 接口,多注意提示就可以了。

接口是给编译器和 IDE 看的,这很有用。比如,我们可以在 IntelliJ/WebStorm 中声明某个类实现了一个接口,然后在这个类名上按 alt-enter ,就会出现 “Implement interface XXX” 菜单 —— 就像 Java 中一样。事实上,一些 IDE 对 TypeScript 的支持程度已经接近 Java 了:代码提示、重构、类型检查、简短写法提醒等,应有尽有。

值得注意的是:你也可以 implement 一个类,而不仅是 extends 它,也就是说类可以在很多场景下代替接口!Angular 风格指南提出,“考虑在服务和可声明对象(组件、指令和管道)中用类代替接口”。因为运行期间接口不存在,所以在 Angular 中不能把接口用作依赖注入的 Token,也就不能像 Java 中那样要求注入一个接口,并期待框架帮你找出实现了这个接口的可注入对象,但类存在,因此,上述场景下要尽量用抽象类来代替接口。

鸭子类型

为了支持 JavaScript 的动态性和遗留代码,TypeScript 的类型匹配要比 Java 宽松不少。比如,如果两个类(或接口)的属性和方法(名称、类型)都完全一致,那么即使它们没有继承关系,也可以相互替代(但如果类有私有属性,则不能,就算两者完全一样也不行)。表面上看这可能过于宽松了,但在实际开发中还是很有用的,使用中要注意突破 Java 固有思维的限制。

在 TypeScript 中还支持可选属性(name?: Type),也就是说如果两个类的差别仅仅在可选属性上,那么它们也是可以相互替代的。

字面量与匿名类型

TypeScript 在某些方面可能更符合你对 Java “应该是什么样子”的期待,至少在我看来是这样。要声明一个匿名对象、匿名数组型变量?直接写出来就好了const user = {name: 'tom', age: 20}。除此之外,它还能声明匿名类型 let user: {name: string, age: number} = ...

当然,也不能滥用它们。对于一次性使用或暂时一次性使用的变量或类型,用字面量和匿名类型很方便,可读性也好,但是如果它要使用两次以上,那就该重构成正式的类型了。

any

TypeScript 中的 any 大致相当于 Java 中的 Object,如果你看到通篇 Object 的 Java 代码你会不会想骂街?any 也一样。不必完全禁止 any,但如果你要使用 any,请务必先想清楚自己要做什么。

void

如果你在 Java 中经常使用 void,那就遵循同样的原则用在 TypeScript 中。在 TypeScript 中,当你不声明函数的返回类型时,它会返回自动推断的类型(没有明确的 return value 语句时会推断为 undefined 类型),如果你不想返回任何值,那么请把返回类型指定为 void 来防止别人误用。

this

JavaScript 中的 this 是个奇葩。虽然这是函数式语言中的标配,但从语言设计上真是让人忍不住吐槽。要是能像 Groovy 那样分出 this / owner / delegate 就好了。

吐槽归吐槽,对于 Java 程序员,该怎么避免自己踩坑呢?很简单:对普通函数,任何涉及到 this 的地方都用箭头函数 ()=&gt;,而不要用普通的 function foo(),因为前者是替你绑定好了符合直觉的 this 的;对方法,不要把任何涉及到 this 的方法当作函数指针传给别人,但可以在模板中自由使用。在 Angular 中,这两条原则可以帮你回避掉绝大部分 this 错误。更多的细节可以先不管,随着使用经验的增加,你会逐渐弄明白这些规则的。

其它

以上这些是开发中常遇到的注意事项,其它的特性我就不一一列举了,请自行参考 TypeScript 的官方文档。

范式与模型

MVVM

Angular 的基本编程模型是 MVVM,你可以把它看做 MVC 的一个变种。事实上,这是一个很符合直觉的模型:你看到一个页面,先在大脑中抽取出它的信息架构(属性)和操作(方法),定义好它们之间的逻辑关系和处理流程,这就是视图模型(VM)。你把它们落实到代码,变成内存对象,然后 Angular 就会帮你把它和页面(View)关联起来。你不懂怎么操作 DOM?没关系,你只要会操作内存对象就可以了,这应该是你非常擅长的吧?剩下的那些脏活儿 Angular 都会帮你搞定。

不过,Angular 关心的只是“要有” VM,至于你如何生成这个 VM,它并不会做任何假设和限制。

自由混搭与切换

你想怎么生成 VM?

  • 像后端控制器那样直接写在组件中?没问题!
  • 像后端那样委托给服务?没问题!
  • 像 Redux 那样委托给单一 Store?没问题!
  • 像 Java 8 Stream 那样用流水线生成?没问题!
  • 自己几乎不处理,完全委托给后端 API?没问题!

这么多方式各有不同的适用场景,但也不必过早担心如何选型。只要你的组件设计合理(职责分明、接口明确等),那么在这些方式之间切换,或者混用它们,都不会很难。

作为起点,可以先直接写在组件中,然后按需重构成服务,服务中可以直接写代码,也可以实现 Redux 风格的单一 Store,或者用 RxJS 写流水线。

RxJS

在 Angular 开发人员的成长过程中,有一个很重要的坎就是 RxJS,它的背后是 FRP(函数响应式编程)范式。不过对于 Javaer 来说,它的门槛并不高。如果你会用 RxJava / RxGroovy 等 ReactiveX 族的任何一个库,那么你几乎可以不用专门再学,它们都是同一个大家族,编程范式甚至部分操作符的名称都一样,稍微对比一下差异就可以了。如果不会,请继续往下读(以下的讨论也适用于 RxJava 等,不过我文中只用 RxJS 举例)。

RxJS 是一种 FRP(函数响应式编程)库,它同时具有函数式编程和响应式编程的优点。

如果你会用 Java 8 Stream,那么也有很多知识可以复用到这里。相对于 Java 8 Stream,RxJS 的限制稍微宽松一些,但我建议你仍然按照 Java 那种严格的方式使用它(比如不要对流外的变量赋值)。

所谓响应式编程,我们可以把它想象成一条流水线,流水线上不断传送待加工的材料(原料、半成品、成品等),流水线上每个工序的工人负责对传送到眼前的材料进行一定的处理(做出响应),然后放回流水线,接着它就会被传送到下一个工序。

设计上,每个工序的职责都应该是明确而单一的,这样才能达到最高的效率和流水线的可定制性。

把这些概念映射到 RxJS,流水线就是 Observable(可观察对象),工序就是 operator(操作符),材料就是传给每个 operator 的参数。

是不是感到很熟悉?没错,它跟 MessageQueue 是一样的模型,只是应用在不同的层次而已。在编程领域,这种现象随处可见,善于发现并掌握这种现象,是你作为资深程序员能实现快速跨领域学习的根本保障。

相对于 Java 8 Stream,RxJS 比较特别的一点是它完全屏蔽了同步和异步之间的差异。也就是说,其中的 operator 不知道也不需要关心这个数据是同步传过来的还是异步传过来的。只要你遵循一些显而易见的原则,你就可以一直用同步方式给数据,之后即使要突然改成异步,原有的代码也不会被破坏。

事实上,我在 Angular 开发中经常利用这种特性来加速开发。比如假设我最终需要从后端 API 获取某些信息,在这个 API 开发好之前,我可以先在前端模拟出响应结果,进行后续开发。这时候,如果我用 Observable 的方式声明数据源,那么虽然我目前用同步的方式提供数据,但是将来我可以直接切换成 HTTP 数据源,而不用担心破坏现有代码。

细部原理

宏观上的要点已经讲完了,接下来我们快速过一遍微观的。我只讲要点,要想深入细节请参阅文中给出的参考资料。

Angular 模块

Angular 模块不同于 JavaScript 模块,它是一个架构级的基础设施,用来对应用进行宏观拆分,硬化边界,防止意外耦合。

模块的划分主要基于业务领域的边界,而在开发组织形式上,也要和模块划分方式相互对齐,尽量让每个模块都有明确的负责人。

参见 https://angular.cn/guide/ngmodules

路由

传统的路由功能完全是由后端提供的,但是在单页面应用中,在页面中点击 URL 时,将会首先被前端程序拦截,如果前端程序能处理这个 URL,那就会直接在前端处理,而不会向后端发送这个请求。

前端可以根据这个 URL 修改视图,给用户与后端路由一样的结果,但省去了网络交互的过程,因此会显得非常快捷。

路由是业务功能的天然边界,善用路由对于改善代码结构和可维护性是很有帮助的。

在 Angular 中,路由还同时提供了惰性加载等特性,因此,早期对路由进行合理规划非常重要。不过也不用过于担心,Angular 中重新划分路由的代价并不高。

参见 https://angular.cn/guide/router#appendix-emlocationstrategyem-and-browser-url-styles

模板与视图

你可以把模板看做 JSP,主要区别是 JSP 是后端渲染的,每次生成都需要一次网络交互,而模板是前端渲染的,在浏览器中执行模板编译成的 JS 来改变外观和响应事件。

模板语法

虽然看起来奇怪,但 [prop](click)*ngFor 等模板语法中的特殊符号都是完全合法的 HTML 属性名,实际上,属性名中只禁用各类空白字符、单双引号等少数几个显而易见的无效字符(正则:[^\t\n\f \/&gt;"'=])。

参见 https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#syntax-attribute-name

属性与……属性

由于历史原因,英文的 Attribute 和 Property 都被译为属性,然而两者是截然不同的。Angular 中的常规绑定语法针对的都是 Property,只有 [attr.xxx] 绑定针对的是 Attribute。

参见 https://angular.cn/guide/template-syntax#html-attribute-vs-dom-property

组件与指令

你可以把组件看做后端模板中的 taglib,区别是它们运行在浏览器中而不是服务端。组件与指令在用途上的区别是,组件充当搭建界面的砖块,它的地位和 HTML 元素并无区别;而指令用于为 HTML 元素(包括组件)添加能力或改变行为。

所以,组件中不应该操纵 DOM,只应该关注视图模型,而指令负责在模型和 DOM 之间建立联系。指令应该是单一职责的,如果需要完成多个职责,请拆成多个指令附加到同一个元素上。

服务与依赖注入

Angular 的服务与依赖注入和 Spring 中的很像,主要的区别是 Angular 是个树状的多级注入体系,注入器树是和组件树一一对应的,当组件要查找特定的服务时,会从该组件逐级向上查找,直到根部。

这实际上是职责链模式。当前组件找不到某个服务时,就会委托给其父节点来查找。和策略模式结合使用,组件就可以通过自己提供一个服务来替换父组件提供的服务,实现一种支持默认处理的逻辑。

参见 https://angular.cn/guide/hierarchical-dependency-injection

表单与验证

在前端程序中,验证主要是为了用户友好性,而不是安全。安全是后端的工作,不能因为前端做了验证而放松。

Angular 对表单提供了非常强力的支持。如果你的应用中存在大量表单、大型表单、可复用表单或交互比较复杂的表单,那么 Angular 的表单功能可以为你提供强大的助力。

Angular 的表单提供了不同层级的抽象,让你可以在开发中轻松分离开显示、校验、报错等不同的关注点。也让你可以先用文本框快速搭出一个表单,将来再逐个把这些文本框替换成自定义编辑框,而不会破坏客户代码。

参见 https://angular.cn/guide/user-input

测试

Angular 对测试的支持非常全面,可以实现各个不同层次的测试。

但是不要因为拿到把这么好用的锤子就满世界敲。要根据不同的价值需求去决定测什么不测什么。

别忘了每个 Angular 的类,无论服务、组件、指令还是管道等,都是 POJO,你可以用测 POJO 的方式测试它们,得到毫秒级反馈,而且这往往会更高效。

参见 https://angular.cn/guide/testing。但要记住:虽然 Angular 支持这么多种方式,但你不一定要用到这么多种方式。

安全

在 Angular 中,你不会无意间造成安全隐患。只要注意一点就够了:DomSanitizer.bypassSecurityTrust* 要慎用,务必确保传给它的东西不可能被攻击者定制,必要时请找安全专家推演。参见 https://angular.cn/guide/security#sanitization-and-security-contexts。

如果你在发起 POST 等请求时收到了 403 错误,那可能是因为后端开启了 CSRF 防护。Angular 内置了一个约定 —— 如果服务端 csrf token 的cookie名是 XSRF-TOKEN,并且能识别一个名叫 X-XSRF-TOKEN 的请求头,那么它就会自动帮你完成 CSRF 验证。当然,你也可以自定义这些名称来适配后端,参见 https://angular.cn/guide/http#configuring-custom-cookieheader-names

跨域与反向代理

本地开发时,前端有自己的服务器,显然无法与后端 API 服务器运行在同一个端口上,这样就导致了跨域问题。要解决跨域问题,主要有 CORS 和反向代理这两种方式。CORS 是标准化的,但是受浏览器兼容性的影响较大;而反向代理则通过把 API “拉”到前端的同一个域下,从根本上消除了跨域访问。

开发时,Angular CLI 内置了对反向代理的支持;部署时,各个主流 Web 服务器都能很好地支持反向代理。

一般项目中建议还是优先使用反向代理的方式。

(图片来自:http://t.cn/RgsWKEJ

杂谈

你不必写 CSS

很多后端初学前端时会被卡在 CSS 上,在心里喊一句 WTF。但实际上,在团队开发中,你可能根本不必写 CSS。

现在已经有了很多现成的 CSS 库,比如已经熟透的 Bootstrap,还有后起之秀 Material Design、Ant Design 等等。你只要能根据其表达的视觉含义,正确套用它们定义的 CSS 类就够了。尽量不要自己手写 CSS,否则可能反倒会给将来的页面美化工作带来困扰。

选好了基础框架,并且和 UX 对齐之后,团队中只需要一个 CSS 高手就能实现所有的全局性设计规则。对于全栈工程师来说,充其量只有对当前页面中的少量元素进行定制时才需要写 CSS,况且还可以通过找人 pair 来解决偶尔碰到的难题。

全栈,让设计更简单

前后端技术各有所长,有些事情用前端实现更简单,有些用后端实现更简单。综合考量前端技术和后端技术,往往可以产生更简单、更优秀的设计。广度在业务开发中往往比深度有用,这也是全栈工程师的优势所在。而团队中的技术专家主要负责深度。

分工是动态的

技术专家或全栈工程师,并不是什么荣誉头衔,只是分工不同而已。

同一个项目上你可以同时担任全栈工程师和技术专家;这个项目你是全栈工程师,下一个项目上也可能专门担任技术专家。团队的协作方式永远是动态的、随需应变的。

不用担心全栈会限制你的技术深度,实际上,全栈对提高你的技术深度是有帮助的,因为很多技术的“根”都是互通的。

相信你的直觉

资深后端首先是一个资深程序员,你对于“应该如何”的期待,很可能是对的。如果你觉得 Angular 应该有某项功能或某种设计,它很可能确实有。去 Stackoverflow 搜一下,找找你的答案,这是你成为高级 Angular 程序员的捷径。

万法归一

形容某人聪明时经常说“万法皆通”,实际上“万法皆通”不如“一法通而万法通”。很多技术之间的相似程度超出你的想象,这些相似的部分其实就是技术的核心。用万法归一的思路去学习总结,会给你带来真正的提高。

资料 & 学习指南

学习 Angular 的最佳资料是它的官方文档,它无论是从准确、全面,还是及时性等方面都是最佳的。

它的英文文档站是 https://angular.io,中文文档站是 https://angular.cn,这是由我和另外两位社区志愿者共同翻译的,期间还得到了很多社区志愿者的支持。中文文档和英文文档至少在每个大版本都会进行一次同步翻译。虽然时间有限导致语言上还有粗糙之处,不过你可以相信它的技术准确度是没问题的。

阅读时,请先阅读架构概览 https://angular.cn/guide/architecture,然后阅读教程 https://angular.cn/tutorial(有经验的程序员不需要跟着敲代码,如果时间紧也可跳过),最后阅读风格指南 https://angular.cn/guide/styleguide。风格指南很重要,不用记住,但务必通读一遍,有点印象供将来查阅即可。

文档站中还提供了 API 参考手册,它提供了简单快速的站内搜索功能,需要了解哪些细节时到里面查就可以了。

另外,ng-zorro 组件库的一位开发者还整理了一份不完全指南,包括中英文资料:https://zhuanlan.zhihu.com/p/36385830


更多精彩洞见,请关注微信公众号:思特沃克

Share

它比微信小程序早出现半年,却不曾引爆技术圈

作为一个程序员,这两天的朋友圈被微信小应用刷屏了吧?想试了吧?没拿到邀请吧?没关系,我来帮你!

当然,我没法帮你拿到邀请码 —— 因为我也没有。不过,别失望,我今天要跟大家谈一项比微信小程序更宏观的概念 —— Google提出的PWA。

PWA是什么?

PWA全称是“Progressive Web Apps”,译成中文就是渐进式应用。这是Google在2015年6月15日提出的概念,参见Alex Russell写的《Infrequently Noted》

那么,什么是渐进式应用呢?

Alex写道:

在昨天的晚餐上,我和Frances列举出了新型应用的几个特征:

  • 响应式:适应任何形态因素
  • 不依赖网络:在Service Worker(H5中的新技术,appcache的升级版)的支持下能够离线使用
  • 像本地应用一样交互:采用“壳+内容”模式来实现类似本地应用的浏览和交互体验
  • 保持最新:始终透明的升级到最新版,这要感谢Service Worker提供的升级机制
  • 安全:通过TLS(这是Service Worker所要求的)来防止通讯被窥探
  • 可发现的:具有像“应用”一样的唯一标识,这要感谢W3C的Manifest文件格式,它在“Service Worker”中注册的scope能允许搜索引擎发现这些应用。
  • 可“再接触”的:可以访问操作系统的“可再接触”界面,比如推送通知
  • 可安装的:通过浏览器提供的提示,可以把它加入主屏,允许用户把觉得有用的应用“保留”下来,而不用到应用商店去安装,那样太麻烦了。
  • 可链接的:这意味着它们是零阻力、零安装,并且易于共享的。URL更能发挥社交传播的能量。

PWA怎么用?

一个PWA的使用过程大概是这样的:

这货开始就是一个普通网页:

01_pwa_start

然后,这是什么鬼?

02_pwa_prompt

这网站看着还不错,那就接受吧!

03_pwa_accept

它出现在手机的首屏了,以后随叫随到!

04_pwa_homescreen

点了就能像一个本地应用那样启动它:

05_pwa_launched

看,启动完也跟一个本地应用没有区别:

06_pwa_launcher

PWA的好处

Web应用(网页)和本地应用(App)各有优势,比如:

  • Web应用不需要安装,因此减少了用户流失(想想你要安装一个应用时有多么犹豫吧)
  • Web应用方便传播,你只要在微信或者QQ里发一个网址就有很多人会去看(比如本文 ^_^)
  • Web应用的收藏、前进、后退这些功能非常有用
  • 本地应用的留存度比较高。你看过一个网址后再回来的概率大约是本地应用的1/3。
  • 本地应用可以推送通知,而Web应用只有在浏览器打开状态下才行
  • 本地应用可以离线使用,只要做适当的设计就可以不受网络环境的影响
  • 本地应用可以访问更加丰富的功能,比如更大的本地存储空间

那么,它们能否合二为一呢?这就是PWA所做的事!

通过前面提到的这些步骤,PWA可以让用户同时获得这两种好处。这无论对于用户本身还是对于应用的开发者都非常有意义。

如何实现PWA?

其实从Alex的描述中就可以看到,在技术上我们离实现PWA并不远:

Web应用开发技术

已经具备。

特别是得益于Angular等SPA应用的普及,SPA不但响应更加快速,更加接近原生应用的操作体验,而且让我们可以完全不依赖服务器工作。

离线存储技术

可能有人还记得一项叫AppCache的技术,它已经废弃了,现在被Service Worker所取代。这是一个更强大的AppCache,它不仅能存储HTML/JS等静态文件,而且还让你能够在客户设备上运行起一个仿真的超轻量级Web服务器,你在里面已经几乎可以写Node程序了!

这是新的H5标准的一部分,而不是PWA特有的。

推送通知

H5标准中已经有了用来推送通知的API(用过Web Gmail的话应该见过),现在只要把它和操作系统的API对接就行了。当然,这最好由操作系统提供支持,这已经实现了,那就是新推出的Android 7。

App壳程序

这个壳程序其实自己来实现也没问题,技术上没啥难度,不过不用重复造轮子了,因为官方已经出了一个,参见https://github.com/GoogleChrome/application-shell。如果用Angular 2就更幸福了,因为Angular 2还制作了一个Angular 2专用的壳:https://github.com/angular/mobile-toolkit。不过,它现在还处于未发布状态,早鸟可以开始提前尝试了。

微信小程序与PWA

虽然”微信小程序”概念的提出比PWA晚了半年,不过相信张小龙这个级别的产品经理不会是简单的抄袭PWA概念,而是会发挥微信生态圈的优势,提供更广阔的应用场景。或许我们应该把微信小程序看做PWA的一种优质实现。

假如你没有拿到微信小程序的邀请码,不妨像我一样,先从研究PWA开始吧。

Share

弯道超车!后端程序员的Angular快速指南

开篇寄语 —— 弯道超车,为时未晚

前端领域如火如荼,工资水平也水涨船高。作为后端程序员的你,羡慕吗?但羡慕是没用的,更别提嫉妒恨了。古人曰:与其临渊羡鱼,不如退而结网。

接下来,我不但要教你结网,还要教你后端程序员弯道超车的秘诀。我将对前端领域的概念进行简要说明,并尽量用后端领域的概念来作类比,受到笔者个人背景的限制,可能会更多使用Java世界的概念来进行类比,不过.net等世界也大同小异。

笔者就是从后端程序员转型而来,深知阻挠后端程序员转型的那些拦路虎。比如:陌生的工具链、陌生的语言、庞杂的W3C标准、不一样的设计风格、不一样的编码风格等等,不过最大的拦路虎是……框架太多!每套框架都号称自己有一系列优点,让人眼花缭乱,无从抉择。

事实上,对于前端程序员来说,在这些框架之间进行选择确实很困难,因为有时候前端程序员会过于沉迷细节。比如,他/她可能在50毫秒和100毫秒的响应时间之间举棋不定,可能会为了实现细节上的优点,而影响项目管理和可维护性。最严重的问题可能还在于:误判了前端需求的增长速度和工程化开发的重要性,而这正是后端程序员们弯道超车的好时机。

细说从头

1-origin

以2004年Gmail的推出为起点,前端领域已经呈现出爆炸性增长的趋势,经过十多年的成长,它是否即将进入停滞期?我只能说“Too young, too naive!”。在Web初兴的时候,同样有编写传统桌面应用的程序员觉得它会很快成长到停滞期,不过结果大家都已经看到了。在我看来,并不是什么“前端编程技术”横空出世了,而是它的需求一直都存在,却由于技术条件不足而被压抑着 —— 直到Web技术成熟到能把它释放出来,这才导致了前端横空出世并急速膨胀的假象。

回忆一下,在“前端”的概念诞生之前,我们是怎样实现一个Web应用的?我们会先在服务器上合成一段HTML,把它发回给浏览器;之后,几乎任何操作都会向服务器发送一个请求,服务器再渲染一个完整的新页面发回来。

跳出习惯性思维,反思一下:这是自然的吗?我们为什么会需要如此复杂的过程?这其实是被迫做出的妥协:在从Netscape诞生开始的很长时间内,浏览器中的JS都是一个“玩具语言(这是JS之父说的)”:语法繁杂、坑多、解释器性能低下、无模块化机制、无成熟的工具链,无成熟的第三方库等等,JS程序员(如果当时有的话)也毫无悬念的占据了鄙视链的底端。

面对JS这样一位“猪队友”,程序员们还能怎么办?只能求助于万能的服务端语言了:它几乎不会受到浏览器的制约,可以自由使用所需的一切编程资源。幸运的是,Web技术的标准化工作在这个过程中得以蹒跚前行,而JS的标准化工作也在三大浏览器巨头的博弈中艰难的前进着。

Chrome,特别是V8引擎的诞生,终于结束了JS解释器的性能问题,更重要的是,基于V8引擎,诞生了伟大的NodeJS。NodeJS就是前端世界的JRE或.net CLR。它主要有三大贡献:

  1. 让JS语言“入侵”了后端世界和桌面世界。

    这在前端开发的襁褓期有效扩大了JS语言的适用范围,积累了大量第三方库,很多第三方库只要在合适的工具支持下也能在前端领域正常使用。

  2. 为前端开发提供了工具链。

    如今的前端世界,其工具链的复杂度和完善度已经逐渐逼近后端世界,比如:类似于gradle的构建工具gulp、grunt,类似于maven的包管理工具npm,类似于junit的测试框架karma等等,它们无一例外,都是基于NodeJS的。

  3. 提供了标准化的模块化方案CommonJS。

    在NodeJS诞生之前,模块化一直是JS世界的短板,虽然也有不少相互竞争的JS模块化方案,却都没能一统江湖,这主要是因为当时的很多前端应用都过于简单,对模块化并没有迫切需求。而随着NodeJS入侵到后端世界和桌面世界,模块化成了不得不做的事情,于是NodeJS内置的CommonJS就成为了事实性的标准。JS的最新版本ES6中内置的模块化机制就是类似于CommonJS的。

与此同时,在另一条战线上,还有一些技术在平行推进,那就是前端DOM库与前端框架。在2006年,一个名叫jQuery的DOM库横空出世,它封装了复杂的、特定于浏览器的DOM操纵类API,让程序员可以不必处理一些繁琐的细节差异,从而简化了浏览器中的DOM编程。

在那个时代,虽然尚未正式提出“前端”的概念,不过已经出现了不少事实上的前端程序。但这些前端程序相对于如今包罗万象的前端还是过于原始了,很多前端代码都只是嵌入在后端页面中的龙套。

不过,这些程序就像最早爬到岸上的鱼一样,带人们发现了一个新世界,对前端程序的需求也随之井喷。什么是“前端程序”呢?在我看来主要有如下几个特征:

  1. 客户端渲染

    与传统上借助后端生成新页面的方式不同,前端程序借助浏览器的API来呈现内容(也就是“渲染”)并处理用户动作,在这个过程中,并不需要借助服务端的运算能力,也不需要网络。

  2. 单页面

    客户端渲染技术衍生出的一个主要特征是单页面应用。因为不需要再由服务器发回新页面,所以前端程序在理论上就具备了独自渲染内容并全权处理用户交互的能力,只在必要时,才会通过Web API寻求服务器的帮助。

  3. 实时反馈

    客户端渲染技术衍生出的另一个主要特征是实时反馈。在传统的应用中,除非内嵌JS代码,否则任何反馈都需要由服务端代码生成并发回,而且编程相对复杂。这导致很少有程序能够给出实时反馈,即使做到了实时反馈的,也会因为网络延迟等问题而损害用户体验,而专业的前端程序则可以借助客户端运算轻松实现实时反馈。

随着技术的进步,前端终于具备了摆脱“石器时代”的条件,于是,前端的时代终于要开始了。

前端时代!

2-competition

以jQuery为代表的DOM库在使用中逐渐暴露出了很多缺点,特别是混杂逻辑代码和操纵DOM的代码导致难以维护。于是一大批新的前端MV*框架悄然出现了。

框架不同于库:库是一组被动式的代码,如果你不调用它,它就什么都不做;而框架不同,框架提供了启动、事件处理等各种通用性代码,你按照框架规约写自己的代码,并把它“告诉”框架,框架会在合适的时机用合适的方式调用它们。

确实,这没什么新鲜的,你早就用过Spring或asp.net了,不是吗?从这一点上来说,前端框架与后端框架大同小异。不过,前端框架还是有自己的鲜明特色的:

  1. 它们是……用JS写的。

    毫无疑问,JS尽管并不完善,但它目前是并且仍将是前端世界的霸主,我认为其中最重要的原因是:它是各大浏览器厂商利益的最大公约数。Google曾孵化了一个在浏览器和后端共用的语言Dart,不过现在连自己的浏览器都不打算直接支持它了。从技术上讲,Dart无疑是相当先进的,但现实却更加残酷。

  2. 它们是弱类型的。

    受限于JS的能力,前端框架无法访问运行时类型(就像Java或.net中的反射机制),也就无法像后端框架那样大量借助接口来定义扩展方式。因此,框架只能借助一些复杂的技巧来达成目标。当然,后续的技术发展在一定程度上改变了这一点,那就是微软的新语言TypeScript的诞生,我们稍后再展开这个话题。

  3. 它们是灵活的。

    得益于JS的动态特性和弱类型特性,前端框架也非常灵活,比如你可以把任意对象传给调用者,只要这个对象有调用者所需的属性或方法即可,而不用像Java那样明确定义接口。灵活,是优点,也是缺点 —— 在小规模、需求稳定的程序中,它可以极大的提高开发速度,用过Ruby或者Python的程序员大概深有体会;但在大规模、需求频繁变化的程序中,它将是BUG之源,用过Ruby或Python的程序员大概深有体会。

那些年,前端MV*库的竞争,其激烈程度几乎不下于各种语言的竞争。2009年,一个注定要名声大噪的框架加入了这场前端MV*大战,它叫Angular。

Hello, Angular!

3-angular-1

Angular的英文原意是“角”,也就是“锐角、直角”的“角”。它的主要开发者是Adobe Flex的开发者Misko以及很多来自Google的后端程序员,因此它有很多理念和概念来自于Flex和后端编程领域,如声明式界面(Declarative UI)、服务(Service)、依赖注入(Dependency Injection)等,并为单元测试提供了优秀的支持。可以说,它天生就有后端基因,其设计目标也是处理像传统后端一样复杂的需求。幸运或者不幸的是,它仍然是一个前端框架。它具有高度的灵活性 —— 既可以写得很规整,也可以写得很烂。当然,在某种意义上,这不应该算作Angular的问题,而是JS的“原罪”。

这种情况意味着,如果有成熟的最佳实践和优秀的开发规范,Angular程序可以写得很漂亮:简洁明了、模块清晰、分层明确、关注点分离。但在开发组意识到社区需要一份来自官方的开发规范之前,Angular 0.x和1.x版本的烂代码和坏习惯已经泛滥成灾了。

幸运的是,Angular有一个繁荣、强大的社区,社区在行动。无论是英文社区还是中文社区,都出现了一些优秀的Angular工程师,他们总结出了一些经验和教训,并给出了自己的解决之道,全凭自己的力量与热情在社区中传播。如果你是一个后端程序员,会发现这些最佳实践和开发规范似曾相识。没错,很多优秀的Angular工程师本来就是后端工程师出身。这并不奇怪,前端岁月尚浅,优秀的前端工程师当然会有很多是从优秀的后端工程师转型而来的。

但这还不是根本原因。在有些人的思维中,前端和后端好像是两个截然不同的世界。并非如此!编程之道本来就是互通的,并不存在前端的编程之道和后端的编程之道。主导这两个开发领域的设计原则不外乎就是SOLID等少数几个,无论是前端的编程规范还是后端的编程规范,都是对这些原则的实例化。

社区的努力,在一定程度上弥补了Angular早期版本的缺憾,但,这还不够。我们需要一份官方的开发规范,甚至,一个更好的Angular。后者才是重点!

Hello, Angular 2!

优秀的框架特性、繁荣的社区、广泛的应用,但都被ES5(JS的早期版本)这个猪队友给拖累了,另一个猪队友则是老版本浏览器 —— 特别是IE8及更低的版本。于是,就在Angular 1.x如日中天的时候,Angular开发组高调开始了新版本的开发工作,它就是Angular 2!这里还有很多小插曲按下不表,等我有时间开杂谈时再慢慢说。

Angular 2本身不再是用ES5写成的,而是TypeScript,简称TS。TS是微软开发的一个新语言,它是ES6的超集,这意味着,凡是有效的ES6代码都同样是有效的TS代码;另一方面,ES6是ES5的超集,所以凡是有效的ES5代码也同样是有效的TS代码。但是在ES6的基础上,TS增加了可选的类型系统以及在未来ES8中才会出现的装饰器等特性。

你想知道TS为什么这么牛?很简单,因为他爸是 —— 不,不,不是李刚,他爸是Anders Hejlsberg,如果Java程序员没听说过他还情有可原,如果是.net程序员,那就自己去面壁思过吧 —— 他是微软.net的总架构师,C#语言之父,更资深的程序员可能还用过Delphi,那是Anders的“长子”。一个人设计了三个流行的工业级语言,也真是够了。

虽然TS已经诞生了很久,但却一直没有流行起来,这主要是因为它还缺少一个“杀手级应用”。现在,Angular 2来了!

在摆脱了一个猪队友之后,Angular 2终于可以随心所欲的展示自己的风采了,比如:基于类型的依赖注入、强类型的库文件、更加便捷的语法、标准化的模块化机制等等,无法一一列举。

但还有另一个猪队友在拖后腿,那就是老式浏览器,对,说的就是你 —— IE 8!Angular从1.3开始就彻底抛弃了它,2.x就更不用说了。有一阵子,曾经传言Angular 2不支持IE 11以下所有版本的IE,不过幸好,Angular开发组终于对现实做出了妥协,否则这又会是一个重大的公关危机了。能与IE 8说再见,真好。不过,这也意味着,当你准备开始用Angular 2做项目的时候,务必先跟客户或产品经理敲定不需要支持IE 8,否则还是老老实实用Angular 1.2吧。

Angular 2,后端之友

Introduction-to-website-building-using-Angular-2-740X296

在Angular 1中就从后端借鉴过很多概念,到Angular 2自然就更进一步了。这些概念对没有做过后端开发的新前端来说会有一定的难度,不过对后端程序员来说这不过是小菜一碟。接下来我们就逐个讲讲。

服务与依赖注入

没错,它们跟后端的服务与依赖注入是同一个概念,只是在实现细节上略有不同:

后端的服务是一个单例,在Angular 2中同样如此;

后端的服务是使用类型来注入的,在Angular 2中同样如此,不过由于TS的限制,Angular 2中通常会根据类进行注入,而不是像传统的后端程序那样优先使用接口;

后端的依赖注入器是由框架提供的,Angular 2中同样如此;

后端的依赖可以进行配置,Angular 2中同样如此,不过它的配置方式更加灵活,它不需要单独的配置文件(该死的XML),而是直接用程序代码,这赋予了它额外的灵活性,却几乎没有损失(这让我想起了Grails)。

不过Angular 2的依赖注入体系比传统的后端更加灵活,它是一棵由多个注入器组成的树,这棵树跟组件树平行存在。你可以把局部使用的服务放在中下层节点上,来限制它的作用范围,减小耦合度;你可以预留一些占位(Placeholder)服务,等待调用方实现它,以达到“用组合代替继承”的效果(要了解详情,请自行分析LocationStrategy的设计);可以在不同的层级上配置同一个类的不同依赖实例,这样它就可以覆盖掉上层的配置,在必要时临时建立一个“独立王国”。

可选强类型

强类型是很多Java程序员信心的保障,但同时也因为过于繁琐而饱受抨击。

现在,它随着TS又来到了前端世界。不过不用害怕Java世界中的悲剧重演,因为TS中的强类型是“可选”强类型。这意味着你可以完全不定义变量、属性、参数等的数据类型,TS编译器也会照样放行。当你需要快速建立一个原型时,这种特性会非常有用,因为你不用现在就做很多决策。但当有一天你的原型经历了从产品经理到CEO的重重考验,终于修成正果的时候,你会发现它“太烂”了。

这是好事,这说明你在开发过程中没有浪费精力。但如果你想继续像这样把它发展成一个产品级应用,那就要悲剧了。因为代码中有太多只有你自己知道的约定和隐式接口,但新过来和你进行合作开发的人是无法和你心灵相通的。用不了多久,本来就是一团面条的代码就变成了一坨浆糊,然后你就开始了无止境的加班岁月。没错,“福兮祸之所依”,现实就是这么残酷。

为了走得更远,你先得为代码中的变量、属性、参数等标上数据类型、抽象出接口,并且基于它们建立相应的开发规范(最好能用持续集成(CI)工具进行保障)。有了这些,即使是两个负情商的大老爷们儿也能轻松做到“心灵相通”了。

加完类型之后,你仿佛回到了自己所熟悉的后端领域。现在,你的地盘儿,你做主!

测试驱动开发

如果测试驱动开发还不是你的基本功,那可能说明你在后端开发方面还有短板。即使你不是想做全栈,而是想完全转型成前端,也应该补习一下测试驱动开发的技能。因为未来的前端开发,即使在纯逻辑类代码的复杂度上都可能会赶上后端。

在1.x的时代,Angular就以其优秀的“可测试性”而著称了,Angular 2当然不会放弃这个传统优势。Angular 2的单元测试更加简单,我还是直说吧:Angular 2中单元测试的方式更像后端。在Angular 1.x的时代,单元测试中不得不使用诸如$controller(如果你不懂,请忽略它)等框架内部API,而Angular 2测试框架的设计中完全封装了它们,当你测试一个组件时,大部分时候几乎就是在测试一个普通的类。

传统的前端程序员可能不太容易理解测试驱动开发的思维方式,特别是对于没有什么后端经验的资深前端。这也同样是后端程序员实现弯道超车的好机会。随着前端职责的加重,在前端代码中,会出现越来越多的复杂逻辑,这些复杂逻辑如果没有测试驱动开发的保障,将被迫用“写代码、切换到浏览器、界面上点点看看、切换回IDE”的低效循环进行开发。

更重要的是,它很容易诞生高度耦合、恰好能用的烂代码。但在测试驱动开发的保障下,可以先从最简单的规约开始,逐步补充更多规约。在开发过程中,你只要不时瞥一眼IDE的测试控制台就可以了。这样不但开发起来更快,而且可以收获良好的代码结构,因为容易测试的代码通常也都是松耦合的。

分工,1+1 > 2

后端程序员学习前端技术时,往往会为HTML/CSS等头疼不已,这些都是相对陌生甚至完全陌生的领域,如果急于为团队贡献生产力,那么请把这些“后背”交给你的队友。

延续Angular的一贯传统,Angular 2对团队分工提供了卓越的支持,它通常会把一个界面分成模板(*.html*.jade)、样式(*.css*.scss*.less*.styl)、组件(*.js*.ts)和组件单元测试(*.spec.ts*.spec.js)等几个基本名(base name)相同的文件,它们被放在独立文件中但能很好的相互协作。

当你的前端技能还在蹒跚学步的时候,请放心的写下一些粗糙的HTML/CSS代码,比如用div搭建出丑陋但能与你所写的组件顺畅协作的html文件。然后,提交它,等你的队友帮你把它修成漂亮的产品级界面。同样的,如果你的前端队友还不太清楚该如何干净漂亮的从组件中抽取出服务,那么你就可以放心的帮他/她修改组件代码,而不用担心无意间破坏了模板和样式。

一个团队中,如果能有谦逊的super star当然好,但这种团队是可遇而不可求的,更现实的期望是一个能相互信任、各尽所能的团队。而Angular在设计时就充分考虑了团队分工的需求,要想建设这样一个团队,毫无疑问,Angular将是你的首选平台!

结束语 —— Angular 2,全栈养成计划

5-team

好吧,我承认我可耻的做了一次标题党。本文并非在煽动后端程序员去革前端程序员的命,而是希望无论是前端程序员还是后端程序员,都能成长为优秀的全栈程序员(是的,前端程序员如果理解了Angular 2中的这些概念也会更容易向后端发展)。全栈程序员由于能有效节省沟通成本(比如不用频繁协商API)而被很多开发组织寄予厚望,但真正培养起来可没那么容易。

有一阵子,培养全栈程序员的期望被放在了Fullstack JavaScript上 —— 它既能写前端程序又能写后端程序还能写桌面程序。不过事实证明,这种期望落空了。即使经过了大爆发,NodeJS在企业应用开发、大数据等领域的资源积累也远远不及Java、C#、Python,甚至将来还有被新崛起的Scala和Go超越的危险。

或许我们应该换一种思路了:全栈一定要用同一种语言写前端和后端吗?

并非如此。事实上,我们更应该看重的是编程模型、思维方式和协作模式等方面的复用,而语言层面只是细枝末节而已。所以,Java或C#,加上TS与Angular 2,给了培养全栈的新曙光。相似的概念模型、相似的思维方式、相似的协作模式,这才是全栈程序员真正的核心技能,与语言无关。

这些,才是Angular 2给专业开发团队带来的,最珍贵的礼物!

Share