弯道超车!后端程序员的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

我为什么选择Angular 2?

没有选择是痛苦的,有太多的选择却更加痛苦。而后者正是目前前端领域的真实写照。新的框架层出不穷: 它难吗?它写得快吗?可维护性怎样?运行性能如何?社区如何?前景怎样?好就业吗?好招人吗?组建团队容易吗?

每一个框架都得评估数不清的问题,直到耗光你的精力。这种困境,被称为“布利丹的驴子” —— 一只 驴子站在两堆看似完全相同的干草堆中间,不知道如何选择,最终饿死了。

 

当然,那只是一个哲学寓言。现实中,大多数人采用了很简单的策略来破解它:跟风,选择目前最流行的那个。这是一个低成本高收益的策略, 不过也有风险:成为现实版的《猴子下山》。最理想的方案还是要看出这两堆“干草”之间的差异,选择更适合自己的那个。

本文就将带你了解Angular 2这个“干草堆”的各种细节。

ALL-IN-ONE

不管是1还是2,Angular最显著的特征就是其整合性。它是由单一项目组常年开发维护的一体化框架, 涵盖了M、V、C/VM等各个层面,不需要组合、评估其它技术就能完成大部分前端开发任务。 这样可以有效降低决策成本,提高决策速度,对需要快速起步的团队是非常有帮助的。

让我们换一种问法吧:你想用乐高搭一个客厅,还是买宜家套装?

Angular 2就是前端开发领域的“宜家套装”,它经过精心的前期设计,涵盖了开发的各个层面,层与层之间都经过了精心调适, 是一个“开箱即用”的框架。

当然,你可能还记着Angular 1带给你的那些快乐以及……痛苦。这是有历史原因的。由于它是从一个用来做原型的框架演化而来的, 加上诞生时间很早(2009年,作为对比,jQuery诞生于2006年),生态不完善,连模块化机制都得自己实现 (这就是angular.module的由来,也是Angular 2中抛弃它的理由)。在长达七年的演化过程中,各种进化的遗迹非常明显,留下了不少“阑尾”。

但Angular 2就不同了,它的起点更高,整合了现代前端的各种先进理念,在框架、文档、工具等各个层面提供了全方位的支持。 比如它的“组件样式”能让你在无需了解CSS Module的前提下获得CSS Module的好处,它的Starter工程能让你在无需了解Webpack的 前提下获得Hot Module Replacement等先进特性,它能让你从Web Worker(你知道这是什么吗?)中获得显著的性能提升。

你只管在前台秀肌肉吧!剩下的,让Angular在幕后帮你搞定!

逃离“版本地狱”

如果是一两个人开发一个预期寿命不到一年的系统,那么任何框架都可以胜任。但,现实中我们都把这种系统称之为“坑”。 作为一个高度专业、高度工程化的开发组,我们不能把“坑”留给友军。这些坑中,最明显的就是“版本地狱”。

想象一下,你仅仅升级了库的小版本号,原来的软件就不能用了,损坏了N处单元测试以及M个E2E场景。 这种情况对于开发组来说简直是一个噩梦 —— 毕竟,谁也不想做无用功,更何况是一而再、再而三的。 或者,它每次小的改动都会修改大版本号 —— 对,我就是不向后兼容,你能咋地?你所能做的就是每一次决定升级都如临大敌, 不但要小心翼翼的升级这个库本身还要升级与它协作的几乎所有库。

可见,乱标版本号可能会让使用者付出巨大的代价。这不但体现在工作量上,还会体现在研发团队的招募与培养上,想象一下, 对小版本之间都不兼容的框架,研发团队都需要记住多少东西?那简直是噩梦!

但是,版本号的问题在业内早就有了事实性标准,那就是SemVer语义化版本标准。 唯一的问题是,作为框架/库的作者,遵循SemVer标准需要付出的努力是巨大的。是否愿意付出这些代价, 是一个框架(及其开发组)是否成熟的重要标志。

Angular就是这样一个框架,其开发组曾作出的努力是有目共睹的。如果你是从Angular 1.2开始使用的,那么你为1.2所写的代码 都可以毫无障碍的迁移到最新的1.5版,新的版本只是引入了新特性,而没有破坏老特性。 这是因为Angular开发组写了大量单元测试和E2E测试,借助CI来保障这种平滑过渡。只有在从1.0到1.2的迁移过程中(1.1一直是beta,忽略不计), 出现了一系列破坏性变更,这些变更被明确的记录在文档中,并且解释了做出这些变更的理由 —— 大多数是因为提升前端代码安全性。 即使你恰好遇到了这些破坏性变更,它也会给出明确的错误提示。

这些必要而无聊的工作,正是帮助我们逃离“版本地狱”的关键。是否愿意做这些无聊而琐碎的工作,是一个框架的开发组是否成熟、专业的关键特征。

模块化的技术

虽然Angular是一个ALL-IN-ONE的框架,但这并不妨碍它同时是一个灵活的框架。还记得OCP(开闭原则)吗? 一个好的设计,对扩展应该是开放的,对修改应该是关闭的。

Angular 2很好的践行了OCP原则以及SoC(关注点分离)原则。

它非常有效的分离了渲染与交互逻辑,这就让它可以很好的跟 包括React在内的渲染引擎搭配使用,除此之外,它还可以使用内存渲染引擎,以实现服务端渲染;还可以使用Native渲染引擎, 以编译出真正的原生程序(NativeScript)。

它还分离了数据供应与变更检测逻辑,从而让它可以自由使用包括RxJS以及ImmutableJS在内的第三方数据框架/工具。

不仅如此。

在Angular 1和Angular 2中最具特色的应该算是依赖注入(DI)系统了,它把后端开发中用来解决复杂问题、实现高弹 性设计的DI技术引入了前端。Angular 2中更是通过引入TypeScript赋予它更高的灵活性和便利性。

不过,Angular 2并没有敝帚自珍,把它跟框架本身紧紧黏结在一起,而是把它设计成了一个独立可用的模块。 这就意味着,无论你正在使用什么前端框架,甚至NodeJS后端框架,都可以自由使用它,并从中获益。

开放的心态

当年的浏览器大战,让人记忆犹新,Chrome的出品商Google和IE的出品商微软正是浏览器大战的两大主角。

俗话说:没有永远的朋友,也没有永远的敌人,只有永远的…… 精益求精。

正是在这样的“俗话”指导下,Google与微软相逢一笑泯恩仇,撤销了很多针对彼此的诉讼,甚至在一些领域开始合作。 而实际上,在他们公开和解之前,就已经开始公开合作了,其契机就是Angular 2。

这就要扯出一点小八卦了。

在Angular 2开发的早期阶段,由于传统JS(ES5)以及当时的ES6草案无法满足项目组的开发需求,项目组有点烦。 后来有人灵机一动开发了一种叫做AtScript的新语言,它是什么呢?一个带类型支持和Annotation支持的升级版JS。 其实在类型支持方面,TypeScript早就已经做了,而且那时候已经比较成熟,唯一的遗憾是不支持Annotation, 也就是像Java中那样通过@符号定义元数据的方式。

Angular开发组就这样孤独的走过了一小段儿时间,后来也不知道怎么这两个欢喜冤家就公然牵手了。 总之,最后的结果是:TypeScript中加入了Annotation特性,而Angular放弃了AtScript改用TypeScript。

这次结合无论对两大厂商还是对各自的粉丝,都是一个皆大欢喜的结局,称其为世纪联手也不为过。 此后,Google与微软不再止于暗送秋波,而是开始了一系列秀恩爱的动作。无论如何,对于开发者来说,这都是一个好消息。

软粉们可能还记得在6.1的微软开发者大会上,微软曾公开提及Angular 2。事实上,TypeScript目前虽然已经相当完备,但应用度仍然不高。 就个人感觉来说,Angular 2将是TypeScript的一个杀手级应用。TypeScript如果当选TIOBE年度语言,Angular 2的推出功不可没。

为什么我要特意插播这段儿故事呢?那是因为我认为一个产品的未来最终取决于开发组的未来,而不是相反。 软件的本质是人!一个心态开放、讲究合作、勇于打破陈规陋习的开发组,才是框架给人信心的根本保障。

全生命周期支持

除非你打算写一个一次性的代码,否则软件的生命周期会很长。宏观来看,真正的挑战来自多个方面,而且在不断变化。 开始的阶段,我们需要非常快速的建立原型,让它跑起来,引入最终用户来试用,这个时候,挑战来自开发速度以及可复用资产。

之后,会建立一个逐渐扩大的项目组,来完善这个原型的功能。主要的挑战是让这个原型通过重构不断进行演化, 特别是在演化的后半个阶段,产品需要保持随时可以上线的状态。但技术上的挑战那都不是事儿!关键是人。

如何让新人快速融入项目组,贡献生产力?这可没那么简单。“你先去学xxx 0.5、yyy 0.9、zzz 5.3… 还要了解它们前后版本之间的差异,我再给你讲代码”,这种话,我可说不出口。 一个优秀的框架需要对分工提供良好的支持,每个人都可以先从一些简单任务开始,逐步的从修改一个文件扩大到修改一个目录再到独立实现一个特性。

有了这种分工,每个团队成员就可以从一个成功走向一个更大的成功。这就需要框架在设计上具有良好的局部性: 你可以放心大胆的修改一部分,而不用担心影响另一部分。你可以只修改CSS,可以只修改HTML,可以只修改TS/JS, 而不用担心自己的修改破坏了其他部分。而Angular 2通过声明式界面、组件样式以及独立模板文件等对这种安全感提供了有力的保障。

再然后,如果你的软件顺利的进入了线上维护阶段,那么恭喜你,你终于迎来真正的大Boss了!这个大Boss就是可维护性。 Angular开发组有很多程序员来自Google,如果要问谁的软件维护经验最丰富,Google和微软必然位列其中。 微软通过TypeScript的强类型机制体现了自己的经验,而Google则通过将OCP、SoC、SRP等一系列软件设计原则融入Angular体现了自己的经验。 具体技术上则体现为:DI、生命周期钩子、组件等等。

最后,如果你的软件完成了历史使命需要逐步退出,是不是就能松口气了?不行,你还得继续留人维护它。 如果你选择的是一个闭源的或某个社区很羸弱的开源技术,你就把以前的主力架构师、资深程序员留下来继续维护它吧。 或者你就得鼓起勇气跟用户说:你们玩儿,我先走了。而Angular是一个大型开源项目,并得到了Google的鼎力支持。 即使经历过Angular 2项目组初期的公关失败,它仍然有着其它竞品无法企及的繁荣社区 —— 无论在全球还是在中国。 显然,无论是对传统社区资源的继承,还是对新社区资源的开拓,我们都有理由相信,Angular社区仍将一如既往地繁荣。 至少,如果你不想让自己的软件建立在一大堆由个人维护的核心库上,那么Angular毫无疑问是最好的选择。

遇见未来的标准

编程领域,创新无处不在。但对一个工程团队来说,最难得的是标准。标准意味着:

  • 招人容易。因为标准的东西会的人最多,而且人们愿意学它 —— 因为知道学了就不会浪费。
  • 养人容易。因为网上有很多教程可用,即使不得已自己做教程,性价比也是最高的。
  • 换人容易。标准就意味着不是私有技术,一个人离开了,就能用另一个人补上,而不会出现长期空缺,影响业务。

但是,标准永远会比创新慢一拍或N拍。这就意味着如果你追新,那么在获得技术上的收益的同时,也要付出工程上的代价。 固然,两者不可兼得,但是还是有一些策略能够调和两者的。最简单的策略就是:遇见未来的标准。

所谓未来的标准,就是某个标准的草案,它很有希望成为未来的标准,这代表了行业对某项技术的认可,于是,即使它还不成熟,人们也愿意为之投资。 比如虽然Google曾经提出过N种自有技术,包括SPDY、Gears、OGG等,但却并没有把它们变成私有技术,而是对社区开放以及并入W3C的标准草案。

Angular 2采用的就是这种策略。它所参照的标准草案比较多,一个是WebWorker,它借助WebWorker来把繁重的计算工作移入辅助线程, 让界面线程不受影响;另一个是WebComponents,Angular 2的组件化体系就是对WebComponents的一种实现;最后是CSS scoping, 现在虽然市面上有了很多CSS模块化技术,但事实上最终还是会被统一到CSS Scoping标准上, 届时,如果:local等关键字无法进入标准,就会成为私有技术。而Angular 2选择的方式是直接实现CSS scoping标准草案, 比如:host伪类、:host-context伪类等。 显然,采用这种策略,“遇见未来的标准”的成功率会高得多。

可以看到,Angular 2的设计哲学中贯穿着对标准化的热烈拥抱,这是我判断它将赢得未来的另一个信心来源。

速度与激情

Angular 2终于摆脱了旧的技术框架束缚,开始了对速度的极致追求。在Angular 1中,虽然绝大多数场景下性能都不是问题, 不过还是因为其代码中存在的一个用来实现脏检查的三重循环而饱受抨击 —— 似乎真有人能感受到30毫秒和100毫秒的差异似的。

不过,有软肋总是不太好。于是,在Angular 2中,通过重新设计和引入新技术,从原理上对速度进行了提升, 据官方以前提供的一个数据说是把“变更检测”的效率提升了500%。

它在“变更检测”算法上做了两项主要的改进:

  1. 在设计上,把以前的“多轮检查、直到稳定”策略改成了“一轮检查、直接稳定”策略。 当然,这会对自己的代码产生一定的限制,但实际上只在有限的少数几个场景下才会遇到这个限制,而且官方文档中也给出了明确的提示。
  2. 在实现上,把“变更检测”算法移入了由WebWorker创建的辅助线程中执行。 现代的家用电脑基本都已经是多核超线程的,但是浏览网页时实际上无法充分利用全部线程,而WebWorker提供了一个全新的机制, 让一些相对繁重的计算工作运行在辅助线程中,等执行完了再通知主线程。即使你不明白WebWorker的工作原理, 至少也能知道Ajax请求是不会阻塞界面响应的,WebWorker也一样。

除此之外,Angular还对数据源进行了抽象,你完全可以用ImmutableJS来作为Angular的数据源,获得其在提升“变更检测”速度方面的所有优势。

除了“变更检测”外,Angular以及所有前端SPA程序,都有一个通病:首次加载太慢,要下载完所有js和css才能渲染出完整的界面来。 React通过虚拟DOM解决了此问题,而Angular 2则通过独立的服务端渲染引擎解决了这个问题。 我们前面说过,Angular 2把渲染引擎独立了出来,因此可以在NodeJS中实现服务端的内存渲染。 所谓内存渲染就是在内存中把DOM结构渲染出来并发回给浏览器,这样,客户端不需要等JS代码下载完就可以显示出完整的界面了。 这种分离还带来了另一个好处,那就是SEO。SEO同样是传统SPA的一个难点,它和服务端渲染是同一个问题的两个方面, 因此随着服务端渲染的完成,SEO问题也被顺便解决了。

让你无缝升级

Angular 2开发组在早期阶段曾犯下一个严重的公关错误:过早宣布不兼容Angular 1,也不提供从Angular 1到2的升级方案。

这让Angular 2开发组付出了沉重的代价,可谓伤透了粉丝的心。作为技术人员,这种失误固然是可以理解的,但却需要付出更多的努力来弥补它。 而Angular 2确实这么做了。

在Angular 2中,开发组提供了一个UpgradeAdapter类,这个升级适配器让Angular 1.x的所有部件都能和Angular 2.x中的所有部件协同工作。

而最牛的地方在于,它让你可以一个文件一个文件的逐步升级到Angular 2,而在整个升级过程中,应用可以继续在线上跑!看着神奇吗? 其实也算不了啥,Google做这种事早就是轻车熟路了。这只不过是对Angular开发组出色的工程化开发能力的一点小小证明而已。

不过,既然如此,当初又何必急匆匆宣布不兼容呢?可见, 再出色的工程团队,如果缺少了和社区沟通的产品运营技巧,也会付出巨大代价。希望Angular开发组以后能谨言慎行,多用行动来平息质疑。

后面我还会有专门的文章以亲历者的视角讲述这个故事。

Share

为什么你的Angular代码很难测试

Angular推出有好几年的时候了, 跟其他的MV*框架相比, 它的双向绑定, 无须显式声明Model, 模块管理, 依赖注入等特点都给Web应用开发带来了极大的便利, 另外, 借助于它众多强大的原生directive, 我们几乎可以避免麻烦的DOM操作了, 除了这些, Angular还有一个很大的亮点, 那就是高度的可测试性.

今天的Web开发已经不同往日, 更多的交互与逻辑都需要在前端完成, 有时候, 前端的代码量甚至在后端之上. 怎么去保证如此多的前端逻辑不被破坏, 依赖于功能测试? 这显示不现实, 功能测试很耗时, 而且它的创建成本较高, 所以通常只用它来覆盖最基本的那部分逻辑, 另一方面, 功能测试是依赖于流程的, 如果你想验证购买页面上的某个前端逻辑, 那么你就不得不一路从产品详情页面老老实实点过来, 反馈时间太长了, 可能你要等一分多钟才知道某个功能出错了, 我们自然不想把宝贵的开发时间浪费在等待上.

我在过去一段比较长的时候里都在项目上使用Angular, 在感受到Angular带来的便利的同时, 也饱受了Angular测试的折磨, 因为我一直觉得Angular的单元测试很难写, 跟JUnit + Mockito比起来, Angular代码的单元测试真是感觉写起来不得心应手, 更别说用TDD的方式来驱动开发.

我一直在思考为什么Angular社区说Angular的测试性很高, 但是在项目上实现用起来却是另一番境地. 经过分析项目上的代码, 我觉得要想驱动测试开发Angular代码, 那么其实是对你的Angular代码提出了比较高的要求, 你要遵循Angular的风格来开发你的应用, 只有你了解了其中的思想, 你的测试写起来才会轻松.

如果你已经使用Angular有一段时间了, 但是还没有读过这篇文章, 那么我强烈推荐你去读一下: Thinking in Angular

先来看一看怎么样的Angular代码才是苗正根红的Angular代码.

避免使用任何的DOM操作

像DOM操作这样的脏活累活都应该交给Angular的原生directive去做, 我们的Angular代码应该只处理与DOM无关的业务逻辑. 来看一个简单的例子, 我们想创建一个简单的邮箱地址验证的directive, 它要实现的功能是, 当焦点从邮箱地址输入框移出的时候, 对输入框中的邮箱地址进行验证, 如果验证失败, 则向输入框添加一个样式表示输入的地址不合法, 比较糟糕的实现可能是这样的

<label for=”email”>Your email:</label>
<input id=”email” type=”text” ng-model=”email” validate-on-blur/>

angular.module(“TheNG”, [])
  .directive(“validateOnBlur”, function() {
    return {
      link: function(scope, element, attrs) {
        var validate = function(email) {
          return email.indexOf(‘@gmail.com’) !== -1;
        };
        element.on(‘blur’, function() {
          if (!validate(scope.email)) {
            element.addClass(‘error - box’);
          }
        });
      }
   };
});

上面的代码应该可以满足我们的要求(验证逻辑因为不是我们关注的重点, 所以并不完善), 而且这个directive实现起来也挺简单的, 但是现在让我们一起来分析一下为什么我们认为这种写法是比较糟糕的.

最简单的办法就是在你的directive里面去找所有与DOM操作相关的代码.

首先看到的就是on()这个事件监听器. 完全没有必要自己去监听发生在被directive修饰的元素上的事件, angular有一整套的原生directive来干这个事情, 这里正确的做法应该是使用ng-blur来处理blur事件.

下一个有问题的地方就是addClass(), angular除了提供了事件监听相关的directive外, 也提供了操作元素本身属性的directive, ng-class就可以用来替换addClass()方法.

按照这个思路修改后的代码:

<label for=”email”>Your email:</label>
<input id=”email” type=”text” ng-model=”email” ng-blur=”validate()” ng-class=”{‘error-box’: !isValid}” validate-on-blur/>

angular.module(“TheNG”, [])
  .directive(“validateOnBlur”, function () {
    return {
      link: function (scope, element, attrs) {
        scope.isValid = true;
        scope.validate = function () {
        scope.isValid = scope.email.indexOf(‘@gmail.com’) !== -1;
      };
    }
   };
  }
);

比较一下这两个版本的实现, 是不是修改后的版本更简短, 更容易理解一些. 在新的版本里面, 我们只处理了业务逻辑, 即判断一个邮箱地址是否合法, 至于何时触发验证, 验证失败或成功之后应该有怎样的样式, 我们都统统交给了angular原生directive去处理了.

从测试的角度来看, 如果想给第一个版本的实现写单元测试, 那么要准备和验证的东西都很多, 我们需要设法去触发对应元素的blur事件, 然后再验证这个元素上是否添加了error-box这个class, 根据我的经验, 有时候为了验证这些DOM更新, 你还不得不创建真实的DOM结构添加到DOM tree上去, 又增加了一部分工作量.

而版本二就简单多了, 只定义了一个Model值isValid来标识当前的邮箱地址是否合法, validate()方法会在每次失焦之后自动执行, 要为它添加单元测试, 则只需要调用一下它的validate()方法, 然后验证isValid的值就可以了. SO EASY!~

将所有第三方服务封装成Service

一个Web项目中总是无法避免地要使用一些第三方的服务, 这里讨论的主要是前端的一些第三方服务, 比如在线客服, 站点统计等, 这些代码都在我们的控制之外, 大多数时候下都是从服务提供商的服务器上下载下来的, 而我们需要在业务代码中调用这些代码.如果我们每次都是赤裸裸地以全局变量的形式来使用这些服务, 那么造成的问题就是这样的代码很难测试, 因为这些代码是不存在于我们的代码库中的, 而且内容应该也是不定时更新的, 大多数情况很多人会因为这些原因放弃到对这类操作的测试.假设我们现在需要在某些动作发生之后调用一个第三方服务, 这个第三方服务叫做serviceLoadedFromExternal, 它提供了一个API叫做makeServiceCall, 如果直接使用这个API, 那么在测试中很难去验证这个服务被执行了(因为在单元测试环境中这个服务根本不存在), 但是如果我们将这个服务包装成一个angular service, 那么就可以在测试中轻易地将它替换成一个mock对象, 然后验证这个mock对象上的方法被调用了就可以了.比较下面的两段代码:

  • 直接使用第三方服务
angular.module(“TheNG”, [])
  .controller(“AController”, function ($scope) {
    $scope.someAction = function () {
    // handle some logic here
    serviceLoadedFromExternal.makeServiceCall();
   };
});
  • 使用封装成service的第三方服务
angular.module(“TheNG”, [])
  .factory(“wrappedService”, function () {
    return {
      makeServiceCall: function () {
        serviceLoadedFromExternal.makeServiceCall();
      }
    };
 })
 .controller(“AController”, function ($scope, wrappedService) {
   $scope.someAction = function () {
   // handle some logic here
   wrappedService.makeServiceCall();
   };
 });

Angular是高度模块化的, 它希望通过这种模块的形式来解决JS代码管理上的混乱, 并且使用依赖注入来自动装配, 这一点与Spring IOC很像, 带来的好处就是你的依赖是可以随意替换的, 这就极大的增加了代码的可测试性.

尽量将Ajax请求放到service中去做

Angular中使用service来组织那些可被复用的逻辑, 除此之外, 我们也可以将service理解为是对应一个领域对象的操作的集合, 因此, 通常会将一组Ajax操作放在一个service中去统一管理.

当然了,你也可以通过向你的directive或是controller中注入$http, 但是我个人不喜欢这种做法. 首先, $http是一个比较初级的依赖, 与其实注入的业务服务不是一个抽象层级, 如果在你的业务代码中直接操作http请求, 给人的一种感觉就像是在Spring MVC的request method中直接使用HttpServeletRequest一样, 有点突兀, 另外会让整个方法失衡, 因为这些操作的抽象层次是不一样的. 其次就是给测试带来的麻烦, 我们不得不使用$httpBackend来模拟一个HTTP请求的发送.

我们应该设法让测试更简单,通过将Ajax请求封装到service中, 我们只需要让被mock的service返回我们期望的结果就可以了. 只有这样大家才会喜欢写测试, 甚至是做到测试驱动开发, 要去mock$http这样的东西, 显然是增加了测试的负担.

使用Promise处理Ajax的返回值, 而不是传递回调函数

Angular中所有的Ajax请求默认都返回一个Promise对象, 不建议将处理Ajax返回值的逻辑通过回调函数的形式传递给发送http请求的service, 而应该是在调用service的地方利用返回的promise对象来决定如何处理.

让我们通过下面的例子来感受一下:

angular.module(“TheNG”, [])
  .service(“deliveryService”, function ($http) {
    this.validateAddress = function (address, success, failure) {
      $http.post(‘/unknown’, address).then(success, failure);
    };
  })
  .controller(“DeliveryController”, function ($scope, deliveryService) {
     var acceptAddress = function () {
       console.log(‘address accepted’);
     };
     var rejectAddress = function () {
       console.log(‘address rejected’);
     };
     $scope.validateAddress = function () {
     deliveryService.validateAddress($scope.address, acceptAddress, rejectAddress);
   };
});

这里的处理办法是将快递地址验证失败或成功之后的处理函数都传给了deliveryService, 当验证结果从服务器端返回之后, 相应的处理函数会被执行. 这做写法其实是比较常见的, 但是问题出在哪里呢?

其实, 作为一个service的接口, validateAddress应该只接收一个待验证的地址, 验证完成之后返回一个验证结果就可以了, 本来应该是一个很干净的接口, 我们之所以丑陋把对应的处理函数也传进去, 原因就在于这是一个异步的请求, 所以需要在发请求的时候就将对处理函数绑定上去.

你应该已经猜到了第二个问题我会说一说对它的测试, 通常来说, 如果一个service创建成本较高或是存在外部依赖/请求的话, 我们会将这个service mock掉, 通过让mocked service直接返回我们想要的结果来让我们只关注被验证的业务逻辑.我们回头看一下上面的实现, 如果我们把deliveryService的validateAddress()方法mock掉, 那么我们根本没有办法去验证acceptAddress()和rejectAddress()里面的逻辑!

所以, 如果你的处理函数是传递给service中的API的话, 那么你的测试其实就已经跟这个API的实现绑定了, 你只有去创建一个真实的service并且让它发送HTTP请求, 你的处理函数才会被执行到.

经过这一番折腾, 你一定要说, 这测试比实现代码难写多了. 正确的打开方式应该是这样的: service的API只需要返回promise, 对应的处理函数的绑定在这个返回的promise上, 这样我们只需要mock那个service的接口让它返回一个我们期望的promise, 然后控制promise的结果让对应的处理函数被执行:

angular.module(“TheNG”, [])
  .service(“deliveryService”, function ($http) {
    this.validateAddress = function (address) {
      return $http.post(‘/unknown’, address);
    };
  })
  .controller(“DeliveryController”, function ($scope, deliveryService) {
     var acceptAddress = function () {
       console.log(‘address accepted’);
     };
     var rejectAddress = function () {
       console.log(‘address rejected’);
     };
     $scope.validateAddress = function () {
       deliveryService.validateAddress($scope.address).then(acceptAddress, rejectAddress);
     };
 });

本来打算接下来介绍一下Angular代码的单元测试的各种模式的, 写着写着篇幅有点多了, 期待下一篇吧.

Share