什么是框架?

先摘录并翻译一段 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 的地方都用箭头函数 ()=>,而不要用普通的 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 \/>"'=])。

参见 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

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